Search code examples
iosswiftuiviewcontrolleruinavigationcontrollerwkwebview

Dynamically switching device orientation with single ViewController


I'm currently developing an app that has a single ViewController with a WKWebView object that acts as a client for a web app. It uses JavaScript communication and injection to allow for objects in native Swift to interact with the WebView.

My Objective

I have a function, urlDidChange(_ url: String), that fires whenever the WKWebView's raw URL value changes. I'm trying to dynamically set the orientation restrictions, depending on the value of said new url, and force-rotate the device to adapt those restrictions once a condition is met.

I'm not sure if this extra information is important, but thought that I'd include it anyway: The UIViewController is also embedded in a UINavigationController. The native NavBar really helps the client feel more like a native app. I haven't setup any custom classes for it and have simply been using let navBar = navigationController?.navigationBar.

Desired Example Usage

func urlDidChange(_ url: String) {
    if url.contains("/dashboard") {
        UIInterfaceOrientationMask = .portrait
    } else if url.contains("/builder") {
        UIInterfaceOrientationMask = [.portrait, .landscapeRight]
    } else {
        UIInterfaceOrientationMask = .all
    }
    // Set new orientation properties
    // Force device rotation based on newly set properties
    UIViewController.attemptRotationToDeviceOrientation()   
}

Current Code

This is my current setup. I have attempted the following, with no luck:

enum Page {
    
    var orientation: UIInterfaceOrientationMask {
        switch self {
        case .login:        return getDeviceOrientation()
        case .dashboard:    return getDeviceOrientation()
        case .newProject:   return getDeviceOrientation()
        case .builder:      return getDeviceOrientation()
        case .other:        return getDeviceOrientation()
        }
    }
    
    func getDeviceOrientation() -> UIInterfaceOrientationMask {
        let phone = Device.isPhone()
        switch self {
        case .login:
            if phone { return .portrait }
            else { return .all }
        case .dashboard:
            if phone { return .portrait }
            else { return .all }
        case .newProject:
            if phone { return .portrait }
            else { return .all }
        case .builder:
            if phone { return [.portrait, .landscapeRight] }
            else { return .all }
        case .other:
            if phone { return .portrait }
            else { return .all }
        }
    }

ViewController

Finally, to apply the new orientation properties, I use this:

func urlDidChange(_ url: String) {
    let page = Page.get(forURL: url)    // Returns Page case for current URL
    
    UIDevice.current.setValue(page.orientation.rawValue, forKey: "orientation")
    UIViewController.attemptRotationToDeviceOrientation() 
}

Bonus question

Is there a more efficient way of using enumerations to combine the Device properties with my Page enum (ie. case .builder with different return values for when Device.isPhone or when Device.isPad)?

As you can see in the code above, I'm simply using if statements to determine which output to provide right now:

case .builder:
    if phone { return [.portrait, .landscapeRight] }
    else { return .all }

Solution

  • If your ViewController is inside a UINavigationController, that might be the reason. Can you try using a custom UINavigationController, that retrieves the orientation from your ViewController? Something like this:

    struct OrientationConfig {
        let phone: UIInterfaceOrientationMask
        let pad: UIInterfaceOrientationMask
    
        static let defaultConfig = OrientationConfig(phone: .portrait, pad: .all)
    }
    
    enum Page {
        case login
        case dashboard
        case newProject
        case builder
        case other
    
        static let orientationConfigs: [Page:OrientationConfig] = [
            .login: OrientationConfig.defaultConfig,
            .dashboard: OrientationConfig.defaultConfig,
            .newProject: OrientationConfig.defaultConfig,
            .builder: OrientationConfig(phone: [.portrait, .landscapeRight],
                                        pad: .all),
            .other: OrientationConfig.defaultConfig
        ]
    }
    
    class MyViewController: UIViewController {
    
        var page: Page = .login
    
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            if let config = Page.orientationConfigs[page] {
                return Device.isPhone ? config.phone : config.pad
            }
            return super.supportedInterfaceOrientations
        }
    
    }
    
    class MyNavigationController: UINavigationController {
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            if let viewController = topViewController as? MyViewController {
                return viewController.supportedInterfaceOrientations
            }
            return super.supportedInterfaceOrientations
        }
    }