Search code examples
iosiphoneipadmodalviewcontrollersize-classes

Override Formsheet Size-Classes


The ViewController in question is embedded in a UINavigationController and presented as a .FormSheet like so:

class PLViewController:UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationController?.navigationBar.translucent = false
    }

    /// Embeds self into a UINavigationController, adds a "done" button to the navVC and uses the passed ViewController to present self embedded in the NavigationController.
    /// - Parameters:
    ///     - presentingVC: ViewController which will present the formSheet.
    ///     - animated: If TRUE, the presentation of the formsheet will be animated.
    func presentAsFormSheet (presentingVC:UIViewController, animated:Bool, completion:(() -> Void)?) {
        let navVC = UINavigationController(rootViewController: self)
        navVC.modalPresentationStyle = .FormSheet
        let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action: Selector("dismissFormSheet"))
        doneButton.tintColor = GlobalVars.cautionColor
        navigationItem.setLeftBarButtonItem(doneButton, animated: false)
        presentingVC.presentViewController(navVC, animated: true, completion: completion)
    }

    /// Dismisses this ViewController with animation from a modal state.
    func dismissFormSheet () {
        dismissViewControllerAnimated(true, completion: nil)
    }

}

When the VC is presented in the "floating" formsheet manner (non-fullscreen modal) it needs to essentially behave as is shown below:

enter image description here

Additionally, the layout needs to be manipulated further if the application is in either 1/3 split screen (but not 2/3's) or on iPhone in general. The iPhone part is easy enough to figure out, basically check the device type and respond accordingly in code.

Is there a to know that, when on iPad, a) in split-screen mode and b) using 1/3, 1/2 or 2/3?


Solution

  • Success! By subclassing UIViewController, I was able to create some methods to programmatically adjust the .FormSheet modal to respond to the presentingVC's trait changes.

    The UIViewController Subclass:

    class MyViewControllerSubclass: UIViewController {
    private func deviceOrientation () -> UIDeviceOrientation {
            return UIDevice.currentDevice().orientation
        }
    
        private func getScreenSize () -> (description:String, size:CGRect) {
            let size = UIScreen.mainScreen().bounds
            let str = "SCREEN SIZE:\nwidth: \(size.width)\nheight: \(size.height)"
            return (str, size)
        }
    
        private func getApplicationSize () -> (description:String, size:CGRect) {
            let size = UIApplication.sharedApplication().windows[0].bounds
            let str = "\n\nAPPLICATION SIZE:\nwidth: \(size.width)\nheight: \(size.height)"
            return (str, size)
        }
    
        /// Called when the UIViewController's traitCollection has changed.
        /// - Description:
        /// Include any changes that need to be made when the trait collection changes.
        func respondToSizeChange (layoutStyle:LayoutStyle) {
    
        }
    
    
        enum LayoutStyle: String {
            case iPadFullscreen         = "iPad Full Screen"
            case iPadHalfScreen         = "iPad 1/2 Screen"
            case iPadTwoThirdScreen     = "iPad 2/3 Screen"
            case iPadOneThirdScreen     = "iPad 1/3 Screen"
            case iPhoneFullScreen       = "iPhone"
        }
    
        /// - Returns: a `LayoutStyle` value containing information about what portion of the screen the application is consuming.
        internal func determineLayout () -> LayoutStyle {
            if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
                return .iPhoneFullScreen
            }
            let screenSize = getScreenSize().size
            let appSize = getApplicationSize().size
            //let orientation = deviceOrientation()
            let screenWidth = screenSize.width
            let appWidth = appSize.width
            if screenSize == appSize {
                return .iPadFullscreen
            }
    
            if deviceOrientation() == .Portrait && screenSize != appSize {
                return .iPadOneThirdScreen
            }
            // In case the math isn't exact, set a range.
            let lowRange = screenWidth - 15
            let highRange = screenWidth + 15
    
            if lowRange / 2 <= appWidth && appWidth <= highRange / 2 {
                return .iPadHalfScreen
            } else if appWidth <= highRange / 3 {
                return .iPadOneThirdScreen
            } else {
                return .iPadTwoThirdScreen
            }
    
        }
    
        override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
            super.traitCollectionDidChange(previousTraitCollection)
            respondToSizeChange(determineLayout())
        }
    }
    

    And on the ViewController that presents the modal:

    class MainViewController:UIViewController {
    
        /// The modally-presented formSheet.
        var presentedFormSheet:MyViewControllerSubclass?
    
        /// Modally presents the VC in `.FormSheet` style and sets the local `presentedFormSheet` variable to the VC.
        func showContentRequirementVC() {
            let vc = MyViewControllerSubclass(nibName: "myCustomViewController", bundle: nil)
            vc.modalPresentationStyle = .FormSheet
            presentedFormSheet = vc
            presentViewController(vc, animated: true, completion: nil)
        }
    
        // When the trait collection changes, tell the presentedFormSheet about it.
        override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
            presentedFormSheet?.respondToSizeChange(determineLayout())
        }
    }
    

    Results: As shown in the below screenshots, the solid red bar is now only present when the formsheet is consuming the entire area of the app. There is enough information in the passed data that you can respond to full, 2/3, 1/2 and 1/3 individually as well. The modal will also respond to changes even after it has been presented.

    Also demonstrated by the below screen shots is the reason this question is relevant. As you can see, the side-by-side tables work great until we get to 1/3 or iPhone. At this point, they need to be either on a tab-bar controller or use some other method so that only one table is shown at a time and the user can toggle between them. This particular VC is built with UIStackViews, so the behavior can easily be programmatically adjusted to suit the UI layout.

    Full Screen full-screen

    ** 2/3 Screen** 2/3 screen

    1/2 Screen 1/2 screen

    1/3 Screen 1/3 screen

    Slide-Over slide-over