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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions