Skip to content

Add a SplitViewController #2

@helje5

Description

@helje5

For common master/detail setups. A SwiftUI NavigationView really is a UI/NSSplitView already :-) But the semantics wrt to show and showDetail would be different.

Implementation shouldn't be too hard, depending on how many features are to be replicated.

A first attempt, to be finished:

/**
 * Type erased version of the ``SplitViewController``. Check that for more
 * information.
 */
public protocol _SplitViewController: _ViewController {
  
  typealias Style  = SplitViewControllerStyle
  typealias Column = SplitViewControllerColumn
  
}

public enum SplitViewControllerStyle: Equatable {
  case doubleColumn
  case tripleColumn
}

public enum SplitViewControllerColumn: Equatable {
  case primary
  case supplementary
  case secondary
}


/**
 * A simple wrapper around SwiftUI's `NavigationView`.
 *
 * Should be used as a root only.
 *
 * This adds a few `UISplitViewController` like behaviour, but in the end just
 * hooks into `NavigationView`
 * (which is a SplitViewController in wider layouts).
 *
 * Unlike `UISplitViewController`, this does not wrap the children in
 * `NavigationController`s (this is handled by SwiftUI itself).
 *
 * Example:
 * ```swift
 * struct ContentView: View { // the "scene view"
 *
 *   var body: some View {
 *     MainViewController(SplitViewController(style: .doubleColumn))
 *   }
 * }
 * ```
 *
 * Note that this works quite differently to a `UISplitViewController`.
 *
 * 2022-04-25: Note that programmatic navigation in SwiftUI is still a mess,
 *             i.e. popping in a 3-pane controller may fail.
 */
open class SplitViewController: ViewController, _SplitViewController {
  // TBD: We could probably make this more typesafe if we tie it to three
  //      columns?
  
  @Published public var style           : SplitViewControllerStyle
  @Published public var viewControllers : [ AnyViewController ]

  init(style: SplitViewControllerStyle = .doubleColumn,
       viewControllers: [ AnyViewController ] = [])
  {
    self.style           = style
    self.viewControllers = viewControllers
  }
  
  convenience
  public init<PrimaryVC, SupplementaryVC, SecondaryVC>(
    _ primary       : PrimaryVC,
    _ supplementary : SupplementaryVC,
    _ secondary     : SecondaryVC
  ) where PrimaryVC       : ViewController,
          SupplementaryVC : ViewController,
          SecondaryVC     : ViewController
  {
    self.init(style: .tripleColumn, viewControllers: [
      AnyViewController(primary),
      AnyViewController(supplementary),
      AnyViewController(secondary)
    ])
    addChild(primary)
    addChild(supplementary)
    addChild(secondary)
  }
  convenience
  public init<PrimaryVC, SecondaryVC>(_ primary   : PrimaryVC,
                                      _ secondary : SecondaryVC)
    where PrimaryVC: ViewController, SecondaryVC: ViewController
  {
    self.init(style: .doubleColumn, viewControllers: [
      AnyViewController(primary),
      AnyViewController(secondary)
    ])
    addChild(primary)
    addChild(secondary)
  }
  
  
  // MARK: - View
  
  public struct ContentView: View {
    
    @EnvironmentObject private var viewController : SplitViewController
    
    public init() {}
    
    struct EmbedChild: SwiftUI.View {
      
      let vc : _ViewController?
      
      var body: some View {
        if let vc = vc {
          vc.anyControlledContentView
        }
      }
    }
    
    public var body: some View {
      // SwiftUI switches the mode based on the _static_ style of the View
      switch viewController.style {
        case .doubleColumn:
          NavigationView {
            EmbedChild(vc: viewController.children.first)
            EmbedChild(vc: viewController.children.count > 1
                       ? viewController.children.dropFirst().first
                       : nil)
          }
        case .tripleColumn:
          NavigationView {
            EmbedChild(vc: viewController.children.first)
            EmbedChild(vc: viewController.children.count > 1
                       ? viewController.children.dropFirst().first
                       : nil)
            EmbedChild(vc: viewController.children.count > 2
                       ? viewController.children.dropFirst(2).first
                       : nil)
          }
      }
    }
  }
}

public extension AnyViewController {

  @inlinable // Note: not a protocol requirement, i.e. dynamic!
  var splitViewController : _SplitViewController? {
    viewController.splitViewController
  }
}

public extension _ViewController {
  
  /**
   * Return the ``SplitViewController`` presenting/wrapping this controller.
   */
  var splitViewController : _SplitViewController? {
    /// Is this VC itself being presented?
    if let presentingVC = presentingViewController {
      if let nvc = presentingVC as? _SplitViewController { return nvc }
      return presentingVC.splitViewController
    }
    if let parent = parent as? _SplitViewController {
      return parent
    }
    return parent?.splitViewController
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions