Search code examples
iosswiftbuttonuiactionsheet

Creating multiple buttons in a line for an action sheet in Swift


I am writing an app where the user controls multiple items from the home page and can switch in between them using arrow buttons or a menu. However, I would like to be able to let the user edit the names of the items through the menu, so that when the user navigates to the menu, each row has the name of the item associated with the row, and to the right of it there is a button that could pull up an alert that lets the user change the name of the item. Currently I am using an action sheet to generate the item, but I can't find a way to display multiple items on row. My code for generating the action sheet is below:

@IBAction func tapMGName(_ sender: Any) {
    let actionSheetController: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)

    for index in 1...5{
        let addMGSelectAction: UIAlertAction = UIAlertAction(title: "Mouthguard \(index)", style: .default){action -> Void in
            let mainStoryboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
            let SettingsViewController : UIViewController = mainStoryboard.instantiateViewController(withIdentifier: "SettingsViewController") as UIViewController
            self.present(SettingsViewController, animated: false, completion: nil)
        }
        actionSheetController.addAction(addMGSelectAction)
    }
    let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .destructive, handler: { (action) -> Void in })
    actionSheetController.addAction(cancelAction)
    self.present(actionSheetController, animated: true, completion: nil)

}

What I want the menu to look like


Solution

  • Unfortunately you need to build the controller from scratch. While it might seem daunting, the hardest part is the presentation logic. afterwards it's just a matter of subclassing this main class per-ui use case and giving each of those subclasses their own logics on build.

    Here's your best approach for custom AlertControllers: https://gist.github.com/murphman300/4a56ace35d0ccdbf3a0c923b4ec7dc96

    You use a main class like the one in this gist file here, pay attention to the comments for each part.

    Then when you subclass each type

    //An example subclass of PopUpAlertController
    class SpecificAlertControllerSubclass : PopUpAlertController {
    
        override func set() {
            super.set() //Good idea to call the super., since you might want to add basic stuff all the time, in that case you would do this in the original set() method, not here in the subclass' set() override.
    
            //Here you add your own content.. that you instantiate in the subclass. The fact set() is being called in viewDidLoad() means you don't need to worry about calling it later on.
    
        }
    
    }
    
    class TestController : UIViewController, PopUpAlertControllerDelegate {
    
    
        var popup : SpecificAlertControllerSubclass?
    
    
        func bringUpPopUp() {
            popup = SpecificAlertControllerSubclass()
            popup?.delegate = self
            present(popup!, animated: true) { 
                print("Popup Presented")
            }
        }
    
    
        /*
         The Delegate methods you will always need to consider when using it.
        */
    
        func popUp(controller: PopUpAlertController, didDismiss withInfo: Any?) {
            if let pop = popup, controller == pop {
                //checks if the delegated controller is popup..
                popup = nil
                let info = withInfo != nil ? String(describing: withInfo!) : "unknown"
                print("Dismissed PopUp, with reason: \(info)")
            }
        }
    
        func popUp(controller: PopUpAlertController, selected item: [String : Any]?) {
            //Here is where you would handle the user selecting one of your options. Dismissing the popup andPresenting another controller. or if you want the popup subclass to handle the logic and the next controller, you don't call this method, but handle it in the subclass object.
        }
    
    }
    

    So this will popup a view in the same way as UIAlertControllers. However, the look of it completely depends on you. Certain things to remember when using this kind of class:

    1. Set the initial position of the popUp view in the set override.
    2. In the set() Override: ALWAYS remember that you're adding subviews to the popUp view, not to view. Of course, you can rename popUp to whatever you want..
    3. If you want to change the presentation of the controller, you need to change the overrides, and presentation variables in your subclasses, not the main class. That way you keep the general one intact for quick re-use, but your specific use cases are still distinguishable.
    4. I've found that overriding viewDidAppear is more useful if done at the subclass level, for the same reason as the previous point. You can plug in separate presentation methods there.
    5. To change the dismissal animations, you'd have to override the methods themselves, without calling super in the overrides.

    Points 3 and 4 mean that SpecificAlertControllerSubclass becomes something like this:

    class SpecificAlertControllerSubclass : PopUpAlertController {
    
        override func set() {
            super.set() //Good idea to call the super., since you might want to add basic stuff all the time, in that case you would do this in the original set() method, not in the subclasses.
    
            //Here you add your own content..
    
            modalPresentationStyle = //which ever one you want
    
            modalTransitionStyle = //which ever one you want
    
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .allowAnimatedContent, animations: {
                self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
                self.popUp.alpha = 1
            }) { (v) in
    
            }
        }
    
    }
    

    Like the comment on the second delegate method explains, you need to decide which controller should handle the action logic, in my opinion, depending on if you present new controllers when those actions are pressed, or if you're just switching up the UI. In the later case, you can make the didSelect method from the PopUpAlertController protocol an optional method. Here's a good tutorial on delegates.

    I also set popUp's alpha to 0, this is for your use case which is a center-screen fade-in effect. Of course, if you want the popup to slide in, you can change that alpha value in the set() override. If you ALSO want a slide in animation, you just need to set the initial position of popUp in the set() override, and animate it where you want it to appear in the viewDidAppear override. So take from this that this approach does require you to keep track of how you change the main class' properties in your subclass, however, the customizability of it all is pretty neat when you get the hang of it.

    From here, you can pretty much do whatever you want. Just remember to handle the ui transition for the popup when an action is selected so as to make sure the transition is smooth etc.

    As for your desired look. I think your best way to go would be to use a UITableView with full-length borders, or a UICollectionView. You should delegate which ever one you decide in PopUpAlertController, interpret the selection method with their respective didSelectItemAt methods and call the PopUpAlertControllerDelegate's popUp(controller: PopUpAlertController, didDismiss withInfo: Any?). That'll allow you to set custom icons for each row.

    Here's a good tutorial on UICollectionView's done programmatically : https://www.youtube.com/watch?v=3Xv1mJvwXok&list=PL0dzCUj1L5JGKdVUtA5xds1zcyzsz7HLj