Budgie IOS Series – a native IOS budgeting app with Salesforce that I am building for my family: Phase 2 – sheetPresentationControllers that will support the expense submission to Salesforce

Posted by

Hey readers, welcome back! With our new baby boy on the way, I started getting REALLY into budgeting. I became super interested in where our money was going and ways that we could improve our spending habits. In my first post, Introducing Budgie – A native IOS budgeting app with Salesforce that I am building for my family: Phase 1 – Building the OAuth 2.0 User-Agent Flow for Mobile App Integration with Salesforce, I talked about the inspiration behind building this IOS app and the overall architecture of the Mobile User Agent Integration flows with Salesforce. Check it out if you are interested in learning more.

In today’s post, we are going to cover sheetPresentationControllers (aka. bottom sheets) in swift that allow you render a half or full sheet from the bottom of the screen and expose information or drive action that is separate from the main screen. This capability on the app will eventually drive how expenses are selected and submitted to Salesforce from the app.

Sheet Presentation Controllers

sheetPresentationControllers in swift

In IOS15 (look at me acting like I am an expert in IOS) they came out with a super easy way in native swift to render bottom sheets in roughly a few lines of code. For those who are unfamiliar with the way storyboard design works within IOS, it looks something like this:

Each of these storyboards that you see, depending on your use case, are typically bound to a UIViewController that is operating its functionality from the backend. So you will typically see a class like this associated with a storyboard that you can use to interact with other parts of your application:

//
//  ViewController.swift
//  SalesforceAuthentication
//
//  Created by Taylor Ortiz on 3/29/22.
//
import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

If you are familiar with Salesforce or React and any sort of MVC pattern that typically goes with that type of development, this is really no different. The screen interacts with the view controller and drives action through that. As you can see in the video above, I loaded in an asset (image) into my xcode project in the form of a ‘plus’ sign so that the user can initiate the bottom screen from. In Swift, I can cntrl + drag from the button on the screen to my view controller and make an @IBOutlet variable that creates a literal metadata bound relationship from the controller to the screen.

Next, very similar to an addEventListener() in JS, I can add a UITapGestureRecognizer in the ViewDidLoad() (this is the function that gets called immediately at the onset of this view controller coming into view) that can bind my button to an action in my controller that is called when the button is tapped.

let tapGR = UITapGestureRecognizer(target: self, action: #selector(self.addExpenseImageTapped))
addExpenseImageView.addGestureRecognizer(tapGR)
addExpenseImageView.isUserInteractionEnabled = true

Once the image is tapped, it is typically best practice to render a brand new view controller that is specifically designed to handle all bottom sheet interaction

— ExpenseSelectorViewController enters the chat —

But first! we need to call it and initiate the bottom sheet to be rendered, but how? Remember that super easy out of the box method with a few lines of code I mentioned above? Lets use that!

@objc func addExpenseImageTapped(sender: UITapGestureRecognizer) {
    
    // initialize the new view controller and assign to a variable    
    let viewControllerToPresent = ExpenseSelectorViewController()
    if let sheet = viewControllerToPresent.sheetPresentationController {
        // detents are the capacity of which the screen can go. if you select just     
        medium in this array, it will only support medium view and not expand larger
        sheet.detents = [.medium(), .large()]
        sheet.largestUndimmedDetentIdentifier = .medium
        sheet.prefersScrollingExpandsWhenScrolledToEdge = false
        sheet.prefersEdgeAttachedInCompactHeight = true
        sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
        // you know that horizontal bar that appears on apps sometimes that lets us know 
        that this can be interacted with and swiped? This is it!
        sheet.prefersGrabberVisible = true
        // know the .this headache of JS? somewhat of the same thing
        sheet.delegate = self
    }
    // this function will literally render your view controller which, bound to your  
    actual storyboard, will render 
    present(viewControllerToPresent, animated: true, completion: nil)
}

We are all familiar with the addExpenseImageTapped function by now and with the comments I left above, please feel free to read through them to understand what some of the heavy hitters attributes do that drive influence to the user experience.

This is where it gets even more interesting. In swift, at least from what I know so far (subject to change), there is no real way to preconfigure a screen to be dynamically rendered at the onset of a presented sheet presentation controller, so you have to programmatically build the page!

import UIKit

class ExpenseSelectorViewController: UIViewController, RestoreSheetDefaults, UISheetPresentationControllerDelegate {
    
    var buttonArray: [UIButton] = []
    
    var addExpenseLabel = UILabel()
    
    var noExpenseSelectedLabel = UILabel()
    
    var selectedExpense = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let sheet = self.sheetPresentationController {
            sheet.delegate = self
        }

        // Do any additional setup after loading the view.
        
        overrideUserInterfaceStyle = .light
        
        view.backgroundColor = UIColor.white
        
        self.addExpenseLabel = UILabel(frame: CGRect(x: 40, y: 40, width: 300, height: 40))
        self.addExpenseLabel.font = UIFont.boldSystemFont(ofSize: 25.0)
        self.addExpenseLabel.text = "Add Expense:"

        self.view.addSubview(self.addExpenseLabel)
        
        self.buttonArray = [
            self.configureExpenseButton(title: "Groceries", systemIcon: "cart.fill", x: 40, y: 100, width: 150, height: 45),
            self.configureExpenseButton(title: "Takeout", systemIcon: "takeoutbag.and.cup.and.straw.fill", x: 200, y: 100, width: 150, height: 45),
            self.configureExpenseButton(title: "Coffee", systemIcon: "cup.and.saucer.fill", x: 200, y: 155, width: 150, height: 45),
            self.configureExpenseButton(title: "Gas", systemIcon: "fuelpump.fill", x: 40, y: 155, width: 150, height: 45),
            self.configureExpenseButton(title: "Shopping", systemIcon: "creditcard.fill", x: 200, y: 210, width: 150, height: 45),
            self.configureExpenseButton(title: "Medical", systemIcon: "heart.fill", x: 40, y: 210, width: 150, height: 45),
            self.configureExpenseButton(title: "Travel", systemIcon: "airplane", x: 40, y: 265, width: 150, height: 45),
        ]
        
        for btn in self.buttonArray {
            self.view.addSubview(btn)
        }
        
        self.noExpenseSelectedLabel = UILabel(frame: CGRect(x: 80, y: 500, width: 300, height: 40))
        self.noExpenseSelectedLabel.font = UIFont.boldSystemFont(ofSize: 25.0)
        self.noExpenseSelectedLabel.text = "No Expense Selected"

        self.view.addSubview(self.noExpenseSelectedLabel)
    }
}

You can read through the code above, but essentially these are the steps I took within the application to build the view you see in the video above:

  • Step 1: Set the delegate of the sheetPresentationController to self.
    • Why does this matter?
      • Eventually, I will want my application to perform different behaviors when it detects a change to the detents of the sheet presentation controller (remember the .medium() and .large() page size?). Swift supports a function and extension called func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) -> This function will allow me to manipulate UX behavior when the user is interacting with it in a meaningful way
  • Step 2: Set the background of the view controller to white so that it has a solid non transparent color that overrides the view state of the tab bar items that you see before the ‘plus’ button is tapped
  • Step 3: Create the ‘Add Expense’ label
    • This is super cool because it actually allows me to create a label programmatically by assigning a label and literally assigning the coordinates of its position on the screen.
  • Step 4: Add all of the buttons, use out of the box apple icons and configure their screen position.
    • I created a function called configureExpenseButton() that takes in a title, system icons and a handful of coordinates so that the controller knows where to put them on the page. I stuck all of them in an array and looped through them to add them to the page at their designated place.
    • Fun fact: I actually plan on creating custom metadata to support this in the future so that when I add a new expense, it just dynamically appears on my app. Pretty cool!
    • Another fun fact, Apple has an insanely cool icon library called SF Symbols that you can use native within your application and I have loved it!
  • Step 5: Add another label to notify the user that no expense has been selected yet which is a repeat of step 3
  • Step 6: Create a function that was previously added as a target function to all of the buttons that were created called buttonAction().
    • A couple of cool things about what this function will do:
      • It will hide all of the buttons and auto create a label thats pushed to the top on the category you select. It will also auto render itself in a [.large()] detent which will take up the whole screen.
      • When a user decides that they do not want to create an expense or wish to select a different one, they can simply swipe back down to half way (which will bring back the expense items to select) and choose a different one or they can swipe completely down altogether to fully close it. This will restore the expense submission back to its default view that you see in the video
    • This contributes to a lot of research I have done about mobile user experience and important attributes of ease of use and accessibility in the applications we build so that everyone can use them

So where am I off to next? My next task on my Jira board (yes, I have a Jira board for this) is to create the actual submission view and link that up with the Apex Salesforce rest service to start submitting expenses to Salesforce and begin our journey with tracking our items within Salesforce. Here is a sneak peak of what it will look like:

It’s a work in progress but I am really excited about the progress being made and how it is coming together. Next time, we will discuss Access/Refresh Token Integration Flows with Salesforce, walk through my Integration Utility class, and discuss properly storing tokens for future use and compliance with Apple’s IOS security standards.

As always it is important for me to say that I am not an expert. If you see something in here that could be improved in practice or implementation or just have general questions, please do not hesitate to reach out. I am always thrilled for an opportunity to learn new ways of working. I hope you have a great rest of your day.

Happy coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s