From 0c0df052b83085e34e984be640b968c72d6c7a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Tue, 26 Apr 2022 18:07:26 +0200 Subject: [PATCH 1/4] Initial drop An initial implementation of the `ViewController` protocol and associated infrastructure. Seems to work OK. --- .github/workflows/swift.yml | 24 + .gitignore | 67 +++ Package.swift | 12 + README.md | 5 + .../ViewController/AnyViewController.swift | 211 +++++++ .../NavigationController.swift | 203 +++++++ .../ViewController/Debugging/DebugMode.swift | 34 ++ .../Debugging/DebugOverlay.swift | 102 ++++ .../Debugging/HierarchyView.swift | 40 ++ .../Debugging/TypeMismatchInfoView.swift | 69 +++ .../Debugging/ViewControllerInfo.swift | 126 ++++ Sources/ViewController/Logger.swift | 49 ++ .../ViewController/MainViewController.swift | 53 ++ .../NavigationLink/PushLink.swift | 321 +++++++++++ .../Presentations/AutoPresentation.swift | 65 +++ .../Presentations/Presentation.swift | 319 +++++++++++ .../Presentations/PresentationMode.swift | 120 ++++ .../Presentations/PushPresentation.swift | 165 ++++++ .../Presentations/SheetPresentation.swift | 168 ++++++ .../ViewControllerPresentation.swift | 53 ++ Sources/ViewController/ReExports.swift | 10 + .../ViewController/RenderContentView.swift | 48 ++ Sources/ViewController/ViewController.swift | 258 +++++++++ .../ViewController/Containment.swift | 59 ++ .../ViewController/DefaultDescription.swift | 38 ++ .../ViewController/RepresentedObject.swift | 56 ++ .../ViewController/Subscriptions.swift | 75 +++ .../ViewController/ViewController/Title.swift | 44 ++ .../ViewController/TypeErasure.swift | 23 + .../ViewControllerStorage.swift | 238 ++++++++ .../ViewController/_ViewController.swift | 196 +++++++ .../ViewControllerEnvironment.swift | 98 ++++ .../ViewController/ViewControllerView.swift | 62 ++ ViewController.xcodeproj/project.pbxproj | 541 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/ViewController-iOS.xcscheme | 67 +++ .../xcschemes/ViewController-macOS.xcscheme | 67 +++ xcconfig/AppKitBase.xcconfig | 7 + xcconfig/Base.xcconfig | 69 +++ xcconfig/Debug.xcconfig | 24 + xcconfig/MacStaticLib.xcconfig | 4 + xcconfig/MobileStaticLib.xcconfig | 5 + xcconfig/Release.xcconfig | 18 + xcconfig/StaticLib.xcconfig | 9 + xcconfig/UIKitBase.xcconfig | 6 + 46 files changed, 4243 insertions(+) create mode 100644 .github/workflows/swift.yml create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/ViewController/AnyViewController.swift create mode 100644 Sources/ViewController/ContainerViewControllers/NavigationController.swift create mode 100644 Sources/ViewController/Debugging/DebugMode.swift create mode 100644 Sources/ViewController/Debugging/DebugOverlay.swift create mode 100644 Sources/ViewController/Debugging/HierarchyView.swift create mode 100644 Sources/ViewController/Debugging/TypeMismatchInfoView.swift create mode 100644 Sources/ViewController/Debugging/ViewControllerInfo.swift create mode 100644 Sources/ViewController/Logger.swift create mode 100644 Sources/ViewController/MainViewController.swift create mode 100644 Sources/ViewController/NavigationLink/PushLink.swift create mode 100644 Sources/ViewController/Presentations/AutoPresentation.swift create mode 100644 Sources/ViewController/Presentations/Presentation.swift create mode 100644 Sources/ViewController/Presentations/PresentationMode.swift create mode 100644 Sources/ViewController/Presentations/PushPresentation.swift create mode 100644 Sources/ViewController/Presentations/SheetPresentation.swift create mode 100644 Sources/ViewController/Presentations/ViewControllerPresentation.swift create mode 100644 Sources/ViewController/ReExports.swift create mode 100644 Sources/ViewController/RenderContentView.swift create mode 100644 Sources/ViewController/ViewController.swift create mode 100644 Sources/ViewController/ViewController/Containment.swift create mode 100644 Sources/ViewController/ViewController/DefaultDescription.swift create mode 100644 Sources/ViewController/ViewController/RepresentedObject.swift create mode 100644 Sources/ViewController/ViewController/Subscriptions.swift create mode 100644 Sources/ViewController/ViewController/Title.swift create mode 100644 Sources/ViewController/ViewController/TypeErasure.swift create mode 100644 Sources/ViewController/ViewController/ViewControllerStorage.swift create mode 100644 Sources/ViewController/ViewController/_ViewController.swift create mode 100644 Sources/ViewController/ViewControllerEnvironment.swift create mode 100644 Sources/ViewController/ViewControllerView.swift create mode 100644 ViewController.xcodeproj/project.pbxproj create mode 100644 ViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-iOS.xcscheme create mode 100644 ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-macOS.xcscheme create mode 100644 xcconfig/AppKitBase.xcconfig create mode 100644 xcconfig/Base.xcconfig create mode 100644 xcconfig/Debug.xcconfig create mode 100644 xcconfig/MacStaticLib.xcconfig create mode 100644 xcconfig/MobileStaticLib.xcconfig create mode 100644 xcconfig/Release.xcconfig create mode 100644 xcconfig/StaticLib.xcconfig create mode 100644 xcconfig/UIKitBase.xcconfig diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..723aa3b --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,24 @@ +name: Build and Test + +on: + push: + pull_request: + schedule: + - cron: "0 9 * * 1" + +jobs: + nextstep: + runs-on: macos-latest + steps: + - name: Select latest available Xcode + uses: maxim-lobanov/setup-xcode@v1.2.1 + with: + xcode-version: 13 + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Build Swift Debug Package + run: swift build -c debug + - name: Build Swift Release Package + run: swift build -c release + - name: Run Tests + run: swift test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d534044 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..59bac8a --- /dev/null +++ b/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.5 + +import PackageDescription + +let package = Package( + name: "ViewController", + platforms: [ .macOS(.v11), .iOS(.v15) ], + products: [ .library(name: "ViewController", targets: [ "ViewController" ]) ], + targets: [ + .target(name: "ViewController") + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..788032d --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ViewController + +ViewController's for SwiftUI. + +WIP. diff --git a/Sources/ViewController/AnyViewController.swift b/Sources/ViewController/AnyViewController.swift new file mode 100644 index 0000000..69bc40e --- /dev/null +++ b/Sources/ViewController/AnyViewController.swift @@ -0,0 +1,211 @@ +// +// AnyViewController.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import Combine +import SwiftUI + +/** + * A type erased version of a (statically typed) ``ViewController``. + * + * If possible type erasure should be avoided. + * + * When a ``ViewController`` is pushed into the environment, it is pushed + * as an `@EnvironmentObject` of its concrete type, but also as an + * ``AnyViewController``. This allows access of all common methods + * (e.g. ``ViewController/dismiss``). + * + * Example access: + * ```swift + * struct TitleLabel: View { + * + * @EnvironmentObject private var viewController : AnyViewController + * + * var body: some View { + * Text(verbatim: viewController.title) + * .font(.title) + * } + * } + * ``` + */ +public final class AnyViewController: ViewController { + + public var id : ObjectIdentifier { ObjectIdentifier(viewController) } + + public let viewController : _ViewController + private var subscription : AnyCancellable? + + @usableFromInline + init(_ viewController: VC) where VC: ViewController { + assert(!(viewController is AnyViewController), + "Attempt to nest an AnyVC into another \(viewController)") + + self.viewController = viewController + + subscription = viewController.objectWillChange.sink { [weak self] _ in + self?.objectWillChange.send() + } + } + + /** + * An initializer that avoids nesting `AnyViewController`s into themselves. + */ + @usableFromInline + init(_ viewController: AnyViewController) { + self.viewController = viewController.viewController + + // TBD: Can't unwrap this? + subscription = viewController.objectWillChange.sink { + [weak self] _ in + self?.objectWillChange.send() + } + } + + + // MARK: - All the any + // Those are typed erased by the base protocol already (_ViewController). + + @inlinable + public var contentView : AnyView { anyContentView } + @inlinable + public var anyContentView : AnyView { viewController.anyContentView } + @inlinable + public var controlledContentView : AnyView { anyControlledContentView } + @inlinable + public var anyControlledContentView : AnyView { + viewController.anyControlledContentView + } + + + // MARK: - Titles + + @inlinable + public var title : String? { + set { viewController.title = newValue } + get { viewController.title } + } + + @inlinable + public var navigationTitle : String { viewController.navigationTitle } + + + // MARK: - Represented Object + + @inlinable + public var representedObject : Any? { + set { anyRepresentedObject = newValue } + get { anyRepresentedObject } + } + @inlinable + public var anyRepresentedObject : Any? { + set { viewController.anyRepresentedObject = newValue } + get { viewController.anyRepresentedObject } + } + + + // MARK: - Presentation + + @inlinable + public var presentedViewController : _ViewController? { + get { viewController.presentedViewController } + } + @inlinable + public var activePresentations : [ ViewControllerPresentation ] { + set { viewController.activePresentations = newValue } + get { viewController.activePresentations } + } + @inlinable + public var presentingViewController : _ViewController? { + set { viewController.presentingViewController = newValue } + get { viewController.presentingViewController } + } + + @inlinable + public func willAppear() { viewController.willAppear() } + @inlinable + public func willDisappear() { viewController.willDisappear() } + + @inlinable + public func present(_ viewController: VC, + mode: ViewControllerPresentationMode) + where VC: ViewController + { + self.viewController.present(viewController, mode: mode) + } + @inlinable + public func present(_ viewController: VC) { + self.viewController.present(viewController) + } + @inlinable + public func dismiss() { viewController.dismiss() } // TBD: really unwrap? + + + @inlinable + public func show(_ viewController: VC) { + self.viewController.show(viewController) + } + + @inlinable + public func showDetail(_ viewController: VC){ + self.viewController.showDetail(viewController) + } + + @inlinable + public func show(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + self.viewController.show(viewController, in: owner) + } + @inlinable + public func showDetail(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + self.viewController.showDetail(viewController, in: owner) + } + + + // MARK: - Hierarchy + + @inlinable + public var children : [ _ViewController ] { + set { viewController.children = newValue } + get { viewController.children } + } + + @inlinable + public var parent : _ViewController? { + set { viewController.parent = newValue } + get { viewController.parent } + } + + @inlinable + public func willMove(toParent parent: _ViewController?) { + viewController.willMove(toParent: parent) + } + + @inlinable + public func didMove(toParent parent: _ViewController?) { + viewController.didMove(toParent: parent) + } + + @inlinable + public func addChild(_ viewController: VC) { + self.viewController.addChild(viewController) + } + @inlinable + public func removeFromParent() { + viewController.removeFromParent() // TBD: really unwrap? + } + + + // MARK: - Better Description + + @inlinable + public var description: String { "" } + @inlinable + public func appendAttributes(to description: inout String) {} +} diff --git a/Sources/ViewController/ContainerViewControllers/NavigationController.swift b/Sources/ViewController/ContainerViewControllers/NavigationController.swift new file mode 100644 index 0000000..bd7117c --- /dev/null +++ b/Sources/ViewController/ContainerViewControllers/NavigationController.swift @@ -0,0 +1,203 @@ +// +// NavigationController.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * Type erased version of the ``NavigationController``. Check that for more + * information. + */ +public protocol _NavigationController: _ViewController { + + var rootViewController : _ViewController { get } + +} + +public extension _NavigationController { + // This is the actual implementation the local VC's `show` hooks into. + + @inlinable + func show(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + owner.present(viewController, mode: .navigation) + } + @inlinable + func showDetail(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + show(viewController, in: owner) + } +} + +internal extension _NavigationController { + + func forEach(yield: ( _ViewController ) -> Bool) { + guard yield(rootViewController) else { return } + + var presentation = rootViewController.activePresentation(for: .navigation) + while let activePresentation = presentation { + // Nested NavigationController. That will own the subsequent + // presentations. + if activePresentation.viewController is _NavigationController { + break + } + guard activePresentation.mode == .navigation || + activePresentation.mode == .pushLink else { break } + + guard yield(activePresentation.viewController) else { break } + presentation = + activePresentation.viewController.activePresentation(for: .navigation) + } + } + func forEach(yield : ( _ViewController ) -> Void) { + forEach { _ in true } + } +} + +public extension _NavigationController { + + // MARK: - Accessors + + /** + * The ``ViewController`` on top of the navigation stack, i.e. the "root". + */ + @inlinable + var topViewController: _ViewController { rootViewController } + + /** + * The ``ViewController``s currently on the navigation stack of this + * particular ``navigationController``. + * + * Careful: The ``NavigationController`` doesn't emit a change notification + * if any child view controllers change. (TBD) + */ + var viewControllers : [ _ViewController ] { + // Note: No setter until programmatic navigation actually works in + // SwiftUI ... + + get { + var children = [ _ViewController ]() + forEach { children.append($0) } + return children + } + } + + /** + * The ``ViewController``s a the bottom of the stack of the particular + * ``navigationController``. + * + * Careful: The ``NavigationController`` doesn't emit a change notification + * if any child view controllers change. (TBD) + */ + var visibleViewController : _ViewController { + var cursor : _ViewController = rootViewController + forEach { cursor = $0 } + return cursor + } +} + + +/** + * A simple wrapper around SwiftUI's `NavigationView`. + * + * The primary purpose of this class is to tell the ``ViewController`` stack, + * that a `NavigationView` is in place. So that `show` methods automatically + * present in the `NavigationView` (instead of showing a sheet, etc). + * + * Example: + * ```swift + * struct ContentView: View { // the "scene view" + * + * var body: some View { + * MainViewController(NavigationController(rootViewController: HomePage())) + * } + * } + * ``` + * + * Note that this works quite differently to a `UINavigationController`. + * I.e. the controller does not really "own" the activation stack. Rather, the + * ``ViewController``'s themselves define the activation trail. + * + * 2022-04-25: Note that programmatic navigation in SwiftUI is still a mess, + * so you can't reliably "deeplink". + * I.e. no `popToRootViewController` + */ +open class NavigationController: ViewController, _NavigationController + where RootVC: ViewController +{ + + // TODO: "show" instead of present + + public let _rootViewController : RootVC + + public var rootViewController : _ViewController { _rootViewController } + + public init(rootViewController: RootVC) { + self._rootViewController = rootViewController + markAsPresentingViewController() + } + + private func markAsPresentingViewController() { + rootViewController.presentingViewController = self + activePresentations.append(TypedViewControllerPresentation( + viewController: _rootViewController, + mode: .custom // not .navigation, that would activate the bg link! + )) + } + + // MARK: - Description + + public func appendAttributes(to description: inout String) { + defaultAppendAttributes(to: &description) + description += " \(rootViewController)" + } + + + // MARK: - View + + public struct ContentView: View { + + @EnvironmentObject private var viewController : NavigationController + + public init() {} + + public var body: some View { + NavigationView { + viewController._rootViewController.contentView + .controlled(by: viewController._rootViewController) + } + } + } +} + +public extension AnyViewController { + + @inlinable // Note: not a protocol requirement, i.e. dynamic! + var navigationController : _NavigationController? { + viewController.navigationController + } +} + +public extension _ViewController { + + /** + * Return the ``NavigationController`` presenting this controller. + * + * Note: If the controller is a ``NavigationController`` itself, this does NOT + * return self. It still looks for the closest presenting controller. + */ + var navigationController : _NavigationController? { + /// Is this VC itself being presented? + if let presentingVC = presentingViewController { // yes + if let nvc = presentingVC as? _NavigationController { return nvc } + return presentingVC.navigationController + } + return parent?.navigationController + } +} diff --git a/Sources/ViewController/Debugging/DebugMode.swift b/Sources/ViewController/Debugging/DebugMode.swift new file mode 100644 index 0000000..927e034 --- /dev/null +++ b/Sources/ViewController/Debugging/DebugMode.swift @@ -0,0 +1,34 @@ +// +// DebugMode.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public enum ViewControllerDebugMode: Equatable { + case none + case overlay +} + +public extension EnvironmentValues { + + @usableFromInline + internal struct DebugModeKey: EnvironmentKey { + #if DEBUG + @usableFromInline + static let defaultValue = ViewControllerDebugMode.overlay + #else + @usableFromInline + static let defaultValue = ViewControllerDebugMode.none + #endif + } + + @inlinable + var viewControllerDebugMode : ViewControllerDebugMode { + set { self[DebugModeKey.self] = newValue } + get { self[DebugModeKey.self] } + } +} diff --git a/Sources/ViewController/Debugging/DebugOverlay.swift b/Sources/ViewController/Debugging/DebugOverlay.swift new file mode 100644 index 0000000..da40b1c --- /dev/null +++ b/Sources/ViewController/Debugging/DebugOverlay.swift @@ -0,0 +1,102 @@ +// +// DebugOverlay.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +#if DEBUG + +@usableFromInline +struct DebugOverlayModifier: ViewModifier { + + @Environment(\.viewControllerDebugMode) private var mode + @State private var showingOverlay = false + + + /// Show information about the VC active in the environment + struct InfoPanel: View { // TBD: Make it a VC? :-) + + @Binding var isShowing : Bool + @EnvironmentObject private var viewController : AnyViewController + + private func dismiss() { isShowing = false } + + #if os(macOS) + var body: some View { + NavigationView { + ViewControllerInfo( + watchChanges: viewController, + viewController: viewController + ) + .navigationTitle(viewController.navigationTitle) + .toolbar { + Button(action: dismiss) { + Label("Close", systemImage: "xmark.circle") + } + } + } + } + #else + var body: some View { + NavigationView { + ViewControllerInfo( + watchChanges: viewController, + viewController: viewController + ) + .navigationTitle(viewController.navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(action: dismiss) { + Label("Close", systemImage: "xmark.circle") + } + } + } + } + #endif + } + + private func showOverlay() { + showingOverlay = true + } + + @usableFromInline + func body(content: Content) -> some View { + content + .overlay( + Button(action: showOverlay) { + Image(systemName: "ladybug") + .accessibilityLabel("Show Debug Overlay") + .foregroundColor(.red) + } + .labelsHidden() + .sheet(isPresented: $showingOverlay) { + InfoPanel(isShowing: $showingOverlay) + } + .opacity(mode == .overlay ? 1.0 : 0.0) + .padding(), alignment: .bottomTrailing + ) + } +} + +public extension View { + + func viewControllerDebugOverlay() -> some View { + self.modifier(DebugOverlayModifier()) + } +} + +#else // non-DEBUG fallback + +public extension View { + + @inlinable + func viewControllerDebugOverlay() -> some View { + self + } +} + +#endif diff --git a/Sources/ViewController/Debugging/HierarchyView.swift b/Sources/ViewController/Debugging/HierarchyView.swift new file mode 100644 index 0000000..fa942f5 --- /dev/null +++ b/Sources/ViewController/Debugging/HierarchyView.swift @@ -0,0 +1,40 @@ +// +// HierarchyView.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +#if DEBUG +struct HierarchyView: View { + + let title : String + let controllers : [ _ViewController ] + + var body: some View { + if controllers.count > 1 { + Divider() + VStack(alignment: .leading, spacing: 16) { + Text(title) + .font(.title2) + + VStack(alignment: .leading, spacing: 12) { + // Don't do this at home + ForEach(Array(zip(controllers.indices, controllers)), id: \.0) { + ( idx, parent ) in + HStack(alignment: .firstTextBaseline) { + Text("\(idx)") + Text(verbatim: "\(parent)") + } + } + } + .padding(.horizontal) + } + .padding() + } + } +} +#endif // DEBUG diff --git a/Sources/ViewController/Debugging/TypeMismatchInfoView.swift b/Sources/ViewController/Debugging/TypeMismatchInfoView.swift new file mode 100644 index 0000000..8dc9b04 --- /dev/null +++ b/Sources/ViewController/Debugging/TypeMismatchInfoView.swift @@ -0,0 +1,69 @@ +// +// TypeMismatchInfoView.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +@usableFromInline +struct TypeMismatchInfoView: View + where DestinationVC: ViewController, ParentVC: ViewController +{ + + @ObservedObject private var parent : ParentVC + private let expectedMode : ViewControllerPresentationMode + + @usableFromInline + init(parent: ParentVC, expectedMode: ViewControllerPresentationMode) { + self.parent = parent + self.expectedMode = expectedMode + } + + @usableFromInline + var body: some View { + VStack { + Label("Type Mismatch", + systemImage: "exclamationmark.triangle") + .font(.title) + .padding() + .foregroundColor(.red) + + HStack(alignment: .firstTextBaseline) { + Text("Parent:") + Spacer() + Text(verbatim: parent.description) + } + .padding() + + if parent.activePresentations.isEmpty { + Text("No presentation active?") + .padding() + .foregroundColor(.red) + .font(.body.bold()) + } + else { + ForEach(Array(zip(parent.activePresentations.indices, + parent.activePresentations)), id: \.0) + { _, presentation in + HStack(alignment: .firstTextBaseline) { + Text("Presented:") + Spacer() + Text(verbatim: presentation.viewController.description) + } + .padding() + HStack(alignment: .firstTextBaseline) { + Text("Mode:") + Spacer() + Text(verbatim: "\(presentation.mode)") + } + } + .padding() + } + + Spacer() + } + } +} diff --git a/Sources/ViewController/Debugging/ViewControllerInfo.swift b/Sources/ViewController/Debugging/ViewControllerInfo.swift new file mode 100644 index 0000000..5b04d81 --- /dev/null +++ b/Sources/ViewController/Debugging/ViewControllerInfo.swift @@ -0,0 +1,126 @@ +// +// ViewControllerInfo.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +#if DEBUG +import SwiftUI + +struct ViewControllerInfo: View { + + @ObservedObject var watchChanges : AnyViewController + let viewController : _ViewController + + struct TitledField: View { + let label : String + let value : V + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(verbatim: "\(value)") + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder() + .foregroundColor(.secondary) + .padding(.vertical, -4) + .padding(.horizontal, -8) + ) + Text(label) + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + + private var parentHierarchy : [ _ViewController ] { + guard let parent = viewController.parent else { return [] } + return Array(sequence(first: parent) { $0.parent }) + } + + private var presentationHierarchy : [ _ViewController ] { + let toRoot = sequence(first: viewController) { + $0.presentingViewController + } + if let presented = viewController.presentedViewController { + let downwards = sequence(first: presented) { + $0.presentedViewController + } + return Array(toRoot) + Array(downwards) + } + else { + return Array(toRoot) + } + } + + + private var addressString : String { + let oid = UInt(bitPattern: ObjectIdentifier(viewController)) + return "0x" + String(oid, radix: 16) + } + + private var title : String { + if viewController is AnyViewController { + return "AnyVC: " + addressString + } + else { + return String(describing: type(of: viewController)) + } + } + + var body: some View { + if let avc = viewController as? AnyViewController { + VStack(alignment: .leading) { + Label(title, systemImage: "envelope") + .padding() + + Divider() + + ViewControllerInfo(watchChanges: avc, + viewController: avc.viewController) + } + } + else { + VStack(alignment: .leading) { + Label(title, systemImage: "ladybug") + .padding() + + Divider() + + ScrollView { + VStack(alignment: .leading, spacing: 20) { + TitledField(label: "Description", value: viewController) + + if let vc = viewController.parent { + TitledField(label: "Parent", value: vc) // TBD: recurse? + } + + if let vc = viewController.presentedViewController { + TitledField(label: "Presenting other", value: vc) + } + + if let vc = viewController.presentingViewController { + TitledField(label: "Presented by", value: vc) + } + else { + if viewController.parent == nil { Text("Root") } + else { Text("Contained") } + } + } + .padding() + + HierarchyView(title: "Parent Hierarchy", + controllers: parentHierarchy) + HierarchyView(title: "Presentation Hierarchy", + controllers: presentationHierarchy) + + Spacer() + } + } + } + } +} + +#endif // DEBUG diff --git a/Sources/ViewController/Logger.swift b/Sources/ViewController/Logger.swift new file mode 100644 index 0000000..4608420 --- /dev/null +++ b/Sources/ViewController/Logger.swift @@ -0,0 +1,49 @@ +// +// Logger.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import os + +// Print Log Helper, since we apparently can't set the log level of os_log 🤦‍♀️ + +#if DEBUG && true + +@usableFromInline +struct PrintLogger { + + let level : OSLogType = .error // TODO: derive from LOGLEVEL env variable + + func log(_ level: OSLogType, _ prefix: String, _ message: () -> String) { + guard level.rawValue >= self.level.rawValue else { return } + print(prefix + message()) + } + + @usableFromInline + func debug(_ message: @autoclosure () -> String) { + log(.debug, "", message) + } + @usableFromInline + func warning(_ message: @autoclosure () -> String) { + log(.error, "WARN: ", message) + } + @usableFromInline + func error(_ message: @autoclosure () -> String) { + log(.error, "ERROR: ", message) + } +} + +@usableFromInline +let logger = PrintLogger() + +#else + +@usableFromInline +let logger = Logger( + subsystem : Bundle.main.bundleIdentifier ?? "Main", + category : "VC" +) +#endif diff --git a/Sources/ViewController/MainViewController.swift b/Sources/ViewController/MainViewController.swift new file mode 100644 index 0000000..1286bad --- /dev/null +++ b/Sources/ViewController/MainViewController.swift @@ -0,0 +1,53 @@ +// +// MainViewController.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * This allocates the state for, and assigns, a scene view controller, + * i.e. one which starts a new VC hierarchy. + * Usually only one root VC is used per scene. + * + * Checkout the ``View/main`` modifier for the more general solution. + * + * E.g. this could be used in the `ContentView` of an app like this: + * ```swift + * struct ContentView: View { + * + * var body: some View { + * MainViewController(HomePage()) + * } + * } + * ``` + */ +public struct MainViewController: View where VC: ViewController { + + @StateObject private var viewController : VC + + public init(_ viewController: @escaping @autoclosure () -> VC) { + self._viewController = StateObject(wrappedValue: viewController()) + } + + /** + * Helper to avoid using ``presentInNavigation`` with a ``ViewController`` + * that doesn't have a proper ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + public init(_ viewController: @escaping @autoclosure () -> VC) + where VC.ContentView == DefaultViewControllerView + { + assertionFailure("Incorrect use of ViewController") + self._viewController = StateObject(wrappedValue: viewController()) + } + + public var body: some View { + VC.ContentView() + .controlled(by: viewController) + } +} diff --git a/Sources/ViewController/NavigationLink/PushLink.swift b/Sources/ViewController/NavigationLink/PushLink.swift new file mode 100644 index 0000000..3246a6b --- /dev/null +++ b/Sources/ViewController/NavigationLink/PushLink.swift @@ -0,0 +1,321 @@ +// +// PushLink.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * Push a new view controller and navigate to it within a `NavigationView` + * (using a `NavigationLink`). + * + * Note: Unlike an explicit `Button` calling `present` or `show`, + * a `PushLink` caches the target controller while active. + * I.e. pressing the same `PushLink` twice, will not result in a new + * instantiation. + * + * Content View Example: + * ```swift + * struct ContentView: View { + * var body: some View { + * PushLink("Preferences…", to: PreferencesPage()) + * } + * } + * ``` + * + * Explicit View Example: + * ```swift + * struct ContentView: View { + * var body: some View { + * PushLink(to: PreferencesPage(), using: Text("Prefs!") { + * Text("Preferences…") + * } + * } + * } + * ``` + * + * A workaround until I found a way to make the `NavigationLink` init extension + * working. + */ +public struct PushLink: View + where VC: ViewController, CV: View, Label: View +{ + // TBD: Call this `PushSegue`? + // This works, but not as nice as being able to use NavLink directly. + + @EnvironmentObject private var parentViewController : AnyViewController + @State private var childViewController : _ViewController? + private let childViewControllerFactory : () -> VC + private let contentView : CV + private let label : Label + + private let mode = ViewControllerPresentationMode.pushLink + + + // MARK: - Public Initializers + + /** + * Create a ``PushLink`` that is using an explicit `View` as the destination. + * + * Example: + * ``` + * PushLink(to: SettingsPage(), using: MySettingsView()) { + * Text("Go to settings…") + * } + * ``` + */ + public init(to viewController : @autoclosure @escaping () -> VC, + using contentView : CV, + @ViewBuilder label : () -> Label) + { + self.childViewControllerFactory = viewController + self.contentView = contentView + self.label = label() + } + + + // MARK: - Implementation + + private var isActive : Bool { + // The thing to keep in mind here is that the `childViewController` is NOT + // the truth. The truth is the presentation state in the + // `parentViewController`. + guard let activeVC = childViewController else { + logger.debug("PushLink[isActive]: no CVC is set…") + return false + } + guard let presentation = + parentViewController.activePresentation(for: activeVC) else + { + // OK, this is FINE. It happens because when clicking a different NavLink + // the _new_ position is set to "showing" before the old one is being + // dismissed. + // Note: Do not update state in an accessor! + logger.debug( + "PushLink[isActive]: No presentation: \(activeVC.description), off.") + return false + } + + assert(presentation.viewController === activeVC) + logger.debug("PushLink[isActive]: active: \(activeVC.description)") + return presentation.viewController === activeVC + } + + private func presentIfNecessary() { + if let activeVC = childViewController { // we are already active + logger.debug( + "PushLink[present]: VC is already active: \(activeVC.description)") + assert(isActive) // checks the parent as well + return + } + + // Not yet active + let activeVC = childViewControllerFactory() + logger.debug("PushLink[present]: VC \(activeVC) \(mode)") + childViewController = activeVC + parentViewController.present(activeVC, mode: mode) + } + + private func dismissIfNecessary() { + // The thing to keep in mind here is that the `childViewController` is NOT + // the truth. The truth is the presentation state in the + // `parentViewController`. + guard let activeVC = childViewController else { + logger.debug("PushLink[dismiss]: no VC is active: \(parentViewController)") + return + } + + defer { childViewController = nil } + + // OK, we have the local VC state, but is the VC still active in the parent + // ViewController? + guard let _ = parentViewController.activePresentation(for: activeVC) else { + // OK, this is FINE. It happens because when clicking a different NavLink + // the _new_ position is set to "showing" before the old one is being + // dismissed. + // This is called during a View update. We should not update state in + // here, but only in the follow up, which will call our Binding w/ a + // isActive=false (but only _after_ setting the new item to true) + logger.debug( + "PushLink[dismiss]: No presentation: \(activeVC.description), off.") + + if activeVC.presentingViewController != nil { + logger.error( + "PushLink[dismiss]: \(activeVC.description) wasn't dismissed?") + assert(activeVC.presentingViewController == nil, + "The cache VC should have been dismissed!") + activeVC.dismiss() + } + + return + } + + logger.debug("PushLink[dismiss]: \(activeVC.description)") + activeVC.dismiss() + logger.debug("PushLink[dismiss]: done: \(activeVC.description)") + } + + private var isActiveBinding: Binding { + Binding( + get: { + isActive + }, + set: { isActive in + if isActive { presentIfNecessary() } + else { dismissIfNecessary() } + } + ) + } + + @ViewBuilder private var destination: some View { + if let presentedVC = parentViewController + .presentedViewController(of: VC.self, mode: mode) + { + contentView + .controlled(by: presentedVC) + .environment(\.viewControllerPresentationMode, .navigation) + .navigationTitle(presentedVC.navigationTitle) + } + else { + SwiftUI.Label("Error: Missing/wrong presented VC", + systemImage: "exclamationmark.triangle") + } + } + + public var body: some View { + NavigationLink( + isActive : isActiveBinding, + destination : { destination }, + label : { label } + ) + } +} + + +// MARK: - Convenience Initializers + +extension PushLink { + + /** + * Create a ``PushLink`` that is using the ``ViewController/ContentView`` + * as the destination. + * + * Example: + * ``` + * PushLink(to: SettingsPage()) { + * Text("Go to settings…") + * } + * ``` + */ + @inlinable + public init(to viewController : @autoclosure @escaping () -> VC, + @ViewBuilder label : () -> Label) + where VC: ViewController, CV == RenderContentView + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(to: viewController(), using: RenderContentView()) { label() } + } + + /** + * Create a ``PushLink`` that is using the ``ViewController/ContentView`` + * as the destination. + * + * Example: + * ``` + * PushLink("Go to settings…", to: SettingsPage()) + * ``` + */ + @inlinable + public init(_ title: S, to viewController: @autoclosure @escaping () -> VC) + where VC: ViewController, CV == RenderContentView, + Label == Text, S: StringProtocol + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(to: viewController(), using: RenderContentView()) { + Text(title) + } + } + + /** + * Create a ``PushLink`` that is using the ``ViewController/ContentView`` + * as the destination. + * + * Example: + * ``` + * PushLink("Go to settings…", to: SettingsPage()) + * ``` + */ + @inlinable + public init(_ titleKey: LocalizedStringKey, + to viewController: @autoclosure @escaping () -> VC) + where VC: ViewController, CV == RenderContentView, Label == Text + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(to: viewController(), using: RenderContentView()) { + Text(titleKey) + } + } + +} + + +// MARK: - Unavailable Initializers + +extension PushLink { + + /** + * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper + * ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + public init(to viewController : @autoclosure @escaping () -> VC, + @ViewBuilder label : () -> Label) + where VC: ViewController, CV == RenderContentView, + VC.ContentView == DefaultViewControllerView + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(to: viewController(), using: RenderContentView()) { label() } + } + + /** + * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper + * ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + public init(_ title: S, to viewController: @autoclosure @escaping () -> VC) + where VC: ViewController, CV == RenderContentView, + Label == Text, S: StringProtocol, + VC.ContentView == DefaultViewControllerView + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(title, to: viewController()) + } + + /** + * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper + * ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + public init(_ titleKey: LocalizedStringKey, + to viewController: @autoclosure @escaping () -> VC) + where VC: ViewController, CV == RenderContentView, Label == Text, + VC.ContentView == DefaultViewControllerView + { + assert(VC.ContentView.self != DefaultViewControllerView.self, + "Attempt to use ContentView based Push w/ VC w/o ContentView") + self.init(to: viewController(), using: RenderContentView()) { + Text(titleKey) + } + } +} diff --git a/Sources/ViewController/Presentations/AutoPresentation.swift b/Sources/ViewController/Presentations/AutoPresentation.swift new file mode 100644 index 0000000..83eacb6 --- /dev/null +++ b/Sources/ViewController/Presentations/AutoPresentation.swift @@ -0,0 +1,65 @@ +// +// AutoPresentation.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * This is used by `controlled(by:)` to attach the logic to present the + * "automatic" presentations in sheets or `NavigationView`s. + * + * It watches the current VC to detect presentation changes, + * and binds the sheet/navlink to the respective mode. + */ +struct AutoPresentationViewModifier: ViewModifier where VC: ViewController { + + @ObservedObject var viewController : VC + + func body(content: Content) -> some View { + // Note: Also used internally during presentation. + content + // Note: The `VC` class is that of the "parent" ViewController, not of the + // ViewController being presented! + .sheet( + isPresented: viewController.isPresentingMode(.sheet), + content: { + if let presentation = viewController.activePresentation(for: .sheet) { + presentation.contentView() + .environment(\.viewControllerPresentationMode, .sheet) + .navigationTitle(presentation.viewController.navigationTitle) + } + else { + TypeMismatchInfoView( + parent: viewController, expectedMode: .sheet + ) + } + } + ) + .background( + NavigationLink( + isActive: viewController.isPresentingMode(.navigation), + destination: { + if let presentation = viewController + .activePresentation(for: .navigation) + { + presentation.contentView() + .environment(\.viewControllerPresentationMode, .navigation) + .navigationTitle(presentation.viewController.navigationTitle) + } + else { + TypeMismatchInfoView( + parent: viewController, expectedMode: .navigation + ) + } + }, + label: { Color.clear } // TBD: EmptyView? + ) + ) + + .viewControllerDebugOverlay() + } +} diff --git a/Sources/ViewController/Presentations/Presentation.swift b/Sources/ViewController/Presentations/Presentation.swift new file mode 100644 index 0000000..1106b7e --- /dev/null +++ b/Sources/ViewController/Presentations/Presentation.swift @@ -0,0 +1,319 @@ +// +// Presentation.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public extension ViewController { + + @inlinable + func willAppear() {} + + @inlinable + func willDisappear() {} + + @inlinable + var presentedViewController : _ViewController? { + // Really a set in our case ... + activePresentations.first?.viewController + } +} + +public extension _ViewController { + + func activePresentation(for mode: PresentationMode?) + -> ViewControllerPresentation? + { + guard let mode = mode else { return activePresentations.first } + return activePresentations.first(where: { $0.mode == mode }) + } + func activePresentation(for presentedViewController: _ViewController) + -> ViewControllerPresentation? + { + activePresentations.first { $0.viewController === presentedViewController } + } + + // MARK: - Bindings + + /** + * This allows us to check whether a particular VC is being presented, + * e.g. in case the presentation should be done differently (e.g. sheet vs + * navigation). + */ + @inlinable + func isPresenting(mode: ViewControllerPresentationMode?, + _ condition: @escaping ( _ViewController ) -> Bool) + -> Binding + { + Binding( + get: { + guard let presentation = self.activePresentation(for: mode) else { + return false + } + if let mode = mode, mode != presentation.mode { return false } + return condition(presentation.viewController) + }, + set: { isShowing in + // We cannot make VCs "appear", that would require a factory. + // Instead, the factory part is provided using the presentation mode + // helpers. + guard !isShowing else { return } // isShowing=true would be activation + + guard let presentation = self.activePresentation(for: mode) else { + // This is fine, could have happened by other means. + return logger.debug("Did not find VC to deactivate: \(self)") + } + + assert(mode != .automatic) + assert(presentation.mode != .automatic) + if let mode = mode, presentation.mode != mode { return } + + /// Does our condition match? + guard condition(presentation.viewController) else { return } + + logger.debug( + "Binding dismiss: \(presentation.viewController.description)") + presentation.viewController.dismiss() + } + ) + } + + /** + * Only checks whether a specific mode is active. This is used for the + * internally supported "auto" modes (`.sheet` and `.navigation`). + */ + @inlinable + func isPresentingMode(_ mode: ViewControllerPresentationMode) + -> Binding + { + // In here, `.pushLink` and `.navigation` must be treated differently! + // Only during presentation, we may need to dismiss the other type! + Binding( + get: { + self.activePresentations.contains(where: { $0.mode == mode }) + }, + set: { isShowing in + // We cannot make VCs "appear", that would require a factory. + // Instead, the factory part is provided using the presentation mode + // helpers. + guard !isShowing else { + // isShowing=true would be activation + logger.warning("Attempt to activate VC via Binding, won't work!") + return + } + + // Dismiss if the presentation mode matches + + guard let presentation = self.activePresentation(for: mode) else { + // This is fine, could have happened by other means. + return logger.debug("did not find VC to deactivate \(self)?") + } + + /// If a mode was requested, make sure it is the right one. + /// TBD: what about "automatic" and such? Never active in an actual + /// presentation! + assert(presentation.mode == mode, "internal inconsistency!") + guard presentation.mode == mode else { return } + + logger.debug( + "Dismiss by mode binding: \(presentation.viewController.description)") + presentation.viewController.dismiss() + } + ) + } + + /** + * Lookup a presented ``ViewController`` of a particular type. Returns nil + * if there is none such (or the mode doesn't match) + * + * E.g. this is used by the SheetPresentation. + */ + @inlinable + func presentedViewController(of type: VC.Type, + mode: ViewControllerPresentationMode?) + -> VC? + where VC: ViewController + { + guard let presentation = activePresentation(for: mode) else { return nil } + if let mode = mode, mode != presentation.mode { return nil } + return presentation.viewController as? VC + } + + + /** + * This allows us to check whether a particular type of VC is being presented, + * e.g. in case the presentation should be done differently (e.g. sheet vs + * navigation). + * + * CAREFUL: This only checks the type, there could be multiple presentations + * with the same type! (leading to multiple Bindings being true, + * and different ContentViews being active, potentially capturing the + * wrong environment). + */ + func isPresenting(_ controllerType: VC.Type, + mode: ViewControllerPresentationMode?) + -> Binding + where VC: ViewController + { + isPresenting(mode: mode) { $0 is VC } + } + + + // MARK: - API Methods + + @inlinable + func show(_ viewController: VC) { + show(viewController, in: self) + } + @inlinable + func showDetail(_ viewController: VC) { + showDetail(viewController, in: self) + } + + @inlinable + func show(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + { + present(viewController) + } + + @inlinable + func showDetail(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + { + show(viewController) + } + + @inlinable + func present(_ viewController: VC) { + defaultPresent(viewController, mode: .automatic) + } + + @inlinable + func present(_ viewController: VC, mode: PresentationMode) + where VC: ViewController + { + defaultPresent(viewController, mode: mode) + } + + /** + * Present a ``ViewController`` that doesn't have a + * ``ViewController/ContentView`` assigned. + */ + @inlinable + func present(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + { + // Requires a custom `PushPresentation` or `SheetPresentation` + defaultPresent(viewController, mode: .custom) + } + + + // MARK: - Present Implementation + + @inlinable + func show(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + if let handler = presentingViewController ?? parent { + return handler.show(viewController, in: owner) + } + owner.present(viewController) // fallback to `present` + } + @inlinable + func showDetail(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + { + if let handler = presentingViewController ?? parent { + return handler.showDetail(viewController, in: owner) + } + owner.show(viewController) // fallback to `show` + } + + func modalPresentationMode(for viewController: VC) -> PresentationMode + where VC: ViewController + { + // Check if the ViewController being presented explicitly requested a + // certain style + let desiredStyle = viewController.modalPresentationStyle + if desiredStyle != .automatic { return desiredStyle } + + if VC.ContentView.self == DefaultViewControllerView.self { + // Requires an explicit ``PushPresentation`` or ``SheetPresentation`` in + // the associated `View`. + return .custom + } + + return .sheet + } + + func defaultPresent(_ viewController: VC, mode: PresentationMode) + where VC: ViewController + { + guard viewController !== self else { + logger.error("Attempt to present a VC in itself: \(self), \(mode)") + assert(viewController !== self, "attempt to present a VC in itself") + return + } + + let mode = mode != .automatic + ? mode // an explicit mode was requested + : modalPresentationMode(for: viewController) + + // the very same VC is already being presented + guard presentedViewController !== viewController else { + logger.warning("Already presenting VC: \(viewController) in: \(self)") + assertionFailure("already presenting VC") + return + } + + if let activePresentation = self.activePresentation(for: mode) { + // This is OK. On iPad landscape activation is out of order. E.g. when + // switching from link A to B, the binding first sets B to true! before + // setting A to false. + logger.debug("Already presenting VC in: \(self) new: \(viewController)") + activePresentation.viewController.dismiss() + } + + activePresentations.append(TypedViewControllerPresentation( + viewController: viewController, + mode: mode + )) + viewController.presentingViewController = self + viewController.willAppear() // it is still not on screen + + logger.debug("Presented: \(viewController) in: \(self)") + } + + func dismiss() { + guard let parentVC = presentingViewController else { + logger.warning("Dismiss of: \(self) but no `presentingViewController`?") + assertionFailure("No parent VC?") + return + } + + defer { presentingViewController = nil } + + guard let presentation = parentVC.activePresentation(for: self) else { + logger.warning( + "Dismiss of: \(self), but not being presented in parent") + assertionFailure("VC dismiss not being presented") + return + } + + assert(presentation.viewController === self, + "VC dismiss other being presented") + + self.willDisappear() + parentVC.activePresentations.removeAll { + assert($0.viewController !== self || $0.mode == presentation.mode, + "Same VC active in different modes!") + return $0.viewController === self + } + logger.debug("Dismissed: \(self) from: \(parentVC.description)") + } +} diff --git a/Sources/ViewController/Presentations/PresentationMode.swift b/Sources/ViewController/Presentations/PresentationMode.swift new file mode 100644 index 0000000..46e5b38 --- /dev/null +++ b/Sources/ViewController/Presentations/PresentationMode.swift @@ -0,0 +1,120 @@ +// +// PresentationMode.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * The active presentation mode for the ViewController. Can be accessed in the + * environment using: + * ```swift + * public struct ContentView: View { + * @Environment(\.viewControllerPresentationMode) private var mode + * } + * ``` + */ +public enum ViewControllerPresentationMode: Hashable { + // FIXME: Used in two different ways, for accessing the actual presentation, + // and for deciding what presentation to use. + + /// The ``ViewController`` will decide on an appropriate presentation mode. + case automatic + + /** + * The ``ViewController`` won't do the presentation automagically, + * the user needs to handle the presentation explicitly. + * E.g. using `presentAsSheet()` or `presentInNavigation()`, or in a + * completely manual way. + */ + case custom + + /** + * The presentation is done in a sheet that is handled automatically by the + * framework. + */ + case sheet + + /** + * The presentation is done using a programmatic `NavigationLink` that is + * handled automatically by the framework. + */ + case navigation + + /** + * The presentation is done using the ``PushLink``, an wrapped, + * in-View `NavigationLink`. + */ + case pushLink + + // TODO: popover +} + +extension ViewControllerPresentationMode: CustomStringConvertible { + + public var description: String { + switch self { + case .automatic : return "PM:auto" + case .custom : return "PM:custom" + case .sheet : return "PM:sheet" + case .navigation : return "PM:nav" + case .pushLink : return "PM:push" + } + } +} + +public extension ViewController { + + /** + * The active presentation mode for the ViewController. Can be accessed in the + * environment using: + * ```swift + * public struct ContentView: View { + * @Environment(\.viewControllerPresentationMode) private var mode + * } + * ``` + */ + typealias PresentationMode = ViewControllerPresentationMode +} + +public extension EnvironmentValues { + + @usableFromInline + internal struct ViewControllerPresentationModeKey: EnvironmentKey { + public static let defaultValue = ViewController.PresentationMode.custom + } + + /** + * Access the means by which the current ``ViewController`` got presented, + * i.e. `sheet` or `navigation`. + * + * This is set properly by either the `presentInSheet`, `presentInNavigation` + * and the likes. + * + * This is sometimes useful to define how "inner" UI should look like. + * Example: + * ```swift + * public struct ContentView: View { + * @EnvironmentObject private var viewController : ViewController + * @Environment(\.viewControllerPresentationMode) private var mode + * + * var body: some View { + * Text("Hi!") + * + * // show explicit dismiss button for ``NavigationView`` pushes only. + * if mode == .navigation { + * Button("Dismiss", action: viewController.dismiss) + * } + * } + * } + * ``` + */ + @inlinable + var viewControllerPresentationMode : ViewController.PresentationMode { + set { self[ViewControllerPresentationModeKey.self] = newValue } + get { self[ViewControllerPresentationModeKey.self] } + } +} diff --git a/Sources/ViewController/Presentations/PushPresentation.swift b/Sources/ViewController/Presentations/PushPresentation.swift new file mode 100644 index 0000000..a24fc36 --- /dev/null +++ b/Sources/ViewController/Presentations/PushPresentation.swift @@ -0,0 +1,165 @@ +// +// PushPresentation.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public extension View { + + /** + * Controls how a specific ViewController is being presented in `.custom` + * mode. + * + * It does NOT do the actual presentation! I.e. `.present(MyViewController)` + * still has to be called. + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) { + * NavigationView { + * ChildView() + * } + * .navigationViewStyle(.stack) + * } + * .presentInNavigation(AddWidgetVC.self) { + * AddWidgetVC.ContentView() + * } + * .presentInNavigation(ViewConfigVC.self) { + * AddWidgetVC.ContentView() + * } + * ``` + * + * Note: This is using a `background` for the `NavigationLink`. + * Use a ``PushLink`` for a real ``NavigationLink``. + */ + func presentInNavigation(_ vc: VC.Type, + @ViewBuilder content: @escaping () -> V) + -> some View + where VC: ViewController, V: View + { + // Note: The explicit specialization is NECESSARY, otherwise the wrong ones + // are picked up! + self.modifier(PushPresentation(destination: content)) + } + + /** + * Controls how a specific ``ViewController`` is being presented in `.custom` + * mode. + * + * It does NOT do the actual presentation! I.e. `.present(MyViewController)` + * still has to be called. + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) + * .presentInNavigation(AddWidgetVC.self) + * .presentInNavigation(ViewConfigVC.self) + * ``` + */ + @inlinable + func presentInNavigation(_ vc: VC.Type) -> some View { + presentInNavigation(vc, content: { RenderContentView() }) + } + + /** + * Helper to avoid using ``presentInNavigation`` with a ``ViewController`` + * that doesn't have a proper ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + func presentInNavigation(_ vc: VC.Type) -> some View + where VC.ContentView == DefaultViewControllerView + { + presentInNavigation(vc, content: { RenderContentView() }) + } +} + +/** + * Controls how a specific ViewController is being presented in `.custom` mode. + * It does NOT do the actual presentation! + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) { + * NavigationView { + * ChildView() + * } + * .navigationViewStyle(.stack) + * } + * .presentInNavigation(AddWidgetVC.self) { + * AddWidgetVC.ContentView() + * } + * .presentInNavigation(ViewConfigVC.self) { + * AddWidgetVC.ContentView() + * } + * ``` + * + * Note: This is using a `background` for the `NavigationLink`. + * Use a ``PushLink`` for a real ``NavigationLink``. + */ +@usableFromInline +struct PushPresentation: ViewModifier + where DestinationVC: ViewController, V: View +{ + + @EnvironmentObject var parent : AnyViewController + + private let destination : () -> V + private let mode : ViewControllerPresentationMode + + public init(destination: @escaping () -> V) { + self.destination = destination + self.mode = .custom + } + @usableFromInline + internal init(mode: ViewControllerPresentationMode, + destination: @escaping () -> V) + { + self.destination = destination + self.mode = mode + } + + @usableFromInline + func body(content: Content) -> some View { + content + .background( + NavigationLink( + // TODO: This needs an optional extra condition + isActive: parent.isPresenting(DestinationVC.self, mode: mode), + destination: { + if let presentedVC = + parent.presentedViewController(of: DestinationVC.self, + mode: mode) + { + destination() + .environment(\.viewControllerPresentationMode, .navigation) + .controlled(by: presentedVC) + .navigationTitle(presentedVC.navigationTitle) + } + else { + TypeMismatchInfoView( + parent: parent, expectedMode: mode + ) + } + }, + label: { Color.clear } // TBD: EmptyView? + ) + ) + } +} diff --git a/Sources/ViewController/Presentations/SheetPresentation.swift b/Sources/ViewController/Presentations/SheetPresentation.swift new file mode 100644 index 0000000..a7a8367 --- /dev/null +++ b/Sources/ViewController/Presentations/SheetPresentation.swift @@ -0,0 +1,168 @@ +// +// SheetPresentation.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public extension View { + + /** + * Controls how a specific ViewController is being presented in `.custom` + * mode. + * + * It does NOT do the actual presentation! I.e. `.present(MyViewController)` + * still has to be called. + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) { + * NavigationView { + * ChildView() + * } + * .navigationViewStyle(.stack) + * } + * .presentInSheet(AddWidgetVC.self) { + * AddWidgetVC.ContentView() + * } + * .presentInSheet(ViewConfigVC.self) { + * AddWidgetVC.ContentView() + * } + * ``` + */ + func presentInSheet(_ vc: VC.Type, + @ViewBuilder content: @escaping () -> V) + -> some View + where VC: ViewController, V: View + { + // Note: The explicit specialization is NECESSARY, otherwise the wrong ones + // are picked up! + /* + * This has issues in the content closure: + * .presentInSheet(ConfigVC.self) { + * // Passing parameters (like `widget`) here, ends up w/ the "first" + * // widget of the type. + * ConfigVC.ContentView() + * } + * Environment objects do seem to be fine though? So something strange + * happening w/ the capture here. + */ + self.modifier(SheetPresentation(destination: content)) + } + + /** + * Controls how a specific ``ViewController`` is being presented in `.custom` + * mode. + * + * It does NOT do the actual presentation! I.e. `.present(MyViewController)` + * still has to be called. + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) + * .presentInSheet(AddWidgetVC.self) + * .presentInSheet(ViewConfigVC.self) + * ``` + */ + @inlinable + func presentInSheet(_ vc: VC.Type) -> some View { + presentInSheet(vc, content: { RenderContentView() }) + } + + /** + * Helper to avoid using ``presentInSheet`` with a ``ViewController`` + * that doesn't have a proper ``ContentView``. + */ + @available(*, unavailable, + message: "The ViewController needs a proper `ContentView`") + func presentInSheet(_ vc: VC.Type) -> some View + where VC.ContentView == DefaultViewControllerView + { + presentInSheet(vc, content: { RenderContentView() }) + } +} + +/** + * Controls how a specific ViewController is being presented in `.custom` mode. + * It does NOT do the actual presentation! + * + * This peeks into the ``ViewController/presentedVC`` of the current + * ``ViewController`` (which is stored in the environment!) + * + * How to use: + * ``` + * ContentView() + * .presentInSheet(WidgetViewVC.self) { + * NavigationView { + * ChildView() + * } + * .navigationViewStyle(.stack) + * } + * .presentInSheet(AddWidgetVC.self) { + * AddWidgetVC.ContentView() + * } + * .presentInSheet(ViewConfigVC.self) { + * AddWidgetVC.ContentView() + * } + * ``` + */ +@usableFromInline +struct SheetPresentation: ViewModifier + where DestinationVC: ViewController, V: View +{ + + @EnvironmentObject private var parent : AnyViewController + + private let destination : () -> V + private let mode : ViewControllerPresentationMode + + public init(destination: @escaping () -> V) { + self.destination = destination + self.mode = .custom + } + @usableFromInline + internal init(mode: ViewControllerPresentationMode, + destination: @escaping () -> V) + { + self.destination = destination + self.mode = mode + } + + private func isActive(_ vc: _ViewController) -> Bool { + // TODO: This needs an optional extra condition + vc is DestinationVC + } + + @usableFromInline + func body(content: Content) -> some View { + content + .sheet(isPresented: parent.isPresenting(mode: mode, isActive)) { + if let presentedVC : DestinationVC = parent + .presentedViewController(of: DestinationVC.self, mode: mode) + { + // TODO: This is tricky. The destination here can capture + // the incorrect VC, because the sheet presentation condition + // only checks the type! + destination() + .environment(\.viewControllerPresentationMode, .sheet) + .controlled(by: presentedVC) + } + else { + TypeMismatchInfoView( + parent: parent, expectedMode: mode + ) + } + } + } +} diff --git a/Sources/ViewController/Presentations/ViewControllerPresentation.swift b/Sources/ViewController/Presentations/ViewControllerPresentation.swift new file mode 100644 index 0000000..93ae806 --- /dev/null +++ b/Sources/ViewController/Presentations/ViewControllerPresentation.swift @@ -0,0 +1,53 @@ +// +// ViewControllerPresentation.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import Foundation + +/** + * A `ViewControllerPresentation` holds the currently active presentation + * within a ``ViewController``. + * + * It gets created when the user calls ``ViewController/present`` and friends. + * + * Note that a controller may hold multiple presentations, e.g. it may present + * a detail in a `NavigationView` while also doing a presentation in a sheet. + * + * This is the type erased version, ``TypedViewControllerPresentation`` is + * created internally. + */ +public protocol ViewControllerPresentation { + + var viewController : _ViewController { get } + var mode : ViewControllerPresentationMode { get } + + var contentView : () -> AnyView { get } +} + +/** + * The concrete ``ViewControllerPresentation`` object used internally. + */ +public struct TypedViewControllerPresentation + : ViewControllerPresentation +{ + + public let viewController : _ViewController + public let mode : ViewControllerPresentationMode + public let contentView : () -> AnyView + + init(viewController: VC, mode: ViewControllerPresentationMode) { + self.viewController = viewController + self.mode = mode + self.contentView = { + AnyView( + viewController.contentView + .controlled(by: viewController) + ) + } + } +} + diff --git a/Sources/ViewController/ReExports.swift b/Sources/ViewController/ReExports.swift new file mode 100644 index 0000000..760c150 --- /dev/null +++ b/Sources/ViewController/ReExports.swift @@ -0,0 +1,10 @@ +// +// ReExports.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +@_exported import Foundation +@_exported import SwiftUI diff --git a/Sources/ViewController/RenderContentView.swift b/Sources/ViewController/RenderContentView.swift new file mode 100644 index 0000000..13ee06e --- /dev/null +++ b/Sources/ViewController/RenderContentView.swift @@ -0,0 +1,48 @@ +// +// RenderContentView.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * Accesses the `contentView` of the current viewController of the specific + * class. + * + * User level code usually doesn't need to work with this. + * + * Example: + * ```swift + * var body: some View { + * // Note: The environment needs to have `SettingsPage`! + * RenderContentView() + * } + * ``` + */ +public struct RenderContentView: View { + + @usableFromInline + @EnvironmentObject var viewController : VC + + @inlinable + public init() {} + +#if DEBUG + @inlinable + public var body: some View { + if viewController.contentView is EmptyView { + Text(verbatim: "Embedding EmptyView?") + .foregroundColor(.red) + } + viewController.contentView + } +#else + @inlinable + public var body: some View { + viewController.contentView + } +#endif +} diff --git a/Sources/ViewController/ViewController.swift b/Sources/ViewController/ViewController.swift new file mode 100644 index 0000000..222660b --- /dev/null +++ b/Sources/ViewController/ViewController.swift @@ -0,0 +1,258 @@ +// +// ViewController.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI +import Combine + +/** + * A ViewController. + * + * In WebObjects those would be called `WOComponent`s and are accessible + * using the Environment (`WOContext` in WebObjects). + * I.e. the SwiftUI environment always tracks the "active" VC. + * + * The lifecycle events also do not reflect whether the VC is "really" on + * screen, just whether it has been presented. + * + * There are two parts to presenting a ViewController: + * - Call `present` on the active viewController with the instance of the new, + * child ViewController. The active VC can be accessed using + * `@EnvironmentObject private var viewController : ViewController` + * (or the specific VC subclass) + * - To choose the presentation style, attach it to the View, for example: + * `.presentInNavigation(ChildVC.self) { ChildVC.ContentView() }` + * + * TODO: lotsa more documentation + */ +public protocol ViewController: _ViewController, ObservableObject, Identifiable +{ + + // Note: We can't use an own View w/o crashing swiftc 5.6? Or is the issue + // limited to a single module? + typealias DefaultViewControllerView = EmptyView + + /** + * The primary View associated with the ViewController. + * + * There doesn't have to be just one View associated with the ViewController, + * the ViewController itself can be decoupled from a specific `ContentView`. + * E.g. there could be a different main View for macOS and for iOS. + * + * But having a single associated `ContentView` allows for more convenient + * API for that common case. + * + * Example: + * ```swift + * class Contacts: ViewController { + * + * struct ContentView: View { + * + * @EnvironmentObject var viewController: Contacts + * + * var body: some View { + * Text("The Contacts!") + * } + * } + * } + * ``` + */ + associatedtype ContentView : ViewControllerView = DefaultViewControllerView + + /** + * Dirty trick to let the user avoid the need to explicitly specify the + * `ViewControllerView` when declaring Views within the scope of a + * ViewController. + * Example: + * ```swift + * class Contacts: ViewController { + * + * struct ContentView: View { // <== this is really a ViewControllerView + * ... + * } + * } + * ``` + */ + typealias View = ViewControllerView + + /** + * Instantiates the ``ContentView`` associated with the ``ViewController``. + */ + var contentView : ContentView { get } + + + // MARK: - Represented Object + + associatedtype RepresentedValue = Any + + /** + * Get or set a value represented by the ViewController. + * + * Quite often VCs are used to deal with one primary model object. + * This can be used to directly associated that w/ the View. + * + * The default implementation is going to subscribe and republish the + * `willChange` notification if the ``RepresentedValue`` is an + * `ObservableObject` itself. + */ + var representedObject : RepresentedValue? { get set } + + + // MARK: - Titles + + /** + * Get or set a title associated with a ViewController. + */ + var title : String? { set get } + + /** + * Returns the ``title`` of the ``ViewController``, + * but falls back to a default title in case that's not available. + * + * Suitable for use in `navigationTitle`, `navigationBarTitle` and similar + * SwiftUI `View` modifiers. + */ + var navigationTitle : String { get } + + + // MARK: - Presentation + + /** + * Defines the default style in which a ``ViewController`` wants to be + * presented in. + */ + var modalPresentationStyle : ViewControllerPresentationMode { set get } + + /// An internal property to track the ViewController presentation. + var activePresentations : [ ViewControllerPresentation ] { set get } + + /** + * If the ``ViewController`` is presenting another, this property returns + * the presented ViewController. + */ + var presentedViewController : _ViewController? { get } + + /** + * If the ``ViewController`` got presented by another, this property returns + * the ViewController doing the presentation. + */ + var presentingViewController : _ViewController? { set get } + + /** + * This is called if the VC was added as the presented VC, but SwiftUI + * still needs a tick to diff and actually display the related view. + */ + func willAppear() + + /** + * This is called if the VC was removed as a presented VC. SwiftUI will still + * need a tick to diff and remove the related view. + */ + func willDisappear() + + /** + * Make the ``ViewController`` the currently presented ``ViewController`` for + * the given mode. + */ + func present(_ viewController: VC, mode: PresentationMode) + where VC: ViewController + /** + * Make the ``ViewController`` the currently presented ``ViewController``, + * in `.automatic` mode. + */ + func present(_ viewController: VC) + /** + * Present a ``ViewController`` that doesn't have a + * ``ViewController/ContentView`` assigned. + */ + func present(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + func show(_ viewController: VC) + func show(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + func showDetail(_ viewController: VC) + func showDetail(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + /// Internal method which allows presenting view controllers the actual + /// presentation further down in the stack. + func show(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + /// Internal method which allows presenting view controllers the actual + /// presentation further down in the stack. + func showDetail(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + + /** + * Remove the ViewController from being presented in its presenting + * ViewController. + * + * Will call `willDisappear` (only) if it actually was presented by the + * parent. + */ + func dismiss() + + + // MARK: - Hierarchy + + /** + * The array of contained view controllers (children). + * + * Use ``addChild`` to add children to this array and use ``removeFromParent`` + * to remove a child from its parent. + * + * Not to be confused w/ ``presentedViewControllers``. + */ + var children : [ _ViewController ] { set get } + /** + * The parent of a contained ``ViewController`` (if it is actually contained). + * + * Use ``addChild`` to add children to a parent and use ``removeFromParent`` + * to remove a child from its parent. + * + * Not to be confused w/ ``presentingViewController``. + */ + var parent : _ViewController? { set get } + + /** + * This is called once the ViewController will be added or removed as a child. + */ + func willMove(toParent parent: _ViewController?) + + /** + * This is called once the ViewController was added or removed as a child. + */ + func didMove(toParent parent: _ViewController?) + + /** + * Add the specified ``ViewController`` as a child ("contained") + * ViewController. + * + * This will add the `viewController` to the ``children`` array and set its + * ``parent`` property. + */ + func addChild(_ viewController: VC) + + /** + * Remove the ``ViewController`` from its parent. + */ + func removeFromParent() +} + +public extension ViewController { + + @inlinable + var contentView : ContentView { ContentView() } + + @inlinable + var controlledContentView : some SwiftUI.View { + contentView + .controlled(by: self) + } +} diff --git a/Sources/ViewController/ViewController/Containment.swift b/Sources/ViewController/ViewController/Containment.swift new file mode 100644 index 0000000..e7baead --- /dev/null +++ b/Sources/ViewController/ViewController/Containment.swift @@ -0,0 +1,59 @@ +// +// Containment.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public extension ViewController { + + @inlinable + func willMove(toParent parent: _ViewController?) {} + @inlinable + func didMove (toParent parent: _ViewController?) {} +} + +public extension ViewController { + + @inlinable + func addChild(_ viewController: VC) { + assert(!children.contains(where: { $0 === viewController }), + "ViewController already contained as a child!") + assert(viewController.parent !== self, + "ViewController is already the parent of the child!") + + // Do nothing if it is the same parent (i.e. do NOT re-add) + guard viewController.parent !== self else { return } + + // Remove from old parent + if viewController.parent != nil { removeFromParent() } + + viewController.willMove(toParent: self) + children.append(viewController) + viewController.parent = self + viewController.didMove(toParent: self) + } + + @inlinable + func removeFromParent() { + guard let parent = parent else { + return logger.warning( + "removeFromParent() called w/o a parent being active: \(self)") + } + + guard let idx = parent.children.firstIndex(where: { $0 === self }) else { + logger.warning( + "removeFromParent() w/o parent having the VC: \(self) \(parent.description))") + assertionFailure("parent set to a VC, but missing from the children!") + return + } + + willMove(toParent: nil) + parent.children.remove(at: idx) + self.parent = nil + didMove(toParent: nil) + } +} diff --git a/Sources/ViewController/ViewController/DefaultDescription.swift b/Sources/ViewController/ViewController/DefaultDescription.swift new file mode 100644 index 0000000..cf70065 --- /dev/null +++ b/Sources/ViewController/ViewController/DefaultDescription.swift @@ -0,0 +1,38 @@ +// +// DefaultDescription.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +extension ViewController { // MARK: - Description + + @inlinable + public var description: String { + let addr = String(UInt(bitPattern: ObjectIdentifier(self)), radix: 16) + var ms = "<\(type(of: self))[\(addr)]:" + appendAttributes(to: &ms) + ms += ">" + return ms + } + + @inlinable + public func appendAttributes(to description: inout String) { + defaultAppendAttributes(to: &description) + } + + @inlinable + public func defaultAppendAttributes(to description: inout String) { + // public, so that subclasses can call this "super" implementation! + if let v = title { description += " '\(v)'" } + assert(self !== presentedViewController) + + if activePresentations.count == 1, let v = activePresentations.first { + description += " presenting=\(v.viewController)[\(v.mode)]" + } + else if !activePresentations.isEmpty { + description += " presenting=#\(activePresentations.count)" + } + } +} diff --git a/Sources/ViewController/ViewController/RepresentedObject.swift b/Sources/ViewController/ViewController/RepresentedObject.swift new file mode 100644 index 0000000..78f492c --- /dev/null +++ b/Sources/ViewController/ViewController/RepresentedObject.swift @@ -0,0 +1,56 @@ +// +// RepresentedObject.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import Combine + +public extension ViewController { + + @inlinable + var representedObject : RepresentedValue? { + set { + if let storage = storageIfAvailable { + storage.representedObject = newValue + } + else if let value = newValue { + self.storage.representedObject = value + } + } + get { storageIfAvailable?.representedObject } + } +} + +public extension ViewController + where RepresentedValue: ObservableObject, + RepresentedValue.ObjectWillChangePublisher == ObservableObjectPublisher +{ + // A variant which subscribes to the representedObject if it is an + // ObservableObject. Needs a test. + + @inlinable + var representedObject : RepresentedValue? { + set { + if let storage = storageIfAvailable { + guard newValue !== storage.representedObject else { return } // same + storage.representedObjectSubscription = nil + storage.representedObject = newValue + storage.representedObjectSubscription = + newValue?.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + } + } + else if let value = newValue { + storage.representedObject = value + storage.representedObjectSubscription = + newValue?.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + } + } + } + get { storageIfAvailable?.representedObject } + } +} diff --git a/Sources/ViewController/ViewController/Subscriptions.swift b/Sources/ViewController/ViewController/Subscriptions.swift new file mode 100644 index 0000000..d612d04 --- /dev/null +++ b/Sources/ViewController/ViewController/Subscriptions.swift @@ -0,0 +1,75 @@ +// +// Subscriptions.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import Combine + +public extension ViewController { + + /** + * Subscribes the ViewController to changes in another ``ObservableObject``, + * usually a "model" object. + * If the other object emits a `willChange` event, so will the ViewController. + * + * Example: + * ```swift + * class Contacts: ViewController { + * + * let contacts = ContactsStore.shared + * + * init() { + * willChange(with: contacts) + * } + * } + * ``` + */ + @inlinable + func willChange(with model: T) { + + let subscription = model.objectWillChange.sink { [weak self] _ in + guard let me = self else { return } + me.objectWillChange.send() + } + + if storage.subscriptions == nil { + storage.subscriptions = Set() + } + storage.subscriptions?.insert(subscription) + } + + /** + * Subscribes the ViewController to changes in other ``ObservableObject``s, + * usually "model" objects. + * If the other object emits a `willChange` event, so will the ViewController. + * + * Example: + * ```swift + * class Contacts: ViewController { + * + * let contacts = ContactsStore.shared + * let calendars = CalendarsStore.shared + * let tasks = TasksStore.shared + * + * init() { + * willChange(with: contacts, calendars, tasks) + * } + * } + * ``` + */ + @inlinable + func willChange(with model1: T1, model2: T2, model3: T3?, + model4: T4?, model5: T5?) + where T1: ObservableObject, T2: ObservableObject, T3: ObservableObject, + T4: ObservableObject, T5: ObservableObject + { + willChange(with: model1) + willChange(with: model2) + if let model = model3 { willChange(with: model) } + if let model = model4 { willChange(with: model) } + if let model = model5 { willChange(with: model) } + } +} diff --git a/Sources/ViewController/ViewController/Title.swift b/Sources/ViewController/ViewController/Title.swift new file mode 100644 index 0000000..4650305 --- /dev/null +++ b/Sources/ViewController/ViewController/Title.swift @@ -0,0 +1,44 @@ +// +// Title.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +public extension ViewController { + + @inlinable + var navigationTitle : String { title ?? defaultNavigationTitle } + + /** + * Returns a navigation title based on the ``ViewController``s type name. + */ + @inlinable + var defaultNavigationTitle: String { + let typeName = "\(type(of: self))" + + let cutName : String = { + if typeName.hasSuffix("ViewController") { + return String(typeName.dropLast(14)) + } + else if typeName.hasSuffix("VC") { + return String(typeName.dropLast(14)) + } + else { + return typeName + } + }() + + if let idx = cutName.firstIndex(of: ".") { + return String(cutName[cutName.index(after: idx)...]) + } + + return cutName + } + + + @available(*, deprecated, renamed: "navigationTitle") + @inlinable + var navigationBarTitle : String { navigationTitle } +} diff --git a/Sources/ViewController/ViewController/TypeErasure.swift b/Sources/ViewController/ViewController/TypeErasure.swift new file mode 100644 index 0000000..bf9c41a --- /dev/null +++ b/Sources/ViewController/ViewController/TypeErasure.swift @@ -0,0 +1,23 @@ +// +// TypeErasure.swift +// ViewController +// +// Created by Helge Heß on 26.04.22. +// + +import SwiftUI + +public extension ViewController { + + @inlinable + var anyContentView : AnyView { AnyView(contentView) } + + @inlinable + var anyControlledContentView : AnyView { AnyView(controlledContentView) } + + @inlinable + var anyRepresentedObject : Any? { + set { representedObject = newValue as? RepresentedValue } + get { representedObject } + } +} diff --git a/Sources/ViewController/ViewController/ViewControllerStorage.swift b/Sources/ViewController/ViewController/ViewControllerStorage.swift new file mode 100644 index 0000000..2243d11 --- /dev/null +++ b/Sources/ViewController/ViewController/ViewControllerStorage.swift @@ -0,0 +1,238 @@ +// +// ViewControllerStorage.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import Foundation +import Combine + +// Why all this Storage mess? Because we'd really like to keep `ViewController` +// a protocol and not introduce a `ViewControllerBase` like object to provide +// persistence for implementors. + + +// MARK: - Forwarders (Peek and pop from the associated storage class) + +public extension ViewController { + + // TODO: Make a nice keypath subscript for the repetitive code + + @inlinable + var title : String? { + set { + if let storage = storageIfAvailable { storage.title = newValue } + else if let value = newValue { storage.title = value } + } + get { storageIfAvailable?.title } + } + + + // MARK: - Presentation + + @inlinable + var modalPresentationStyle: ViewControllerPresentationMode { + set { + if let storage = storageIfAvailable { + storage.modalPresentationStyle = newValue + } + else if newValue != .automatic { + storage.modalPresentationStyle = newValue + } + } + get { storageIfAvailable?.modalPresentationStyle ?? .automatic } + } + + @inlinable + var activePresentations : [ ViewControllerPresentation ] { + set { + if let storage = storageIfAvailable { storage.presentations = newValue } + else if !newValue.isEmpty { storage.presentations = newValue } + } + get { storageIfAvailable?.presentations ?? [] } + } + + @inlinable + var presentingViewController : _ViewController? { + set { + if let storage = storageIfAvailable { + storage.presentingViewController = newValue + } + else if let value = newValue { + storage.presentingViewController = value + } + } + get { storageIfAvailable?.presentingViewController } + } + + + // MARK: - Hierarchy + + @inlinable + var children : [ _ViewController ] { + set { + if let storage = storageIfAvailable { + storage.childViewControllers = newValue + } + else if !newValue.isEmpty { + self.storage.childViewControllers = newValue + } + } + get { storageIfAvailable?.childViewControllers ?? [] } + } + + @inlinable + var parent : _ViewController? { + set { + if let storage = storageIfAvailable { + storage.parentViewController = newValue + } + else if let value = newValue { + self.storage.parentViewController = value + } + } + get { storageIfAvailable?.parentViewController } + } +} + + +// MARK: - Holder Class + +/** + * An internal state holder class used to associated some default properties + * with a VC. + * This is done to keep ``ViewController`` as a protocol. + * Alternative: Keep ``ViewController`` as a protocol, but add + * ``ViewControllerBase`` as a base class. + * + * To access the storage of the ViewController, use + * ``ViewController/storage`` or ``ViewController/storageIfAvailable``. + */ +@usableFromInline +internal class ViewControllerStorage: NSObject where VC: ViewController { + // So the important point here is to emit changes! + + weak var viewController : VC? + + init(_ viewController: VC) { self.viewController = viewController } + + @usableFromInline + internal var representedObject : VC.RepresentedValue? { + willSet { viewController?.objectWillChange.send() } + } + @usableFromInline + internal var representedObjectSubscription: AnyCancellable? + + + // MARK: - Title + + @usableFromInline + internal var title : String? { + willSet { viewController?.objectWillChange.send() } + } + + + // MARK: - Presentation + + // no change event needed for this? + @usableFromInline + internal var modalPresentationStyle = ViewControllerPresentationMode.automatic + + @usableFromInline + internal var presentations : [ ViewControllerPresentation ] = [] { + willSet { + if presentations.count == newValue.count { + if newValue.isEmpty { return } // both empty, do not emit + + if !zip(presentations, newValue).contains(where: { lhs, rhs in + lhs.viewController !== rhs.viewController || lhs.mode != rhs.mode + }) { return } // same + } + + viewController?.objectWillChange.send() + } + } + + @usableFromInline + internal weak var presentingViewController : _ViewController? + + + // MARK: - Hierarchy + + @usableFromInline + internal var childViewControllers = [ _ViewController ]() { + willSet { + guard childViewControllers.map({ ObjectIdentifier($0) }) + != newValue.map({ ObjectIdentifier($0) }) else { return } + viewController?.objectWillChange.send() + } + } + + @usableFromInline + internal weak var parentViewController : _ViewController? + + + // MARK: - Subscriptions + + /** + * ViewController's as ObservableObject's very often depend on some model + * objects or other publishers. + * This provides a default storage for such. + */ + @usableFromInline + internal var subscriptions : Set? +} + + +// MARK: - Associated Object + +fileprivate var associatedObjectToken = 42 + +internal extension ViewController { + + /** + * Returns the associated storage for the ``ViewController``, + * i.e. the place where the protocol puts the tracking state. + * + * If the ViewController doesn't have storage yet, it gets allocated and + * assigned. + * + * Use ``ViewController/storageIfAvailable`` to avoid storage allocation. + */ + @usableFromInline + var storage : ViewControllerStorage { + get { + if let storage = storageIfAvailable { return storage } + let storage = ViewControllerStorage(self) + storageIfAvailable = storage + return storage + } + } + + /** + * Returns the associated storage for the ``ViewController``, + * i.e. the place where the protocol puts the tracking state. + * + * If the ViewController doesn't have storage yet, nil is returned. + * + * Use ``ViewController/storage`` to allocate storage on demand. + */ + @usableFromInline + var storageIfAvailable : ViewControllerStorage? { + set { + assert(newValue != nil, "Attempt to clear VC storage?!") + objc_setAssociatedObject(self, &associatedObjectToken, newValue, + .OBJC_ASSOCIATION_RETAIN) + } + get { + guard let value = objc_getAssociatedObject(self, &associatedObjectToken) else { + return nil + } + assert(value is ViewControllerStorage, + "Unexpected storage associated w/ ViewController \(value) \(self)") + return value as? ViewControllerStorage + } + } +} diff --git a/Sources/ViewController/ViewController/_ViewController.swift b/Sources/ViewController/ViewController/_ViewController.swift new file mode 100644 index 0000000..11327b4 --- /dev/null +++ b/Sources/ViewController/ViewController/_ViewController.swift @@ -0,0 +1,196 @@ +// +// _ViewController.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI +import Combine + +/** + * The base protocol which can be used as an existential. + * + * Associated types added by ``ViewController``: + * - ObjectWillChangePublisher + * - ContentView + * - RepresentedValue + */ +public protocol _ViewController: AnyObject, CustomStringConvertible { + // Even just `ObservableObject` is a PAT. + + typealias ObjectWillChangePublisher = ObservableObjectPublisher + + typealias PresentationMode = ViewControllerPresentationMode + + // MARK: - Titles + + /** + * Get or set a title associated with a ViewController. + */ + var title : String? { set get } + + /** + * Returns the ``title`` of the ``ViewController``, + * but falls back to a default title in case that's not available. + * + * Suitable for use in `navigationTitle`, `navigationBarTitle` and similar + * SwiftUI `View` modifiers. + */ + var navigationTitle : String { get } + + + // MARK: - Presentation + + /** + * Defines the default style in which a ``ViewController`` wants to be + * presented in. + */ + var modalPresentationStyle : ViewControllerPresentationMode { set get } + + /// An internal property to track the ViewController presentation. + var activePresentations : [ ViewControllerPresentation ] { set get } + + /** + * If the ``ViewController`` is presenting another, this property returns + * the presented ViewController. + */ + var presentedViewController : _ViewController? { get } + + /** + * If the ``ViewController`` got presented by another, this property returns + * the ViewController doing the presentation. + */ + var presentingViewController : _ViewController? { set get } + + /** + * This is called if the VC was added as the presented VC, but SwiftUI + * still needs a tick to diff and actually display the related view. + */ + func willAppear() + + /** + * This is called if the VC was removed as a presented VC. SwiftUI will still + * need a tick to diff and remove the related view. + */ + func willDisappear() + + /** + * Make the ``ViewController`` the currently presented ``ViewController`` for + * the given mode. + */ + func present(_ viewController: VC, mode: PresentationMode) + where VC: ViewController + /** + * Make the ``ViewController`` the currently presented ``ViewController``, + * in `.automatic` mode. + */ + func present(_ viewController: VC) + /** + * Present a ``ViewController`` that doesn't have a + * ``ViewController/ContentView`` assigned. + */ + func present(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + func show(_ viewController: VC) + func show(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + func showDetail(_ viewController: VC) + func showDetail(_ viewController: VC) + where VC.ContentView == DefaultViewControllerView + + /// Internal method which allows presenting view controllers the actual + /// presentation further down in the stack. + func show(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + /// Internal method which allows presenting view controllers the actual + /// presentation further down in the stack. + func showDetail(_ viewController: VC, in owner: OwnerVC) + where VC: ViewController, OwnerVC: _ViewController + + /** + * Remove the ViewController from being presented in its presenting + * ViewController. + * + * Will call `willDisappear` (only) if it actually was presented by the + * parent. + */ + func dismiss() + + + // MARK: - Hierarchy + + /** + * The array of contained view controllers (children). + * + * Use ``addChild`` to add children to this array and use ``removeFromParent`` + * to remove a child from its parent. + * + * Not to be confused w/ ``presentedViewControllers``. + */ + var children : [ _ViewController ] { set get } + /** + * The parent of a contained ``ViewController`` (if it is actually contained). + * + * Use ``addChild`` to add children to a parent and use ``removeFromParent`` + * to remove a child from its parent. + * + * Not to be confused w/ ``presentingViewController``. + */ + var parent : _ViewController? { set get } + + /** + * This is called once the ViewController will be added or removed as a child. + */ + func willMove(toParent parent: _ViewController?) + + /** + * This is called once the ViewController was added or removed as a child. + */ + func didMove(toParent parent: _ViewController?) + + /** + * Add the specified ``ViewController`` as a child ("contained") + * ViewController. + * + * This will add the `viewController` to the ``children`` array and set its + * ``parent`` property. + */ + func addChild(_ viewController: VC) + + /** + * Remove the ``ViewController`` from its parent. + */ + func removeFromParent() + + + // MARK: - Better Description + + /** + * Override in subclasses to add own properties to the VC description. + */ + func appendAttributes(to description: inout String) + + + // MARK: - Type Erasure + + /** + * Returns the type erased ``ContentView`` of the ``ViewController``. + */ + var anyContentView : AnyView { get } + + /** + * Returns the type erased ``ContentView`` of the ``ViewController``, + * with the ``ViewController`` being applied as the ``controlled(by:)`` + * View. + */ + var anyControlledContentView : AnyView { get } + + /** + * Returns the type erased represented object of the ``ViewController``. + */ + var anyRepresentedObject : Any? { set get } +} diff --git a/Sources/ViewController/ViewControllerEnvironment.swift b/Sources/ViewController/ViewControllerEnvironment.swift new file mode 100644 index 0000000..79ec7f5 --- /dev/null +++ b/Sources/ViewController/ViewControllerEnvironment.swift @@ -0,0 +1,98 @@ +// +// ViewControllerPresentationModifier.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +public extension EnvironmentValues { + + @usableFromInline + internal struct ViewControllerKey: EnvironmentKey { + public static let defaultValue : _ViewController? = nil + } + + /** + * Allows access to the ``ViewController``, w/o having the View refreshed if + * the VC changes. + * + * Can be used like this: + * ```swift + * struct ContentView: View { + * @Environment(\.viewController) private var viewController + * var body: some View { + * Text(verbatim: "VC: \(viewController)") + * } + * } + * ``` + */ + @inlinable + var viewController : _ViewController? { + set { self[ViewControllerKey.self] = newValue } + get { self[ViewControllerKey.self] } + } +} + +public extension View { + + /** + * `.controlled(vc)` pushes the ``ViewController`` as an environment object, + * both under its concrete type, and as the generic ``ViewController``. + * + * It also pushes the VC into the `viewController` environment key, which + * allows access w/o having a View refresh on VC changes (kinda like an + * `UnobservedEnvironmentObject`). + * + * This also sets up the sheet/programmatic `NavigationLink` for automatic + * presentation. + * + * Externally this modifier is usually only used at the very top of the VC + * stack, i.e. for the "SceneController": + * ```swift + * struct ContentView: View { + * + * @StateObject private var viewController : WidgetViewVC + * + * var body: some View { + * NavigationView { + * WidgetViewVC.ContentView() + * .controlled(by: viewController) + * } + * } + * } + * ``` + * + * - Parameters: + * - viewController: The (instantiated) view controller object to apply to + * View. + */ + func controlled(by viewController: VC) + -> some SwiftUI.View + { + // Note: Also used internally during presentation. + self + .modifier(AutoPresentationViewModifier(viewController: viewController)) + .modifier(ControlledViewModifier(viewController: viewController)) + } +} + +// Push the VC into the environment by three means: +// - as an EnvironmentObject using its concrete class +// - type-erased, as an ``AnyViewController`` EnvironmentObject +// - as a plain `viewController` environment key (w/o state observation) +fileprivate struct ControlledViewModifier: ViewModifier + where VC: ViewController +{ + + let viewController : VC + + func body(content: Content) -> some View { + content + .environmentObject(viewController) + .environmentObject(AnyViewController(viewController)) + .environment(\.viewController, viewController) + } +} diff --git a/Sources/ViewController/ViewControllerView.swift b/Sources/ViewController/ViewControllerView.swift new file mode 100644 index 0000000..5246f3f --- /dev/null +++ b/Sources/ViewController/ViewControllerView.swift @@ -0,0 +1,62 @@ +// +// ViewControllerView.swift +// ViewController +// +// Created by Helge Heß. +// Copyright © 2022 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * A View that can be instantiated w/o further arguments. + * + * This is used within ``ViewController``s, to instantiate their + * ``ViewController/ContentView``. + */ +public protocol ViewControllerView: View { + init() +} + +extension EmptyView: ViewControllerView {} + + +#if true // This Works + + public typealias DefaultViewControllerView = EmptyView + +#else // 2022-04-24: This Segfaults swiftc in Xcode 13.3 + + #if DEBUG + public struct DefaultViewControllerView: View { + @Environment(\.viewController) private var viewController + + @usableFromInline + init() { assertionFailure("No VC View is defined!") } + + public var body: some View { + VStack { + Label("Missing VC View", systemImage: "questionmark.circle") + Spacer() + if let viewController = viewController { + Text(verbatim: "Class: \(type(of: viewController))") + Text(verbatim: "ID: \(ObjectIdentifier(viewController))") + Text(verbatim: "\(viewController)") + } + else { + Text("No VC set in environment?!") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + } + #else + public struct DefaultViewControllerView: View { + public var body: some View { + // TODO: report issue etc? (using bug reporter link in Info.plist?) + Label("Missing VC View", systemImage: "questionmark.circle") + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + } + #endif +#endif diff --git a/ViewController.xcodeproj/project.pbxproj b/ViewController.xcodeproj/project.pbxproj new file mode 100644 index 0000000..583a1ef --- /dev/null +++ b/ViewController.xcodeproj/project.pbxproj @@ -0,0 +1,541 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + E800501A2816B00E00E4805C /* DebugMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050192816B00E00E4805C /* DebugMode.swift */; }; + E800501B2816B00E00E4805C /* DebugMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050192816B00E00E4805C /* DebugMode.swift */; }; + E800501D2816B09A00E4805C /* DebugOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501C2816B09A00E4805C /* DebugOverlay.swift */; }; + E800501E2816B09A00E4805C /* DebugOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501C2816B09A00E4805C /* DebugOverlay.swift */; }; + E80050202816C40600E4805C /* AutoPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501F2816C40600E4805C /* AutoPresentation.swift */; }; + E80050212816C40600E4805C /* AutoPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501F2816C40600E4805C /* AutoPresentation.swift */; }; + E80050232816EE1D00E4805C /* HierarchyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050222816EE1D00E4805C /* HierarchyView.swift */; }; + E80050242816EE1D00E4805C /* HierarchyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050222816EE1D00E4805C /* HierarchyView.swift */; }; + E80050262816EE3400E4805C /* ViewControllerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050252816EE3400E4805C /* ViewControllerInfo.swift */; }; + E80050272816EE3400E4805C /* ViewControllerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050252816EE3400E4805C /* ViewControllerInfo.swift */; }; + E836E5A0280EEA870001B85E /* PushPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E59F280EEA870001B85E /* PushPresentation.swift */; }; + E836E5A1280EEA870001B85E /* PushPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E59F280EEA870001B85E /* PushPresentation.swift */; }; + E836E5A3280EEACA0001B85E /* SheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5A2280EEACA0001B85E /* SheetPresentation.swift */; }; + E836E5A4280EEACA0001B85E /* SheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5A2280EEACA0001B85E /* SheetPresentation.swift */; }; + E836E5AD280EECD50001B85E /* PushLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5AC280EECD50001B85E /* PushLink.swift */; }; + E836E5AE280EECD50001B85E /* PushLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5AC280EECD50001B85E /* PushLink.swift */; }; + E83ADA662814267600D98D82 /* Containment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA652814267600D98D82 /* Containment.swift */; }; + E83ADA672814267600D98D82 /* Containment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA652814267600D98D82 /* Containment.swift */; }; + E83ADA78281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */; }; + E83ADA79281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */; }; + E88DCE6B2812C4CF00CD5203 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */; }; + E88DCE6C2812C4CF00CD5203 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */; }; + E88DCE702812C83C00CD5203 /* ReExports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6F2812C83C00CD5203 /* ReExports.swift */; }; + E88DCE712812C83C00CD5203 /* ReExports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6F2812C83C00CD5203 /* ReExports.swift */; }; + E88DCE732812CEE700CD5203 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE722812CEE700CD5203 /* ViewController.swift */; }; + E88DCE742812CEE700CD5203 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE722812CEE700CD5203 /* ViewController.swift */; }; + E88DCE762812D10000CD5203 /* ViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE752812D10000CD5203 /* ViewControllerView.swift */; }; + E88DCE772812D10000CD5203 /* ViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE752812D10000CD5203 /* ViewControllerView.swift */; }; + E88DCE7A2812D1B900CD5203 /* DefaultDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE792812D1B900CD5203 /* DefaultDescription.swift */; }; + E88DCE7B2812D1B900CD5203 /* DefaultDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE792812D1B900CD5203 /* DefaultDescription.swift */; }; + E88DCE7D2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */; }; + E88DCE7E2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */; }; + E88DCE802812D6A700CD5203 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */; }; + E88DCE812812D6A700CD5203 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */; }; + E88DCE832812DD8B00CD5203 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE822812DD8B00CD5203 /* Title.swift */; }; + E88DCE842812DD8B00CD5203 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE822812DD8B00CD5203 /* Title.swift */; }; + E88DCE862812DF8200CD5203 /* RepresentedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE852812DF8200CD5203 /* RepresentedObject.swift */; }; + E88DCE872812DF8200CD5203 /* RepresentedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE852812DF8200CD5203 /* RepresentedObject.swift */; }; + E88DCE892812E9CD00CD5203 /* Presentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE882812E9CD00CD5203 /* Presentation.swift */; }; + E88DCE8A2812E9CD00CD5203 /* Presentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE882812E9CD00CD5203 /* Presentation.swift */; }; + E88DCE8F2812EAF800CD5203 /* AnyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */; }; + E88DCE902812EAF800CD5203 /* AnyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */; }; + E88DCE922812FA7A00CD5203 /* _ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE912812FA7A00CD5203 /* _ViewController.swift */; }; + E88DCE932812FA7A00CD5203 /* _ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE912812FA7A00CD5203 /* _ViewController.swift */; }; + E88DCE9B28130B8300CD5203 /* RenderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE9A28130B8300CD5203 /* RenderContentView.swift */; }; + E88DCE9C28130B8300CD5203 /* RenderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE9A28130B8300CD5203 /* RenderContentView.swift */; }; + E8BA6EBB281588D800FA6C5C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBA281588D800FA6C5C /* Logger.swift */; }; + E8BA6EBC281588D800FA6C5C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBA281588D800FA6C5C /* Logger.swift */; }; + E8BA6EBF2815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */; }; + E8BA6EC02815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */; }; + E8BA6EC32815956200FA6C5C /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EC22815956200FA6C5C /* NavigationController.swift */; }; + E8BA6EC42815956200FA6C5C /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EC22815956200FA6C5C /* NavigationController.swift */; }; + E8E7066F27F2014F00F50160 /* PresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066827F2014F00F50160 /* PresentationMode.swift */; }; + E8E7067027F2014F00F50160 /* PresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066827F2014F00F50160 /* PresentationMode.swift */; }; + E8E7067327F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */; }; + E8E7067427F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */; }; + E8FC0F12281848880051E640 /* TypeErasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FC0F11281848880051E640 /* TypeErasure.swift */; }; + E8FC0F13281848880051E640 /* TypeErasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FC0F11281848880051E640 /* TypeErasure.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + E80050192816B00E00E4805C /* DebugMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMode.swift; sourceTree = ""; }; + E800501C2816B09A00E4805C /* DebugOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOverlay.swift; sourceTree = ""; }; + E800501F2816C40600E4805C /* AutoPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoPresentation.swift; sourceTree = ""; }; + E80050222816EE1D00E4805C /* HierarchyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HierarchyView.swift; sourceTree = ""; }; + E80050252816EE3400E4805C /* ViewControllerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerInfo.swift; sourceTree = ""; }; + E836E59E280EEA0A0001B85E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + E836E59F280EEA870001B85E /* PushPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPresentation.swift; sourceTree = ""; }; + E836E5A2280EEACA0001B85E /* SheetPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetPresentation.swift; sourceTree = ""; }; + E836E5AC280EECD50001B85E /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; + E83ADA652814267600D98D82 /* Containment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Containment.swift; sourceTree = ""; }; + E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeMismatchInfoView.swift; sourceTree = ""; }; + E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + E88DCE6F2812C83C00CD5203 /* ReExports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReExports.swift; sourceTree = ""; }; + E88DCE722812CEE700CD5203 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + E88DCE752812D10000CD5203 /* ViewControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerView.swift; sourceTree = ""; }; + E88DCE792812D1B900CD5203 /* DefaultDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDescription.swift; sourceTree = ""; }; + E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerStorage.swift; sourceTree = ""; }; + E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = ""; }; + E88DCE822812DD8B00CD5203 /* Title.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Title.swift; sourceTree = ""; }; + E88DCE852812DF8200CD5203 /* RepresentedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepresentedObject.swift; sourceTree = ""; }; + E88DCE882812E9CD00CD5203 /* Presentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentation.swift; sourceTree = ""; }; + E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyViewController.swift; sourceTree = ""; }; + E88DCE912812FA7A00CD5203 /* _ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _ViewController.swift; sourceTree = ""; }; + E88DCE9A28130B8300CD5203 /* RenderContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderContentView.swift; sourceTree = ""; }; + E8BA6EBA281588D800FA6C5C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerPresentation.swift; sourceTree = ""; }; + E8BA6EC22815956200FA6C5C /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; + E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libViewController-macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E8E7065627F1EEA600F50160 /* AppKitBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppKitBase.xcconfig; sourceTree = ""; }; + E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MobileStaticLib.xcconfig; sourceTree = ""; }; + E8E7065827F1EEA600F50160 /* UIKitBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UIKitBase.xcconfig; sourceTree = ""; }; + E8E7065927F1EEA600F50160 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + E8E7065A27F1EEA600F50160 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MacStaticLib.xcconfig; sourceTree = ""; }; + E8E7065C27F1EEA600F50160 /* StaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = StaticLib.xcconfig; sourceTree = ""; }; + E8E7065D27F1EEA600F50160 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; + E8E7066527F1EF4300F50160 /* libViewController-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libViewController-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E8E7066827F2014F00F50160 /* PresentationMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationMode.swift; sourceTree = ""; }; + E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerEnvironment.swift; sourceTree = ""; }; + E8FC0F11281848880051E640 /* TypeErasure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeErasure.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E8E7064A27F1EE4700F50160 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E8E7066127F1EF4300F50160 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E80050182816AFF000E4805C /* Debugging */ = { + isa = PBXGroup; + children = ( + E80050192816B00E00E4805C /* DebugMode.swift */, + E800501C2816B09A00E4805C /* DebugOverlay.swift */, + E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */, + E80050222816EE1D00E4805C /* HierarchyView.swift */, + E80050252816EE3400E4805C /* ViewControllerInfo.swift */, + ); + path = Debugging; + sourceTree = ""; + }; + E836E59D280EE9F40001B85E /* Presentations */ = { + isa = PBXGroup; + children = ( + E88DCE882812E9CD00CD5203 /* Presentation.swift */, + E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */, + E8E7066827F2014F00F50160 /* PresentationMode.swift */, + E800501F2816C40600E4805C /* AutoPresentation.swift */, + E836E59F280EEA870001B85E /* PushPresentation.swift */, + E836E5A2280EEACA0001B85E /* SheetPresentation.swift */, + ); + path = Presentations; + sourceTree = ""; + }; + E836E5AB280EECC40001B85E /* NavigationLink */ = { + isa = PBXGroup; + children = ( + E836E5AC280EECD50001B85E /* PushLink.swift */, + ); + path = NavigationLink; + sourceTree = ""; + }; + E8BA6EBD2815912300FA6C5C /* ViewController */ = { + isa = PBXGroup; + children = ( + E88DCE912812FA7A00CD5203 /* _ViewController.swift */, + E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */, + E88DCE792812D1B900CD5203 /* DefaultDescription.swift */, + E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */, + E88DCE822812DD8B00CD5203 /* Title.swift */, + E88DCE852812DF8200CD5203 /* RepresentedObject.swift */, + E83ADA652814267600D98D82 /* Containment.swift */, + E8FC0F11281848880051E640 /* TypeErasure.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + E8BA6EC12815954300FA6C5C /* ContainerViewControllers */ = { + isa = PBXGroup; + children = ( + E8BA6EC22815956200FA6C5C /* NavigationController.swift */, + ); + path = ContainerViewControllers; + sourceTree = ""; + }; + E8E7064327F1EE4700F50160 = { + isa = PBXGroup; + children = ( + E836E59E280EEA0A0001B85E /* README.md */, + E8E7065327F1EEA100F50160 /* Sources */, + E8E7065527F1EEA600F50160 /* xcconfig */, + E8E7064D27F1EE4700F50160 /* Products */, + ); + sourceTree = ""; + }; + E8E7064D27F1EE4700F50160 /* Products */ = { + isa = PBXGroup; + children = ( + E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */, + E8E7066527F1EF4300F50160 /* libViewController-iOS.a */, + ); + name = Products; + sourceTree = ""; + }; + E8E7065327F1EEA100F50160 /* Sources */ = { + isa = PBXGroup; + children = ( + E8E7065427F1EEA100F50160 /* ViewController */, + ); + path = Sources; + sourceTree = ""; + }; + E8E7065427F1EEA100F50160 /* ViewController */ = { + isa = PBXGroup; + children = ( + E88DCE722812CEE700CD5203 /* ViewController.swift */, + E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */, + E8BA6EBD2815912300FA6C5C /* ViewController */, + E836E59D280EE9F40001B85E /* Presentations */, + E836E5AB280EECC40001B85E /* NavigationLink */, + E8BA6EC12815954300FA6C5C /* ContainerViewControllers */, + E80050182816AFF000E4805C /* Debugging */, + E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */, + E88DCE752812D10000CD5203 /* ViewControllerView.swift */, + E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */, + E88DCE9A28130B8300CD5203 /* RenderContentView.swift */, + E8BA6EBA281588D800FA6C5C /* Logger.swift */, + E88DCE6F2812C83C00CD5203 /* ReExports.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + E8E7065527F1EEA600F50160 /* xcconfig */ = { + isa = PBXGroup; + children = ( + E8E7065627F1EEA600F50160 /* AppKitBase.xcconfig */, + E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */, + E8E7065827F1EEA600F50160 /* UIKitBase.xcconfig */, + E8E7065927F1EEA600F50160 /* Debug.xcconfig */, + E8E7065A27F1EEA600F50160 /* Release.xcconfig */, + E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */, + E8E7065C27F1EEA600F50160 /* StaticLib.xcconfig */, + E8E7065D27F1EEA600F50160 /* Base.xcconfig */, + ); + path = xcconfig; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + E8E7064827F1EE4700F50160 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E8E7065F27F1EF4300F50160 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + E8E7064B27F1EE4700F50160 /* ViewController-macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = E8E7065027F1EE4700F50160 /* Build configuration list for PBXNativeTarget "ViewController-macOS" */; + buildPhases = ( + E8E7064827F1EE4700F50160 /* Headers */, + E8E7064927F1EE4700F50160 /* Sources */, + E8E7064A27F1EE4700F50160 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ViewController-macOS"; + productName = ViewController; + productReference = E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */; + productType = "com.apple.product-type.library.static"; + }; + E8E7065E27F1EF4300F50160 /* ViewController-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = E8E7066227F1EF4300F50160 /* Build configuration list for PBXNativeTarget "ViewController-iOS" */; + buildPhases = ( + E8E7065F27F1EF4300F50160 /* Headers */, + E8E7066027F1EF4300F50160 /* Sources */, + E8E7066127F1EF4300F50160 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ViewController-iOS"; + productName = ViewController; + productReference = E8E7066527F1EF4300F50160 /* libViewController-iOS.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E8E7064427F1EE4700F50160 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1320; + TargetAttributes = { + E8E7064B27F1EE4700F50160 = { + CreatedOnToolsVersion = 13.2.1; + LastSwiftMigration = 1320; + }; + E8E7065E27F1EF4300F50160 = { + LastSwiftMigration = 1320; + }; + }; + }; + buildConfigurationList = E8E7064727F1EE4700F50160 /* Build configuration list for PBXProject "ViewController" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E8E7064327F1EE4700F50160; + productRefGroup = E8E7064D27F1EE4700F50160 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E8E7064B27F1EE4700F50160 /* ViewController-macOS */, + E8E7065E27F1EF4300F50160 /* ViewController-iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + E8E7064927F1EE4700F50160 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E8BA6EC32815956200FA6C5C /* NavigationController.swift in Sources */, + E8E7067327F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */, + E88DCE762812D10000CD5203 /* ViewControllerView.swift in Sources */, + E80050232816EE1D00E4805C /* HierarchyView.swift in Sources */, + E88DCE6B2812C4CF00CD5203 /* MainViewController.swift in Sources */, + E88DCE802812D6A700CD5203 /* Subscriptions.swift in Sources */, + E8BA6EBF2815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */, + E88DCE702812C83C00CD5203 /* ReExports.swift in Sources */, + E836E5A3280EEACA0001B85E /* SheetPresentation.swift in Sources */, + E83ADA662814267600D98D82 /* Containment.swift in Sources */, + E800501D2816B09A00E4805C /* DebugOverlay.swift in Sources */, + E83ADA78281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */, + E8BA6EBB281588D800FA6C5C /* Logger.swift in Sources */, + E88DCE7A2812D1B900CD5203 /* DefaultDescription.swift in Sources */, + E80050262816EE3400E4805C /* ViewControllerInfo.swift in Sources */, + E88DCE8F2812EAF800CD5203 /* AnyViewController.swift in Sources */, + E8FC0F12281848880051E640 /* TypeErasure.swift in Sources */, + E88DCE862812DF8200CD5203 /* RepresentedObject.swift in Sources */, + E88DCE832812DD8B00CD5203 /* Title.swift in Sources */, + E80050202816C40600E4805C /* AutoPresentation.swift in Sources */, + E836E5A0280EEA870001B85E /* PushPresentation.swift in Sources */, + E800501A2816B00E00E4805C /* DebugMode.swift in Sources */, + E88DCE892812E9CD00CD5203 /* Presentation.swift in Sources */, + E88DCE7D2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */, + E8E7066F27F2014F00F50160 /* PresentationMode.swift in Sources */, + E88DCE9B28130B8300CD5203 /* RenderContentView.swift in Sources */, + E88DCE922812FA7A00CD5203 /* _ViewController.swift in Sources */, + E836E5AD280EECD50001B85E /* PushLink.swift in Sources */, + E88DCE732812CEE700CD5203 /* ViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E8E7066027F1EF4300F50160 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E8BA6EC42815956200FA6C5C /* NavigationController.swift in Sources */, + E8E7067427F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */, + E88DCE772812D10000CD5203 /* ViewControllerView.swift in Sources */, + E80050242816EE1D00E4805C /* HierarchyView.swift in Sources */, + E88DCE6C2812C4CF00CD5203 /* MainViewController.swift in Sources */, + E88DCE812812D6A700CD5203 /* Subscriptions.swift in Sources */, + E8BA6EC02815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */, + E88DCE712812C83C00CD5203 /* ReExports.swift in Sources */, + E836E5A4280EEACA0001B85E /* SheetPresentation.swift in Sources */, + E83ADA672814267600D98D82 /* Containment.swift in Sources */, + E800501E2816B09A00E4805C /* DebugOverlay.swift in Sources */, + E83ADA79281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */, + E8BA6EBC281588D800FA6C5C /* Logger.swift in Sources */, + E88DCE7B2812D1B900CD5203 /* DefaultDescription.swift in Sources */, + E80050272816EE3400E4805C /* ViewControllerInfo.swift in Sources */, + E88DCE902812EAF800CD5203 /* AnyViewController.swift in Sources */, + E8FC0F13281848880051E640 /* TypeErasure.swift in Sources */, + E88DCE872812DF8200CD5203 /* RepresentedObject.swift in Sources */, + E88DCE842812DD8B00CD5203 /* Title.swift in Sources */, + E80050212816C40600E4805C /* AutoPresentation.swift in Sources */, + E836E5A1280EEA870001B85E /* PushPresentation.swift in Sources */, + E800501B2816B00E00E4805C /* DebugMode.swift in Sources */, + E88DCE8A2812E9CD00CD5203 /* Presentation.swift in Sources */, + E88DCE7E2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */, + E8E7067027F2014F00F50160 /* PresentationMode.swift in Sources */, + E88DCE9C28130B8300CD5203 /* RenderContentView.swift in Sources */, + E88DCE932812FA7A00CD5203 /* _ViewController.swift in Sources */, + E836E5AE280EECD50001B85E /* PushLink.swift in Sources */, + E88DCE742812CEE700CD5203 /* ViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + E8E7064E27F1EE4700F50160 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065927F1EEA600F50160 /* Debug.xcconfig */; + buildSettings = { + MACOSX_DEPLOYMENT_TARGET = 11.0; + SDKROOT = macosx; + }; + name = Debug; + }; + E8E7064F27F1EE4700F50160 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065A27F1EEA600F50160 /* Release.xcconfig */; + buildSettings = { + MACOSX_DEPLOYMENT_TARGET = 11.0; + SDKROOT = macosx; + }; + name = Release; + }; + E8E7065127F1EE4700F50160 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4GXF3JAMM4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_MODULE_NAME = ViewController; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + E8E7065227F1EE4700F50160 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4GXF3JAMM4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_MODULE_NAME = ViewController; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + E8E7066327F1EF4300F50160 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4GXF3JAMM4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_MODULE_NAME = ViewController; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + E8E7066427F1EF4300F50160 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4GXF3JAMM4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_MODULE_NAME = ViewController; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E8E7064727F1EE4700F50160 /* Build configuration list for PBXProject "ViewController" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E8E7064E27F1EE4700F50160 /* Debug */, + E8E7064F27F1EE4700F50160 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E8E7065027F1EE4700F50160 /* Build configuration list for PBXNativeTarget "ViewController-macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E8E7065127F1EE4700F50160 /* Debug */, + E8E7065227F1EE4700F50160 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E8E7066227F1EF4300F50160 /* Build configuration list for PBXNativeTarget "ViewController-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E8E7066327F1EF4300F50160 /* Debug */, + E8E7066427F1EF4300F50160 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E8E7064427F1EE4700F50160 /* Project object */; +} diff --git a/ViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-iOS.xcscheme b/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-iOS.xcscheme new file mode 100644 index 0000000..f504c73 --- /dev/null +++ b/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-iOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-macOS.xcscheme b/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-macOS.xcscheme new file mode 100644 index 0000000..6c03e8a --- /dev/null +++ b/ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-macOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xcconfig/AppKitBase.xcconfig b/xcconfig/AppKitBase.xcconfig new file mode 100644 index 0000000..e968204 --- /dev/null +++ b/xcconfig/AppKitBase.xcconfig @@ -0,0 +1,7 @@ +// Base config file for Mac projects + +#include "Base.xcconfig" + +MACOSX_DEPLOYMENT_TARGET = 11 +SDKROOT = macosx + diff --git a/xcconfig/Base.xcconfig b/xcconfig/Base.xcconfig new file mode 100644 index 0000000..646f37e --- /dev/null +++ b/xcconfig/Base.xcconfig @@ -0,0 +1,69 @@ +// Base config + +// Signing +CODE_SIGN_IDENTITY = - + +// Include +ALWAYS_SEARCH_USER_PATHS = NO + +// Language +GCC_C_LANGUAGE_STANDARD = gnu99 +GCC_NO_COMMON_BLOCKS = YES +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +ENABLE_STRICT_OBJC_MSGSEND = YES +CLANG_ENABLE_OBJC_WEAK = YES + +// Warnings +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES + +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES + +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_STRICT_PROTOTYPES = YES + +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE + +CLANG_CXX_LANGUAGE_STANDARD = gnu++17 +CLANG_CXX_LIBRARY = libc++ + + +MTL_FAST_MATH = YES +COMBINE_HIDPI_IMAGES = YES + +// For embedded frameworks. +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks + +OTHER_LDFLAGS = $(inherited) -ObjC // to import categories in static libs + +OTHER_SWIFT_FLAGS = -DXcode + +// This is a little lame (Xcode shouldn't tie the code to a version ..) +SWIFT_VERSION = 5.0 diff --git a/xcconfig/Debug.xcconfig b/xcconfig/Debug.xcconfig new file mode 100644 index 0000000..8078f9e --- /dev/null +++ b/xcconfig/Debug.xcconfig @@ -0,0 +1,24 @@ +// Extra debug settings are always the same .. + +#include "Base.xcconfig" + +ONLY_ACTIVE_ARCH = YES + +// Debugging +DEBUG_INFORMATION_FORMAT = dwarf +ENABLE_TESTABILITY = YES +COPY_PHASE_STRIP = NO +GCC_OPTIMIZATION_LEVEL = 0 +ENABLE_NS_ASSERTIONS = YES +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 +MTL_ENABLE_DEBUG_INFO = YES +GCC_DYNAMIC_NO_PIC = NO + +// Swift +SWIFT_OPTIMIZATION_LEVEL = -Onone + +// -DXcode is set in Base, but this one seems to override it +// OTHER_SWIFT_FLAGS[config=Debug] = $(inherited) -DDEBUG -DXcode +OTHER_SWIFT_FLAGS = $(inherited) -DDEBUG -DXcode + +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG diff --git a/xcconfig/MacStaticLib.xcconfig b/xcconfig/MacStaticLib.xcconfig new file mode 100644 index 0000000..39a861d --- /dev/null +++ b/xcconfig/MacStaticLib.xcconfig @@ -0,0 +1,4 @@ +// Mac Static Lib + +#include "AppKitBase.xcconfig" +#include "StaticLib.xcconfig" diff --git a/xcconfig/MobileStaticLib.xcconfig b/xcconfig/MobileStaticLib.xcconfig new file mode 100644 index 0000000..2aaba88 --- /dev/null +++ b/xcconfig/MobileStaticLib.xcconfig @@ -0,0 +1,5 @@ +// UIKit Static Lib + +#include "UIKitBase.xcconfig" +#include "StaticLib.xcconfig" + diff --git a/xcconfig/Release.xcconfig b/xcconfig/Release.xcconfig new file mode 100644 index 0000000..dffffb5 --- /dev/null +++ b/xcconfig/Release.xcconfig @@ -0,0 +1,18 @@ +// Platform agnostic release settings + +#include "Base.xcconfig" + +// Optimization +GCC_OPTIMIZATION_LEVEL = s + +// Debugging +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +ENABLE_TESTABILITY = YES +COPY_PHASE_STRIP = YES +ENABLE_NS_ASSERTIONS = NO +MTL_ENABLE_DEBUG_INFO = NO + +GCC_PREPROCESSOR_DEFINITIONS = RELEASE=1 + +SWIFT_OPTIMIZATION_LEVEL = -O +SWIFT_COMPILATION_MODE = wholemodule diff --git a/xcconfig/StaticLib.xcconfig b/xcconfig/StaticLib.xcconfig new file mode 100644 index 0000000..93aaf35 --- /dev/null +++ b/xcconfig/StaticLib.xcconfig @@ -0,0 +1,9 @@ +// Static Library for any platform + +EXECUTABLE_PREFIX = lib + +GCC_DYNAMIC_NO_PIC = NO + +DEFINES_MODULE = YES + +SKIP_INSTALL = YES diff --git a/xcconfig/UIKitBase.xcconfig b/xcconfig/UIKitBase.xcconfig new file mode 100644 index 0000000..ae23807 --- /dev/null +++ b/xcconfig/UIKitBase.xcconfig @@ -0,0 +1,6 @@ +// Base config for Phone projects + +#include "Base.xcconfig" + +IPHONEOS_DEPLOYMENT_TARGET = 14.0 +SDKROOT = iphoneos From 2e9f159fd5444beaa611a3c4a805c1463672fe34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Tue, 26 Apr 2022 18:22:42 +0200 Subject: [PATCH 2/4] No tests yet, sorry ... --- .github/workflows/swift.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 723aa3b..1d91170 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -20,5 +20,3 @@ jobs: run: swift build -c debug - name: Build Swift Release Package run: swift build -c release - - name: Run Tests - run: swift test From 1d800327707949cc0714037f25e20454ebfb4346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Tue, 26 Apr 2022 18:31:30 +0200 Subject: [PATCH 3/4] Push the navigationTitle of the root VC Was still doing this explicitly. --- .../ContainerViewControllers/NavigationController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ViewController/ContainerViewControllers/NavigationController.swift b/Sources/ViewController/ContainerViewControllers/NavigationController.swift index bd7117c..30a5c11 100644 --- a/Sources/ViewController/ContainerViewControllers/NavigationController.swift +++ b/Sources/ViewController/ContainerViewControllers/NavigationController.swift @@ -171,6 +171,7 @@ open class NavigationController: ViewController, _NavigationController NavigationView { viewController._rootViewController.contentView .controlled(by: viewController._rootViewController) + .navigationTitle(viewController._rootViewController.navigationTitle) } } } From f656c49f18a31e6b893fcced2c748cfa47717633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Tue, 26 Apr 2022 18:35:37 +0200 Subject: [PATCH 4/4] Add a very basic explanation To get started. --- README.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/README.md b/README.md index 788032d..08411c7 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,177 @@ ViewController's for SwiftUI. WIP. + +The core idea is that the `ViewController` is owning, or at least driving, +the View(s). Not the other way around. + + +## How to Use + +More details will be posted but to get started. + +### Step A: Setup Project and Root VC + +- create a SwiftUI project in Xcode (iOS is tested better) +- add the `ViewController` package, + e.g. via `git@github.com:ZeeZide/ViewController.git` +- create a new RootViewController, e.g. `HomePage.swift`: + ```swift + import ViewController + + class HomePage: ViewController { + + struct ContentView: View { + + var body: some View { + VStack { + Text("Welcome to MWC!") + .font(.title) + .padding() + + Spacer() + } + } + } + } + ``` +- Instantiate that in the scene view, the `ContentView.swift` + generated by Xcode: + ```swift + import ViewController + + struct ContentView: View { + var body: some View { + MainViewController(HomePage()) + } + } + ``` +- Compile and Run, should show the HomePage + +### Step B: Add a presented VC and navigate to it + +- create a new ViewController, e.g. `Settings.swift`: + ```swift + import ViewController + + class Settings: ViewController { + + struct ContentView: View { + + var body: some View { + VStack { + Text("Welcome to Settings!") + .font(.title) + .padding() + + Spacer() + } + } + } + } + ``` +- Add an action to present the `Settings` from the `HomePage`: + ```swift + import ViewController + + class HomePage: ViewController { + + func configureApp() { + show(Settings()) // or `present(Settings())` + } + + struct ContentView: View { + + @EnvironmentObject private var viewController : HomePage + + var body: some View { + VStack { + Text("Welcome to MWC!") + .font(.title) + .padding() + + Divider() + + Button(action: viewController.configureApp) { + Label("Configure", systemImage: "gear") + } + + Spacer() + } + } + } + } + ``` + +Pressing the button should show the settings in a sheet. + + +### Step C: Add a NavigationController for Navigation :-) + +- Wrap the HomePage in a `NavigationController`, in the scene view: + ```swift + import ViewController + + struct ContentView: View { + var body: some View { + MainViewController(NavigationController(rootViewController: HomePage())) + } + } + ``` + +Note pressing the button does a navigation. Things like this should also +work: +```swift +func presentInSheet() { + let vc = SettingsPage() + vc.modalPresentationStyle = .sheet + present(vc) +} +``` + + +### Adding a `PushLink` + +The presentations so far make use of a hidden link. To explicitly +inline a `NavigationLink`, use `PushLink`, which wraps that. + +- Add a `PushLink` (until I get an `NavigationLink` init extension working) + to present the `Settings` from the `HomePage`: + ```swift + import ViewController + + class HomePage: ViewController { + + struct ContentView: View { + + var body: some View { + VStack { + Text("Welcome to MWC!") + .font(.title) + .padding() + + Divider() + + PushLink("Open Settings", to: Settings()) + + Spacer() + } + } + } + } + ``` + + +### Who + +ViewController is brought to you by [ZeeZide](https://zeezide.de). +We like feedback, GitHub stars, cool contract work, +presumably any form of praise you can think of. + +**Want to support my work**? +Buy an app: +[Past for iChat](https://apps.apple.com/us/app/past-for-ichat/id1554897185), +[SVG Shaper](https://apps.apple.com/us/app/svg-shaper-for-swiftui/id1566140414), +[Shrugs](https://shrugs.app/), +[HMScriptEditor](https://apps.apple.com/us/app/hmscripteditor/id1483239744). +You don't have to use it! 😀