{"id":15038411,"url":"https://github.com/dmytro-anokhin/split-or-flip","last_synced_at":"2025-04-10T01:22:16.882Z","repository":{"id":134611104,"uuid":"106166241","full_name":"dmytro-anokhin/split-or-flip","owner":"dmytro-anokhin","description":"Adaptive split view controller","archived":false,"fork":false,"pushed_at":"2017-10-08T10:19:41.000Z","size":288,"stargazers_count":13,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-24T03:03:59.369Z","etag":null,"topics":["swift","swift-4","swift4","uikit","uiviewcontroller"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmytro-anokhin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-08T10:02:34.000Z","updated_at":"2023-04-01T05:04:48.000Z","dependencies_parsed_at":"2023-08-15T07:34:36.757Z","dependency_job_id":null,"html_url":"https://github.com/dmytro-anokhin/split-or-flip","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2Fsplit-or-flip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2Fsplit-or-flip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2Fsplit-or-flip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2Fsplit-or-flip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmytro-anokhin","download_url":"https://codeload.github.com/dmytro-anokhin/split-or-flip/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248138308,"owners_count":21053848,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["swift","swift-4","swift4","uikit","uiviewcontroller"],"created_at":"2024-09-24T20:38:23.042Z","updated_at":"2025-04-10T01:22:16.822Z","avatar_url":"https://github.com/dmytro-anokhin.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\n# Building adaptive container view controller\n\n## Adaptivity\n\nUIKit went a long way to support various devices and screen sizes in one app. Starting from universal apps for iPhone and iPad, introducing Auto Layout, multiple iterations on rotation API, and up to adaptive apps. Today's number of devices:\n\n![Devices and Orientations](images/DevicesAndOrientations.png)\n\nNumber of possible screen sizes looks scary:\n\n![Compact Width](images/CompactWidth.png)\n\n![Regular Height](images/RegularHeight.png)\n\nApplying concept of adaptivity is a must for a modern app.\n\nAdaptivity is a complex process. It includes responsive layout, adaptive presentations, margins, text and fonts, etc.\n\nOne of the key concepts is adaptive views. When screen is large enough we want to show complete UI. And if space is limited, we need to make a decision: do we need to adjust layout or hide parts of the screen by changing navigation. Creating adaptive container view controllers allows us to reflect screen size change on navigation structure.\n\n## Container view controllers\n\nContainer view controller is simply a view controller that manages other view controllers. Examples in UIKit include `UINavigationController`, `UITabBarController`, and `UISplitViewController`.\n\n\n## UISplitViewController\n\nQuote from documentation:\n\n\u003e A container view controller that presents a master-detail interface.\n\u003e In a master-detail interface, changes in the primary view controller (the master) drive changes in a secondary view controller (the detail). The two view controllers can be arranged so that they are side-by-side, so that only one at a time is visible, or so that one only partially hides the other.\n\nMany iOS apps use split view controller: Mail, Reminders, Notes, ..\n\nMaster-detail interface usually implies arranged list of items in the master view controller. The detail view controller displays selected item. On iPhone this interface is represented by the navigation stack: user selects item in the list and the detail screen is pushed to the navigation stack.\n\n## Custom split view controller\n\nThere are common cases when view controllers are equal. For instance, when we want different representations for a set of items. This can be map and a list of places or cover flow and a grid of files. `UISplitViewController` is not intended to handle this cases. We need to build custom split view controller and make it adaptable.\n\n## Designing split view controller\n\n![Split View](images/SplitView.png)\n\n#### Display Mode\n\nI want my custom split view controller to support similar display modes:\n- both view controllers are displayed at the same time, side-by-side;\n- one view controller at a time is displayed.\n\n```swift\nenum DisplayMode {\n    // Both view controllers are displayed at the same time, side-by-side\n    case sideBySide\n    // One view controller at a time is displayed\n    case one\n}\n\nprivate(set) var displayMode: DisplayMode = .sideBySide\n```\n\nDisplay mode must change when screen size changes. I need to know when screen is large enough for side-by-side presentation. Absolute size in points is not important. I use horizontal size class to decide best display mode.\n\n```swift\n/// Suggests display mode based on trait collection\nprivate func suggestDisplayMode(for traitCollection: UITraitCollection) -\u003e DisplayMode {\n    return traitCollection.horizontalSizeClass == .regular ? .sideBySide : .one\n}\n```\n\n#### Switching Focus\n\nWhen one view controller is displayed, I have two routines to handle. First is transition (flip) between displayed and hidden views. Second is the display mode change. When display mode transitions back and forth, I want to keep previously displayed view controller.\n\nOnce more, enum helps to keep track of the state.\n\n```swift\n/// Defines visible view controller when one at a time displayed\nprivate enum DisplayFocus {\n    /// When .displayMode set to .one, left view is visible\n    case left\n    /// When .displayMode set to .one, right view is visible\n    case right\n}\n\nprivate var displayFocus: DisplayFocus = .left\n```\n\n#### Layout\n\nIn side-by-side mode my split view controller will divide available area into two columns. The left column will occupy fixed space. And I want it to reflect screen size. Therefore, when my screen is wider than taller, the left column will ocupty slightly more space.\n\nI'm deliberately avoid using landscape and portrait terms. Layout should not depend on device orientation.\n\n```swift\n/// Width of the left view in side-by-side display mode\nprivate var leftColumnWidth: CGFloat!\n\n/// Suggests width of the left column in side-by-side display mode\nprivate func suggestLeftColumnWidth(for availableSize: CGSize) -\u003e CGFloat {\n    return availableSize.width \u003e availableSize.height ? 440.0 : 320.0\n}   \n```\n\n`ContainerView` helps me manage positioning of views without constraints.\n\n```swift\nprivate class ContainerView: UIView {\n\n    var contentView: UIView? {\n        willSet {\n            contentView?.removeFromSuperview()\n        }\n\n        didSet {\n            guard let contentView = contentView else { return }\n            addSubview(contentView)\n        }\n    }\n\n    override func layoutSubviews() {\n        super.layoutSubviews()\n        contentView?.frame = bounds\n    }\n}\n\nprivate var leftContainerView: ContainerView!\nprivate var rightContainerView: ContainerView!\n```\n\nFrame calculations are separated in a set of functions.\n\n```swift\n    private func fullFrame(for availableSize: CGSize) -\u003e CGRect\n    private func fullLeftFrame(for availableSize: CGSize) -\u003e CGRect\n    private func fullRightFrame(for availableSize: CGSize) -\u003e CGRect\n    private func leftColumnFrame(for availableSize: CGSize) -\u003e CGRect\n    private func rightColumnFrame(for availableSize: CGSize) -\u003e CGRect\n```\n\nCreating separate functions eliminates boilerplate code, allows me to easily change calculation logic, and implement additional features. For instance, frame calculation functions implement RTL support.\n\n```swift\nprivate func leftColumnFrame(for availableSize: CGSize) -\u003e CGRect {\n    switch traitCollection.layoutDirection {\n        case .unspecified, .leftToRight:\n            return CGRect(x: 0.0, y: 0.0, width: leftColumnWidth, height: size.height)\n        case .rightToLeft:\n            return CGRect(x: size.width - leftColumnWidth, y: 0.0, width: leftColumnWidth, height: size.height)\n    }\n}\n```\n\n## Implementing container view controller\n\nImplementing a container view controller implies establishing relationship between a container view controller and its child view controllers. UIKit forwards various events through chain of responder objects. This happens under the hood. But we must make sure child view controllers are part of it. \n\nTo establishing relationship we need:\n\n\u003e1. Call the `addChildViewController:` method of your container view controller.\nThis method tells UIKit that your container view controller is now managing the view of the child view controller.\n\u003e2. Add the child’s root view to your container’s view hierarchy.\nAlways remember to set the size and position of the child’s frame as part of this process.\n\u003e3. Add any constraints for managing the size and position of the child’s root view.\n\u003e4. Call the `didMoveToParentViewController:` method of the child view controller.\n\n```swift\n    var leftViewController: UIViewController? {\n        willSet {\n            guard let child = leftViewController else { return }\n            child.willMove(toParentViewController: nil)\n            child.view.removeFromSuperview()\n            child.removeFromParentViewController()\n        }\n\n        didSet {\n            guard let child = leftViewController else { return }\n\n            loadViewIfNeeded() // Make sure the view is loaded\n\n            addChildViewController(child)\n            child.view.frame = leftContainerView.bounds\n            leftContainerView.contentView = child.view // Layout managed by container view\n            child.didMove(toParentViewController: self)\n        }\n    }\n```\n\nThis steps described in detail in View Controller Programming Guide for iOS - [Implementing a Container View Controller](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html#//apple_ref/doc/uid/TP40007457-CH11-SW1).\n\n### Implementing adaptive view controller\n\nThere are couple of methods in `UIViewController` that reflect adaptivity.\n\n`UIViewController` receives `viewWillLayoutSubviews` callback when layout of its view is about to change. This is good time to manipulate things.\n\n```swift\noverride func viewWillLayoutSubviews() {\n    super.viewWillLayoutSubviews()\n\n    // Update display state\n    displayMode = suggestDisplayMode(for: traitCollection)\n    \n    let size = view.bounds.size\n\n    switch displayMode {\n        case .sideBySide:\n            // Add subviews. Order is not important\n            view.addSubview(leftContainerView)\n            view.addSubview(rightContainerView)\n\n            // Update left column width\n            leftColumnWidth = suggestLeftColumnWidth(for: size)\n            \n            // Position container views\n            leftContainerView.frame = leftColumnFrame(for: size)\n            rightContainerView.frame = rightColumnFrame(for: size)\n\n        case .one:\n            // Add/remove subviews based on display focus\n            switch displayFocus {\n                case .left:\n                    view.addSubview(leftContainerView)\n                    leftContainerView.frame = fullFrame(for: size)\n                    rightContainerView.removeFromSuperview()\n                case .right:\n                    view.addSubview(rightContainerView)\n                    rightContainerView.frame = fullFrame(for: size)\n                    leftContainerView.removeFromSuperview()\n            }\n    }\n}\n```\n\nWhen overriding `viewWillLayoutSubviews` it is important to be careful not to create a layout cycle.\n\nThere are two methods to reflect changes in screen/view size:\n- `viewWillTransition(to:with:` called when size of the view will change.\n- `willTransition(to:with:)` called when trait collection will change.\n\nBecause my display mode changes based on size class, I need to update UI in `willTransition(to:with:)`.\n\n```swift\noverride func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {\n    super.willTransition(to: newCollection, with: coordinator)\n    \n    coordinator.animate(alongsideTransition: nil) { _ in\n        switch displayMode {\n            case .sideBySide:\n                navigationItem.rightBarButtonItem = nil\n                break\n            case .one:\n                navigationItem.rightBarButtonItem = UIBarButtonItem(title: \"Flip\", style: .plain,\n                    target: self, action: #selector(flipAction))\n                break\n        }\n    }\n}\n```\n\nAs you can see, creating adaptive view controller is not hard and it doesn't take much code/time.\n\n## Key takeaways\n\n- Reflect adaptivity on layout and presentation;\n- Establish relationship between a container view controller and its child view controllers;\n- Reflect changes in `viewWillLayoutSubviews`;\n- Do not forget about localization and RTL support;\n- **Build adaptable apps!**\n\nThank you for reading. Full code is in the repository. Feel free to play with it or use it in real apps.\n\nMore information:\n- [Making Apps Adaptive, Part 1](https://developer.apple.com/videos/play/wwdc2016/222)\n- [Making Apps Adaptive, Part 2](https://developer.apple.com/videos/play/wwdc2016/233)\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmytro-anokhin%2Fsplit-or-flip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmytro-anokhin%2Fsplit-or-flip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmytro-anokhin%2Fsplit-or-flip/lists"}