├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ViewController │ ├── AnyViewController.swift │ ├── ContainerViewControllers │ └── NavigationController.swift │ ├── Debugging │ ├── DebugMode.swift │ ├── DebugOverlay.swift │ ├── HierarchyView.swift │ ├── TypeMismatchInfoView.swift │ └── ViewControllerInfo.swift │ ├── Logger.swift │ ├── MainViewController.swift │ ├── NavigationLink │ └── PushLink.swift │ ├── Presentations │ ├── AutoPresentation.swift │ ├── Presentation.swift │ ├── PresentationMode.swift │ └── ViewControllerPresentation.swift │ ├── ReExports.swift │ ├── RenderContentView.swift │ ├── ViewController.swift │ ├── ViewController │ ├── Containment.swift │ ├── ContentView.swift │ ├── DefaultDescription.swift │ ├── RepresentedObject.swift │ ├── Subscriptions.swift │ ├── Title.swift │ ├── TypeErasure.swift │ ├── ViewControllerStorage.swift │ └── _ViewController.swift │ ├── ViewControllerEnvironment.swift │ └── ViewControllerView.swift ├── ViewController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ViewController-iOS.xcscheme │ └── ViewController-macOS.xcscheme └── xcconfig ├── AppKitBase.xcconfig ├── Base.xcconfig ├── Debug.xcconfig ├── MacStaticLib.xcconfig ├── MobileStaticLib.xcconfig ├── Release.xcconfig ├── StaticLib.xcconfig └── UIKitBase.xcconfig /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 9 * * 1" 8 | 9 | jobs: 10 | SwiftPackage: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Select latest available Xcode 14 | uses: maxim-lobanov/setup-xcode@v1.2.1 15 | with: 16 | xcode-version: 13 17 | - name: Checkout Repository 18 | uses: actions/checkout@v2 19 | - name: Build Swift Debug Package 20 | run: swift build -c debug 21 | - name: Build Swift Release Package 22 | run: swift build -c release 23 | iOS: 24 | runs-on: macos-latest 25 | steps: 26 | - name: Select latest available Xcode 27 | uses: maxim-lobanov/setup-xcode@v1.2.1 28 | with: 29 | xcode-version: 13.2 30 | - name: Checkout Repository 31 | uses: actions/checkout@v2 32 | - name: Prerequisites 33 | run: gem install xcpretty 34 | - name: Build 35 | run: set -o pipefail; xcodebuild -scheme ViewController-iOS build | xcpretty --color 36 | NeXTstep: 37 | runs-on: macos-latest 38 | steps: 39 | - name: Select latest available Xcode 40 | uses: maxim-lobanov/setup-xcode@v1.2.1 41 | with: 42 | xcode-version: 13.2 43 | - name: Checkout Repository 44 | uses: actions/checkout@v2 45 | - name: Prerequisites 46 | run: gem install xcpretty 47 | - name: Build 48 | run: set -o pipefail; xcodebuild -scheme ViewController-macOS build | xcpretty --color 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ViewController", 7 | platforms: [ .macOS(.v11), .iOS(.v15) ], 8 | products: [ .library(name: "ViewController", targets: [ "ViewController" ]) ], 9 | targets: [ 10 | .target(name: "ViewController") 11 | ] 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ViewController](https://user-images.githubusercontent.com/7712892/165515717-acc14f68-f464-48f7-b595-638f385c659f.svg) 2 | 3 | ViewController's for SwiftUI. 4 | 5 | The core idea is that the `ViewController` is owning, or at least driving, 6 | the View(s). Not the other way around. 7 | 8 | Blog entry explaining all the things: 9 | [Model View Controller for SwiftUI](http://www.alwaysrightinstitute.com/viewcontroller/) 10 | 11 | ## Quick: How to Use 12 | 13 | Just the basics to get started quickly. 14 | 15 | ### Step A: Setup Project and Root VC 16 | 17 | - create a SwiftUI project in Xcode (iOS is tested better) 18 | - add the `ViewController` package, 19 | e.g. via `git@github.com:ZeeZide/ViewController.git` 20 | - create a new RootViewController, e.g. `HomePage.swift`: 21 | ```swift 22 | import ViewController 23 | 24 | class HomePage: ViewController { 25 | 26 | var view: some View { 27 | VStack { 28 | Text("Welcome to MWC!") 29 | .font(.title) 30 | .padding() 31 | 32 | Spacer() 33 | } 34 | } 35 | } 36 | ``` 37 | - Instantiate that in the scene view, the `ContentView.swift` 38 | generated by Xcode: 39 | ```swift 40 | import ViewController 41 | 42 | struct ContentView: View { 43 | var body: some View { 44 | MainViewController(HomePage()) 45 | } 46 | } 47 | ``` 48 | - Compile and Run, should show the HomePage 49 | 50 | ### Step B: Add a presented VC and navigate to it 51 | 52 | - create a new ViewController, e.g. `Settings.swift`: 53 | ```swift 54 | import ViewController 55 | 56 | class Settings: ViewController { 57 | 58 | var view: some View { // the View being controlled 59 | VStack { 60 | Text("Welcome to Settings!") 61 | .font(.title) 62 | .padding() 63 | 64 | Spacer() 65 | } 66 | } 67 | } 68 | ``` 69 | - Add an action to present the `Settings` from the `HomePage`: 70 | ```swift 71 | import ViewController 72 | 73 | class HomePage: ViewController { 74 | 75 | func configureApp() { 76 | show(Settings()) // or `present(Settings())` 77 | } 78 | 79 | var view: some View { 80 | VStack { 81 | Text("Welcome to MWC!") 82 | .font(.title) 83 | .padding() 84 | 85 | Divider() 86 | 87 | Button(action: self.configureApp) { 88 | Label("Configure", systemImage: "gear") 89 | } 90 | 91 | Spacer() 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | Pressing the button should show the settings in a sheet. 98 | 99 | 100 | ### Step C: Add a NavigationController for Navigation :-) 101 | 102 | - Wrap the HomePage in a `NavigationController`, in the scene view: 103 | ```swift 104 | import ViewController 105 | 106 | struct ContentView: View { 107 | var body: some View { 108 | MainViewController(NavigationController(rootViewController: HomePage())) 109 | } 110 | } 111 | ``` 112 | 113 | Note pressing the button does a navigation. Things like this should also 114 | work: 115 | ```swift 116 | func presentInSheet() { 117 | let vc = SettingsPage() 118 | vc.modalPresentationStyle = .sheet 119 | present(vc) 120 | } 121 | ``` 122 | 123 | 124 | ### Adding a `PushLink` 125 | 126 | The presentations so far make use of a hidden link. To explicitly 127 | inline a `NavigationLink`, use `PushLink`, which wraps that. 128 | 129 | - Add a `PushLink` (until I get an `NavigationLink` init extension working) 130 | to present the `Settings` from the `HomePage`: 131 | ```swift 132 | import ViewController 133 | 134 | class HomePage: ViewController { 135 | 136 | var view: some View { 137 | VStack { 138 | Text("Welcome to MWC!") 139 | .font(.title) 140 | .padding() 141 | 142 | Divider() 143 | 144 | PushLink("Open Settings", to: Settings()) 145 | 146 | Spacer() 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 153 | ### Who 154 | 155 | ViewController is brought to you by [ZeeZide](https://zeezide.de). 156 | We like feedback, GitHub stars, cool contract work, 157 | presumably any form of praise you can think of. 158 | 159 | **Want to support my work**? 160 | Buy an app: 161 | [Past for iChat](https://apps.apple.com/us/app/past-for-ichat/id1554897185), 162 | [SVG Shaper](https://apps.apple.com/us/app/svg-shaper-for-swiftui/id1566140414), 163 | [Shrugs](https://shrugs.app/), 164 | [HMScriptEditor](https://apps.apple.com/us/app/hmscripteditor/id1483239744). 165 | You don't have to use it! 😀 166 | -------------------------------------------------------------------------------- /Sources/ViewController/AnyViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewController.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import SwiftUI 11 | 12 | /** 13 | * A type erased version of a (statically typed) ``ViewController``. 14 | * 15 | * If possible type erasure should be avoided. 16 | * 17 | * When a ``ViewController`` is pushed into the environment, it is pushed 18 | * as an `@EnvironmentObject` of its concrete type, but also as an 19 | * ``AnyViewController``. This allows access of all common methods 20 | * (e.g. ``ViewController/dismiss``). 21 | * 22 | * Example access: 23 | * ```swift 24 | * struct TitleLabel: View { 25 | * 26 | * @EnvironmentObject private var viewController : AnyViewController 27 | * 28 | * var body: some View { 29 | * Text(verbatim: viewController.title) 30 | * .font(.title) 31 | * } 32 | * } 33 | * ``` 34 | */ 35 | public final class AnyViewController: ViewController { 36 | 37 | public var id : ObjectIdentifier { ObjectIdentifier(viewController) } 38 | 39 | public let viewController : _ViewController 40 | private var subscription : AnyCancellable? 41 | 42 | @usableFromInline 43 | internal let contentView : () -> AnyView 44 | 45 | public init(_ viewController: VC) where VC: ViewController { 46 | assert(!(viewController is AnyViewController), 47 | "Attempt to nest an AnyVC into another \(viewController)") 48 | 49 | self.viewController = viewController 50 | self.contentView = { AnyView(viewController.view) } 51 | 52 | subscription = viewController.objectWillChange.sink { [weak self] _ in 53 | self?.objectWillChange.send() 54 | } 55 | } 56 | 57 | /** 58 | * An initializer that avoids nesting `AnyViewController`s into themselves. 59 | */ 60 | init(_ viewController: AnyViewController) { 61 | self.viewController = viewController.viewController 62 | self.contentView = viewController.contentView 63 | 64 | // TBD: Can't unwrap this? 65 | subscription = viewController.objectWillChange.sink { 66 | [weak self] _ in 67 | self?.objectWillChange.send() 68 | } 69 | } 70 | 71 | 72 | // MARK: - All the any 73 | // Those are typed erased by the base protocol already (_ViewController). 74 | 75 | @inlinable 76 | @ViewBuilder public var view : AnyView { contentView() } 77 | 78 | @inlinable 79 | public var controlledContentView : AnyView { anyControlledContentView } 80 | @inlinable 81 | public var anyControlledContentView : AnyView { 82 | viewController.anyControlledContentView 83 | } 84 | 85 | 86 | // MARK: - Titles 87 | 88 | @inlinable 89 | public var title : String? { 90 | set { viewController.title = newValue } 91 | get { viewController.title } 92 | } 93 | 94 | @inlinable 95 | public var navigationTitle : String { viewController.navigationTitle } 96 | 97 | 98 | // MARK: - Represented Object 99 | 100 | @inlinable 101 | public var representedObject : Any? { 102 | set { anyRepresentedObject = newValue } 103 | get { anyRepresentedObject } 104 | } 105 | @inlinable 106 | public var anyRepresentedObject : Any? { 107 | set { viewController.anyRepresentedObject = newValue } 108 | get { viewController.anyRepresentedObject } 109 | } 110 | 111 | 112 | // MARK: - Presentation 113 | 114 | @inlinable 115 | public var presentedViewController : _ViewController? { 116 | get { viewController.presentedViewController } 117 | } 118 | @inlinable 119 | public var activePresentations : [ ViewControllerPresentation ] { 120 | set { viewController.activePresentations = newValue } 121 | get { viewController.activePresentations } 122 | } 123 | @inlinable 124 | public var presentingViewController : _ViewController? { 125 | set { viewController.presentingViewController = newValue } 126 | get { viewController.presentingViewController } 127 | } 128 | 129 | @inlinable 130 | public func willAppear() { viewController.willAppear() } 131 | @inlinable 132 | public func willDisappear() { viewController.willDisappear() } 133 | 134 | @inlinable 135 | public func present(_ viewController: VC, 136 | mode: ViewControllerPresentationMode) 137 | where VC: ViewController 138 | { 139 | self.viewController.present(viewController, mode: mode) 140 | } 141 | @inlinable 142 | public func present(_ viewController: VC) { 143 | self.viewController.present(viewController) 144 | } 145 | @inlinable 146 | public func dismiss() { viewController.dismiss() } // TBD: really unwrap? 147 | 148 | 149 | @inlinable 150 | public func show(_ viewController: VC) { 151 | self.viewController.show(viewController) 152 | } 153 | 154 | @inlinable 155 | public func showDetail(_ viewController: VC){ 156 | self.viewController.showDetail(viewController) 157 | } 158 | 159 | @inlinable 160 | public func show(_ viewController: VC, in owner: OwnerVC) 161 | where VC: ViewController, OwnerVC: _ViewController 162 | { 163 | self.viewController.show(viewController, in: owner) 164 | } 165 | @inlinable 166 | public func showDetail(_ viewController: VC, in owner: OwnerVC) 167 | where VC: ViewController, OwnerVC: _ViewController 168 | { 169 | self.viewController.showDetail(viewController, in: owner) 170 | } 171 | 172 | 173 | // MARK: - Hierarchy 174 | 175 | @inlinable 176 | public var children : [ _ViewController ] { 177 | set { viewController.children = newValue } 178 | get { viewController.children } 179 | } 180 | 181 | @inlinable 182 | public var parent : _ViewController? { 183 | set { viewController.parent = newValue } 184 | get { viewController.parent } 185 | } 186 | 187 | @inlinable 188 | public func willMove(toParent parent: _ViewController?) { 189 | viewController.willMove(toParent: parent) 190 | } 191 | 192 | @inlinable 193 | public func didMove(toParent parent: _ViewController?) { 194 | viewController.didMove(toParent: parent) 195 | } 196 | 197 | @inlinable 198 | public func addChild(_ viewController: VC) { 199 | self.viewController.addChild(viewController) 200 | } 201 | @inlinable 202 | public func removeFromParent() { 203 | viewController.removeFromParent() // TBD: really unwrap? 204 | } 205 | 206 | 207 | // MARK: - Better Description 208 | 209 | @inlinable 210 | public var description: String { "" } 211 | @inlinable 212 | public func appendAttributes(to description: inout String) {} 213 | } 214 | -------------------------------------------------------------------------------- /Sources/ViewController/ContainerViewControllers/NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationController.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * Type erased version of the ``NavigationController``. Check that for more 13 | * information. 14 | */ 15 | public protocol _NavigationController: _ViewController { 16 | 17 | var rootViewController : _ViewController { get } 18 | 19 | } 20 | 21 | public extension _NavigationController { 22 | // This is the actual implementation the local VC's `show` hooks into. 23 | 24 | @inlinable 25 | func show(_ viewController: VC, in owner: OwnerVC) 26 | where VC: ViewController, OwnerVC: _ViewController 27 | { 28 | owner.present(viewController, mode: .navigation) 29 | } 30 | @inlinable 31 | func showDetail(_ viewController: VC, in owner: OwnerVC) 32 | where VC: ViewController, OwnerVC: _ViewController 33 | { 34 | show(viewController, in: owner) 35 | } 36 | } 37 | 38 | internal extension _NavigationController { 39 | 40 | func forEach(yield: ( _ViewController ) -> Bool) { 41 | guard yield(rootViewController) else { return } 42 | 43 | var presentation = rootViewController.activePresentation(for: .navigation) 44 | while let activePresentation = presentation { 45 | // Nested NavigationController. That will own the subsequent 46 | // presentations. 47 | if activePresentation.viewController is _NavigationController { 48 | break 49 | } 50 | guard activePresentation.mode == .navigation || 51 | activePresentation.mode == .pushLink else { break } 52 | 53 | guard yield(activePresentation.viewController) else { break } 54 | presentation = 55 | activePresentation.viewController.activePresentation(for: .navigation) 56 | } 57 | } 58 | func forEach(yield : ( _ViewController ) -> Void) { 59 | forEach { _ in true } 60 | } 61 | } 62 | 63 | public extension _NavigationController { 64 | 65 | // MARK: - Accessors 66 | 67 | /** 68 | * The ``ViewController`` on top of the navigation stack, i.e. the "root". 69 | */ 70 | @inlinable 71 | var topViewController: _ViewController { rootViewController } 72 | 73 | /** 74 | * The ``ViewController``s currently on the navigation stack of this 75 | * particular ``navigationController``. 76 | * 77 | * Careful: The ``NavigationController`` doesn't emit a change notification 78 | * if any child view controllers change. (TBD) 79 | */ 80 | var viewControllers : [ _ViewController ] { 81 | // Note: No setter until programmatic navigation actually works in 82 | // SwiftUI ... 83 | 84 | get { 85 | var children = [ _ViewController ]() 86 | forEach { children.append($0) } 87 | return children 88 | } 89 | } 90 | 91 | /** 92 | * The ``ViewController``s a the bottom of the stack of the particular 93 | * ``navigationController``. 94 | * 95 | * Careful: The ``NavigationController`` doesn't emit a change notification 96 | * if any child view controllers change. (TBD) 97 | */ 98 | var visibleViewController : _ViewController { 99 | var cursor : _ViewController = rootViewController 100 | forEach { cursor = $0 } 101 | return cursor 102 | } 103 | } 104 | 105 | 106 | /** 107 | * A simple wrapper around SwiftUI's `NavigationView`. 108 | * 109 | * The primary purpose of this class is to tell the ``ViewController`` stack, 110 | * that a `NavigationView` is in place. So that `show` methods automatically 111 | * present in the `NavigationView` (instead of showing a sheet, etc). 112 | * 113 | * Example: 114 | * ```swift 115 | * struct ContentView: View { // the "scene view" 116 | * 117 | * var body: some View { 118 | * MainViewController(NavigationController(rootViewController: HomePage())) 119 | * } 120 | * } 121 | * ``` 122 | * 123 | * Note that this works quite differently to a `UINavigationController`. 124 | * I.e. the controller does not really "own" the activation stack. Rather, the 125 | * ``ViewController``'s themselves define the activation trail. 126 | * 127 | * 2022-04-25: Note that programmatic navigation in SwiftUI is still a mess, 128 | * so you can't reliably "deeplink". 129 | * I.e. no `popToRootViewController` 130 | */ 131 | open class NavigationController: ViewController, _NavigationController 132 | where RootVC: ViewController 133 | { 134 | 135 | // TODO: "show" instead of present 136 | 137 | public let _rootViewController : RootVC 138 | 139 | public var rootViewController : _ViewController { _rootViewController } 140 | 141 | public enum NavigationViewStyle: Equatable { 142 | case automatic 143 | 144 | @available(iOS 13.0, tvOS 13.0, watchOS 7.0, *) 145 | @available(macOS, unavailable) 146 | case stack 147 | 148 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 149 | case columns 150 | } 151 | @Published public var navigationViewStyle = NavigationViewStyle.automatic 152 | 153 | public init(rootViewController: RootVC) { 154 | self._rootViewController = rootViewController 155 | markAsPresentingViewController() 156 | } 157 | 158 | private func markAsPresentingViewController() { 159 | rootViewController.presentingViewController = self 160 | activePresentations.append(ViewControllerPresentation( 161 | viewController: _rootViewController, 162 | mode: .custom // not .navigation, that would activate the bg link! 163 | )) 164 | } 165 | 166 | // MARK: - Description 167 | 168 | public func appendAttributes(to description: inout String) { 169 | defaultAppendAttributes(to: &description) 170 | description += " \(rootViewController)" 171 | } 172 | 173 | 174 | // MARK: - View 175 | 176 | private var _view: some View { 177 | NavigationView { 178 | _rootViewController.view 179 | .controlled(by: _rootViewController) 180 | .navigationTitle(_rootViewController.navigationTitle) 181 | } 182 | } 183 | public var view: some View { 184 | switch navigationViewStyle { 185 | case .automatic : 186 | _view 187 | case .stack : 188 | #if os(macOS) 189 | _view 190 | #else 191 | _view.navigationViewStyle(.stack) 192 | #endif 193 | case .columns : 194 | if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { 195 | _view.navigationViewStyle(.columns) 196 | } 197 | else { 198 | _view 199 | } 200 | } 201 | } 202 | } 203 | 204 | public extension AnyViewController { 205 | 206 | @inlinable // Note: not a protocol requirement, i.e. dynamic! 207 | var navigationController : _NavigationController? { 208 | viewController.navigationController 209 | } 210 | } 211 | 212 | public extension _ViewController { 213 | 214 | /** 215 | * Return the ``NavigationController`` presenting this controller. 216 | * 217 | * Note: If the controller is a ``NavigationController`` itself, this does NOT 218 | * return self. It still looks for the closest presenting controller. 219 | */ 220 | var navigationController : _NavigationController? { 221 | /// Is this VC itself being presented? 222 | if let presentingVC = presentingViewController { // yes 223 | if let nvc = presentingVC as? _NavigationController { return nvc } 224 | return presentingVC.navigationController 225 | } 226 | return parent?.navigationController 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/ViewController/Debugging/DebugMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugMode.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum ViewControllerDebugMode: Equatable { 12 | case none 13 | case overlay 14 | } 15 | 16 | public extension EnvironmentValues { 17 | 18 | @usableFromInline 19 | internal struct DebugModeKey: EnvironmentKey { 20 | #if DEBUG 21 | @usableFromInline 22 | static let defaultValue = ViewControllerDebugMode.overlay 23 | #else 24 | @usableFromInline 25 | static let defaultValue = ViewControllerDebugMode.none 26 | #endif 27 | } 28 | 29 | @inlinable 30 | var viewControllerDebugMode : ViewControllerDebugMode { 31 | set { self[DebugModeKey.self] = newValue } 32 | get { self[DebugModeKey.self] } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ViewController/Debugging/DebugOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugOverlay.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | #if DEBUG 12 | 13 | @usableFromInline 14 | struct DebugOverlayModifier: ViewModifier { 15 | 16 | @Environment(\.viewControllerDebugMode) private var mode 17 | @State private var showingOverlay = false 18 | 19 | 20 | /// Show information about the VC active in the environment 21 | struct InfoPanel: View { // TBD: Make it a VC? :-) 22 | 23 | @Binding var isShowing : Bool 24 | @EnvironmentObject private var viewController : AnyViewController 25 | 26 | private func dismiss() { isShowing = false } 27 | 28 | #if os(macOS) 29 | var body: some View { 30 | NavigationView { 31 | ViewControllerInfo( 32 | watchChanges: viewController, 33 | viewController: viewController 34 | ) 35 | .navigationTitle(viewController.navigationTitle) 36 | .toolbar { 37 | Button(action: dismiss) { 38 | Label("Close", systemImage: "xmark.circle") 39 | } 40 | } 41 | } 42 | } 43 | #else 44 | var body: some View { 45 | NavigationView { 46 | ViewControllerInfo( 47 | watchChanges: viewController, 48 | viewController: viewController 49 | ) 50 | .navigationTitle(viewController.navigationTitle) 51 | .navigationBarTitleDisplayMode(.inline) 52 | .toolbar { 53 | Button(action: dismiss) { 54 | Label("Close", systemImage: "xmark.circle") 55 | } 56 | } 57 | } 58 | } 59 | #endif 60 | } 61 | 62 | private func showOverlay() { 63 | showingOverlay = true 64 | } 65 | 66 | @usableFromInline 67 | func body(content: Content) -> some View { 68 | content 69 | .overlay( 70 | Button(action: showOverlay) { 71 | Image(systemName: "ladybug") 72 | .accessibilityLabel("Show Debug Overlay") 73 | .foregroundColor(.red) 74 | } 75 | .labelsHidden() 76 | .sheet(isPresented: $showingOverlay) { 77 | InfoPanel(isShowing: $showingOverlay) 78 | } 79 | .opacity(mode == .overlay ? 1.0 : 0.0) 80 | .padding(), alignment: .bottomTrailing 81 | ) 82 | } 83 | } 84 | 85 | public extension View { 86 | 87 | func viewControllerDebugOverlay() -> some View { 88 | self.modifier(DebugOverlayModifier()) 89 | } 90 | } 91 | 92 | #else // non-DEBUG fallback 93 | 94 | public extension View { 95 | 96 | @inlinable 97 | func viewControllerDebugOverlay() -> some View { 98 | self 99 | } 100 | } 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /Sources/ViewController/Debugging/HierarchyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HierarchyView.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | #if DEBUG 12 | struct HierarchyView: View { 13 | 14 | let title : String 15 | let controllers : [ _ViewController ] 16 | let active : _ViewController 17 | 18 | private func titleForController(_ vc: _ViewController) -> String { 19 | "\(vc.typeName)[\(vc.oidString)]" 20 | } 21 | 22 | var body: some View { 23 | if controllers.count > 1 { 24 | Divider() 25 | VStack(alignment: .leading, spacing: 16) { 26 | Text(title) 27 | .font(.title2) 28 | 29 | VStack(alignment: .leading, spacing: 12) { 30 | // Don't do this at home 31 | ForEach(Array(zip(controllers.indices, controllers)), id: \.0) { 32 | ( idx, vc ) in 33 | HStack(alignment: .firstTextBaseline) { 34 | Text("\(idx)") 35 | Text(verbatim: titleForController(vc)) 36 | } 37 | .overlay( 38 | RoundedRectangle(cornerRadius: 10) 39 | .strokeBorder() 40 | .padding(-8) 41 | .opacity(vc === active ? 1.0 : 0.0) 42 | ) 43 | } 44 | } 45 | .padding(.horizontal) 46 | } 47 | .padding() 48 | } 49 | } 50 | } 51 | #endif // DEBUG 52 | -------------------------------------------------------------------------------- /Sources/ViewController/Debugging/TypeMismatchInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeMismatchInfoView.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @usableFromInline 12 | struct TypeMismatchInfoView: View 13 | where DestinationVC: ViewController, ParentVC: ViewController 14 | { 15 | 16 | @ObservedObject private var parent : ParentVC 17 | private let expectedMode : ViewControllerPresentationMode 18 | 19 | @usableFromInline 20 | init(parent: ParentVC, expectedMode: ViewControllerPresentationMode) { 21 | self.parent = parent 22 | self.expectedMode = expectedMode 23 | } 24 | 25 | @usableFromInline 26 | var body: some View { 27 | VStack { 28 | Label("Type Mismatch", 29 | systemImage: "exclamationmark.triangle") 30 | .font(.title) 31 | .padding() 32 | .foregroundColor(.red) 33 | 34 | HStack(alignment: .firstTextBaseline) { 35 | Text("Parent:") 36 | Spacer() 37 | Text(verbatim: parent.description) 38 | } 39 | .padding() 40 | 41 | if parent.activePresentations.isEmpty { 42 | Text("No presentation active?") 43 | .padding() 44 | .foregroundColor(.red) 45 | .font(.body.bold()) 46 | } 47 | else { 48 | ForEach(Array(zip(parent.activePresentations.indices, 49 | parent.activePresentations)), id: \.0) 50 | { _, presentation in 51 | HStack(alignment: .firstTextBaseline) { 52 | Text("Presented:") 53 | Spacer() 54 | Text(verbatim: presentation.viewController.description) 55 | } 56 | .padding() 57 | HStack(alignment: .firstTextBaseline) { 58 | Text("Mode:") 59 | Spacer() 60 | Text(verbatim: "\(presentation.mode)") 61 | } 62 | } 63 | .padding() 64 | } 65 | 66 | Spacer() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ViewController/Debugging/ViewControllerInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerInfo.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | #if DEBUG 10 | import SwiftUI 11 | 12 | struct ViewControllerInfo: View { 13 | 14 | @ObservedObject var watchChanges : AnyViewController 15 | let viewController : _ViewController 16 | 17 | struct TitledField: View { 18 | let label : String 19 | let value : V 20 | 21 | var body: some View { 22 | VStack(alignment: .leading, spacing: 8) { 23 | Text(verbatim: "\(value)") 24 | .overlay( 25 | RoundedRectangle(cornerRadius: 8) 26 | .strokeBorder() 27 | .foregroundColor(.secondary) 28 | .padding(.vertical, -4) 29 | .padding(.horizontal, -8) 30 | ) 31 | Text(label) 32 | .font(.footnote) 33 | .foregroundColor(.secondary) 34 | } 35 | } 36 | } 37 | 38 | private var parentHierarchy : [ _ViewController ] { 39 | guard let parent = viewController.parent else { return [] } 40 | return Array(sequence(first: parent) { $0.parent }) 41 | } 42 | 43 | private var presentationHierarchy : [ _ViewController ] { 44 | let toRoot = sequence(first: viewController) { 45 | $0.presentingViewController 46 | } 47 | .reversed() 48 | if let presented = viewController.presentedViewController { 49 | let downwards = sequence(first: presented) { 50 | $0.presentedViewController 51 | } 52 | return Array(toRoot) + Array(downwards) 53 | } 54 | else { 55 | return Array(toRoot) 56 | } 57 | } 58 | 59 | 60 | private var addressString : String { 61 | let oid = UInt(bitPattern: ObjectIdentifier(viewController)) 62 | return "0x" + String(oid, radix: 16) 63 | } 64 | 65 | private var title : String { 66 | if viewController is AnyViewController { 67 | return "AnyVC: " + addressString 68 | } 69 | else { 70 | return viewController.typeName 71 | } 72 | } 73 | 74 | var body: some View { 75 | if let avc = viewController as? AnyViewController { 76 | VStack(alignment: .leading) { 77 | Label(title, systemImage: "envelope") 78 | .padding() 79 | 80 | Divider() 81 | 82 | ViewControllerInfo(watchChanges: avc, 83 | viewController: avc.viewController) 84 | } 85 | } 86 | else { 87 | VStack(alignment: .leading) { 88 | Label(title, systemImage: "ladybug") 89 | .padding() 90 | 91 | Divider() 92 | 93 | ScrollView { 94 | VStack(alignment: .leading, spacing: 20) { 95 | TitledField(label: "Description", value: viewController) 96 | 97 | if let vc = viewController.parent { 98 | TitledField(label: "Parent", value: vc) // TBD: recurse? 99 | } 100 | 101 | if let vc = viewController.presentedViewController { 102 | TitledField(label: "Presenting other", value: vc) 103 | } 104 | 105 | if let vc = viewController.presentingViewController { 106 | TitledField(label: "Presented by", value: vc) 107 | } 108 | else { 109 | if viewController.parent == nil { Text("Root") } 110 | else { Text("Contained") } 111 | } 112 | } 113 | .padding() 114 | 115 | HierarchyView(title : "Parent Hierarchy", 116 | controllers : parentHierarchy, 117 | active : viewController) 118 | HierarchyView(title : "Presentation Hierarchy", 119 | controllers : presentationHierarchy, 120 | active : viewController) 121 | 122 | Spacer() 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | #endif // DEBUG 130 | -------------------------------------------------------------------------------- /Sources/ViewController/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import os 10 | import class Foundation.ProcessInfo 11 | 12 | // Print Log Helper, since we apparently can't set the log level of os_log 🤦‍♀️ 13 | 14 | #if DEBUG && true 15 | 16 | @usableFromInline 17 | struct PrintLogger { 18 | 19 | static let logLevel : OSLogType = { 20 | let env = ProcessInfo.processInfo.environment 21 | switch (env["VIEWCONTROLLER_LOGLEVEL"] ?? env["LOGLEVEL"])?.lowercased() { 22 | case "error" : return .error 23 | case "debug" : return .debug 24 | case "fault" : return .fault 25 | case "info" : return .info 26 | default: return OSLogType.default 27 | } 28 | }() 29 | var logLevel : OSLogType { Self.logLevel } 30 | 31 | func log(_ level: OSLogType, _ prefix: String, _ message: () -> String) { 32 | guard level.rawValue >= self.logLevel.rawValue else { return } 33 | print(prefix + message()) 34 | } 35 | 36 | @usableFromInline 37 | func debug(_ message: @autoclosure () -> String) { 38 | log(.debug, "", message) 39 | } 40 | @usableFromInline 41 | func warning(_ message: @autoclosure () -> String) { 42 | log(.error, "WARN: ", message) 43 | } 44 | @usableFromInline 45 | func error(_ message: @autoclosure () -> String) { 46 | log(.error, "ERROR: ", message) 47 | } 48 | } 49 | 50 | @usableFromInline 51 | let logger = PrintLogger() 52 | 53 | #else 54 | 55 | @usableFromInline 56 | let logger = Logger( 57 | subsystem : Bundle.main.bundleIdentifier ?? "Main", 58 | category : "VC" 59 | ) 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/ViewController/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * This allocates the state for, and assigns, a scene view controller, 13 | * i.e. one which starts a new VC hierarchy. 14 | * Usually only one root VC is used per scene. 15 | * 16 | * E.g. this could be used in the `ContentView` of an app like this: 17 | * ```swift 18 | * struct ContentView: View { 19 | * 20 | * var body: some View { 21 | * MainViewController(HomePage()) 22 | * } 23 | * } 24 | * ``` 25 | */ 26 | public struct MainViewController: View where VC: ViewController { 27 | 28 | @StateObject private var viewController : VC 29 | 30 | public init(_ viewController: @escaping @autoclosure () -> VC) { 31 | self._viewController = StateObject(wrappedValue: viewController()) 32 | } 33 | 34 | /** 35 | * Helper to avoid using ``presentInNavigation`` with a ``ViewController`` 36 | * that doesn't have a proper ``ContentView``. 37 | */ 38 | @available(*, unavailable, 39 | message: "The ViewController needs a proper `ContentView`") 40 | public init(_ viewController: @escaping @autoclosure () -> VC) 41 | where VC.ContentView == DefaultViewControllerView 42 | { 43 | assertionFailure("Incorrect use of ViewController") 44 | self._viewController = StateObject(wrappedValue: viewController()) 45 | } 46 | 47 | public var body: some View { 48 | viewController 49 | .controlledContentView 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ViewController/NavigationLink/PushLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushLink.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * Push a new view controller and navigate to it within a `NavigationView` 13 | * (using a `NavigationLink`). 14 | * 15 | * Note: Unlike an explicit `Button` calling `present` or `show`, 16 | * a `PushLink` caches the target controller while active. 17 | * I.e. pressing the same `PushLink` twice, will not result in a new 18 | * instantiation. 19 | * 20 | * Content View Example: 21 | * ```swift 22 | * class HomePage: ViewController { 23 | * var view: some View { 24 | * PushLink("Preferences…", to: PreferencesPage()) 25 | * } 26 | * } 27 | * ``` 28 | * 29 | * Explicit View Example: 30 | * ```swift 31 | * class HomePage: ViewController { 32 | * var view: some View { 33 | * PushLink(to: PreferencesPage(), using: Text("Prefs!") { 34 | * Text("Preferences…") 35 | * } 36 | * } 37 | * } 38 | * ``` 39 | * 40 | * A workaround until I found a way to make the `NavigationLink` init extension 41 | * working. 42 | */ 43 | public struct PushLink: View 44 | where VC: ViewController, CV: View, Label: View 45 | { 46 | // TBD: Call this `PushSegue`? 47 | // This works, but not as nice as being able to use NavLink directly. 48 | 49 | @EnvironmentObject private var parentViewController : AnyViewController 50 | @State private var childViewController : _ViewController? 51 | private let childViewControllerFactory : () -> VC 52 | private let contentView : CV 53 | private let label : Label 54 | 55 | private let mode = ViewControllerPresentationMode.pushLink 56 | 57 | 58 | // MARK: - Public Initializers 59 | 60 | /** 61 | * Create a ``PushLink`` that is using an explicit `View` as the destination. 62 | * 63 | * Example: 64 | * ``` 65 | * PushLink(to: SettingsPage(), using: MySettingsView()) { 66 | * Text("Go to settings…") 67 | * } 68 | * ``` 69 | */ 70 | public init(to viewController : @autoclosure @escaping () -> VC, 71 | using contentView : CV, 72 | @ViewBuilder label : () -> Label) 73 | { 74 | self.childViewControllerFactory = viewController 75 | self.contentView = contentView 76 | self.label = label() 77 | } 78 | 79 | 80 | // MARK: - Implementation 81 | 82 | private var isActive : Bool { 83 | // The thing to keep in mind here is that the `childViewController` is NOT 84 | // the truth. The truth is the presentation state in the 85 | // `parentViewController`. 86 | guard let activeVC = childViewController else { 87 | logger.debug("PushLink[isActive]: no CVC is set…") 88 | return false 89 | } 90 | guard let presentation = 91 | parentViewController.activePresentation(for: activeVC) else 92 | { 93 | // OK, this is FINE. It happens because when clicking a different NavLink 94 | // the _new_ position is set to "showing" before the old one is being 95 | // dismissed. 96 | // Note: Do not update state in an accessor! 97 | logger.debug( 98 | "PushLink[isActive]: No presentation: \(activeVC.description), off.") 99 | return false 100 | } 101 | 102 | assert(presentation.viewController === activeVC) 103 | logger.debug("PushLink[isActive]: active: \(activeVC.description)") 104 | return presentation.viewController === activeVC 105 | } 106 | 107 | private func presentIfNecessary() { 108 | if let activeVC = childViewController { // we are already active 109 | logger.debug( 110 | "PushLink[present]: VC is already active: \(activeVC.description)") 111 | assert(isActive) // checks the parent as well 112 | return 113 | } 114 | 115 | // Not yet active 116 | let activeVC = childViewControllerFactory() 117 | logger.debug("PushLink[present]: VC \(activeVC) \(mode)") 118 | childViewController = activeVC 119 | parentViewController.present(activeVC, mode: mode) 120 | } 121 | 122 | private func dismissIfNecessary() { 123 | // The thing to keep in mind here is that the `childViewController` is NOT 124 | // the truth. The truth is the presentation state in the 125 | // `parentViewController`. 126 | guard let activeVC = childViewController else { 127 | logger.debug("PushLink[dismiss]: no VC is active: \(parentViewController)") 128 | return 129 | } 130 | 131 | defer { childViewController = nil } 132 | 133 | // OK, we have the local VC state, but is the VC still active in the parent 134 | // ViewController? 135 | guard let _ = parentViewController.activePresentation(for: activeVC) else { 136 | // OK, this is FINE. It happens because when clicking a different NavLink 137 | // the _new_ position is set to "showing" before the old one is being 138 | // dismissed. 139 | // This is called during a View update. We should not update state in 140 | // here, but only in the follow up, which will call our Binding w/ a 141 | // isActive=false (but only _after_ setting the new item to true) 142 | logger.debug( 143 | "PushLink[dismiss]: No presentation: \(activeVC.description), off.") 144 | 145 | if activeVC.presentingViewController != nil { 146 | logger.error( 147 | "PushLink[dismiss]: \(activeVC.description) wasn't dismissed?") 148 | assert(activeVC.presentingViewController == nil, 149 | "The cache VC should have been dismissed!") 150 | activeVC.dismiss() 151 | } 152 | 153 | return 154 | } 155 | 156 | logger.debug("PushLink[dismiss]: \(activeVC.description)") 157 | activeVC.dismiss() 158 | logger.debug("PushLink[dismiss]: done: \(activeVC.description)") 159 | } 160 | 161 | /** 162 | * Returns a `Binding` that handles presentation and dismiss of an 163 | * associated ``ViewController``. 164 | * 165 | * That is: 166 | * - It returns `true` if the link has an associated ``ViewController`` 167 | * that is being presented. 168 | * - If it is set to `true`: 169 | * - If a ``ViewController`` is already presented by this link, it stays 170 | * presented. 171 | * - If no ``ViewController`` is being presented yet, this will construct 172 | * and present a new associated ``ViewController`` 173 | * - If it is set to `false` (i.e. the user navigated away from the 174 | * destination), the link's ViewController gets dismissed. 175 | * 176 | * Note: When switching between two `NavigationLink`s, SwiftUI can set the 177 | * Binding for the new controller to `true`, before the Binding of the 178 | * old controller was set to `false`. PushLink deals w/ that and 179 | * dismisses old VCs before presenting a new. 180 | * 181 | * - Returns: A Binding that controls whether the PushLink is active. 182 | */ 183 | private var isActiveBinding: Binding { 184 | Binding( 185 | get: { 186 | isActive 187 | }, 188 | set: { isActive in 189 | if isActive { presentIfNecessary() } 190 | else { dismissIfNecessary() } 191 | } 192 | ) 193 | } 194 | 195 | /** 196 | * Returns a View representing the destination of the `NavigationLink`. 197 | * 198 | * Which is usually going to be the `contentView` of the destination 199 | * ``ViewController``, bound to the same. 200 | * This also pushes the ``ViewController/navigationTitle`` to the 201 | * SwiftUI environment. 202 | */ 203 | @ViewBuilder private var destination: some View { 204 | if let activeVC = childViewController { 205 | if let presentedVC = activeVC as? VC { 206 | if let presentation = 207 | parentViewController.activePresentation(for: presentedVC), 208 | presentation.mode == mode 209 | { 210 | contentView 211 | .controlled(by: presentedVC) 212 | .environment(\.viewControllerPresentationMode, .navigation) 213 | .navigationTitle(presentedVC.navigationTitle) 214 | } 215 | else { 216 | SwiftUI.Label("Error: The linked VC is not being presented as a link", 217 | systemImage: "exclamationmark.triangle") 218 | } 219 | } 220 | else { 221 | SwiftUI.Label("Error: The linked VC has an unexpected type!", 222 | systemImage: "exclamationmark.triangle") 223 | } 224 | } 225 | else { 226 | SwiftUI.Label("Linked VC is not yet being presented.", 227 | systemImage: "exclamationmark.triangle") 228 | } 229 | } 230 | 231 | public var body: some View { 232 | NavigationLink( 233 | isActive : isActiveBinding, 234 | destination : { destination }, 235 | label : { label } 236 | ) 237 | } 238 | } 239 | 240 | 241 | // MARK: - Convenience Initializers 242 | 243 | extension PushLink { 244 | 245 | /** 246 | * Create a ``PushLink`` that is using the ``ViewController/view`` 247 | * as the destination. 248 | * 249 | * Example: 250 | * ``` 251 | * PushLink(to: SettingsPage()) { 252 | * Text("Go to settings…") 253 | * } 254 | * ``` 255 | */ 256 | @inlinable 257 | public init(to viewController : @autoclosure @escaping () -> VC, 258 | @ViewBuilder label : () -> Label) 259 | where VC: ViewController, CV == RenderContentView 260 | { 261 | assert(VC.ContentView.self != DefaultViewControllerView.self, 262 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 263 | self.init(to: viewController(), using: RenderContentView()) { label() } 264 | } 265 | 266 | /** 267 | * Create a ``PushLink`` that is using the ``ViewController/view`` 268 | * as the destination. 269 | * 270 | * Example: 271 | * ``` 272 | * PushLink("Go to settings…", to: SettingsPage()) 273 | * ``` 274 | */ 275 | @inlinable 276 | public init(_ title: S, to viewController: @autoclosure @escaping () -> VC) 277 | where VC: ViewController, CV == RenderContentView, 278 | Label == Text, S: StringProtocol 279 | { 280 | assert(VC.ContentView.self != DefaultViewControllerView.self, 281 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 282 | self.init(to: viewController(), using: RenderContentView()) { 283 | Text(title) 284 | } 285 | } 286 | 287 | /** 288 | * Create a ``PushLink`` that is using the ``ViewController/ContentView`` 289 | * as the destination. 290 | * 291 | * Example: 292 | * ``` 293 | * PushLink("Go to settings…", to: SettingsPage()) 294 | * ``` 295 | */ 296 | @inlinable 297 | public init(_ titleKey: LocalizedStringKey, 298 | to viewController: @autoclosure @escaping () -> VC) 299 | where VC: ViewController, CV == RenderContentView, Label == Text 300 | { 301 | assert(VC.ContentView.self != DefaultViewControllerView.self, 302 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 303 | self.init(to: viewController(), using: RenderContentView()) { 304 | Text(titleKey) 305 | } 306 | } 307 | 308 | } 309 | 310 | 311 | // MARK: - Unavailable Initializers 312 | 313 | extension PushLink { 314 | 315 | /** 316 | * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper 317 | * ``ContentView``. 318 | */ 319 | @available(*, unavailable, 320 | message: "The ViewController needs a proper `ContentView`") 321 | public init(to viewController : @autoclosure @escaping () -> VC, 322 | @ViewBuilder label : () -> Label) 323 | where VC: ViewController, CV == RenderContentView, 324 | VC.ContentView == DefaultViewControllerView 325 | { 326 | assert(VC.ContentView.self != DefaultViewControllerView.self, 327 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 328 | self.init(to: viewController(), using: RenderContentView()) { label() } 329 | } 330 | 331 | /** 332 | * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper 333 | * ``ContentView``. 334 | */ 335 | @available(*, unavailable, 336 | message: "The ViewController needs a proper `ContentView`") 337 | public init(_ title: S, to viewController: @autoclosure @escaping () -> VC) 338 | where VC: ViewController, CV == RenderContentView, 339 | Label == Text, S: StringProtocol, 340 | VC.ContentView == DefaultViewControllerView 341 | { 342 | assert(VC.ContentView.self != DefaultViewControllerView.self, 343 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 344 | self.init(title, to: viewController()) 345 | } 346 | 347 | /** 348 | * Helper to avoid using ``PushLink`` on a ``ViewController`` w/o a proper 349 | * ``ContentView``. 350 | */ 351 | @available(*, unavailable, 352 | message: "The ViewController needs a proper `ContentView`") 353 | public init(_ titleKey: LocalizedStringKey, 354 | to viewController: @autoclosure @escaping () -> VC) 355 | where VC: ViewController, CV == RenderContentView, Label == Text, 356 | VC.ContentView == DefaultViewControllerView 357 | { 358 | assert(VC.ContentView.self != DefaultViewControllerView.self, 359 | "Attempt to use ContentView based Push w/ VC w/o ContentView") 360 | self.init(to: viewController(), using: RenderContentView()) { 361 | Text(titleKey) 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /Sources/ViewController/Presentations/AutoPresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoPresentation.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * This is used by `controlled(by:)` to attach the logic to present the 13 | * "automatic" presentations in sheets or `NavigationView`s. 14 | * 15 | * It watches the current VC to detect presentation changes, 16 | * and binds the sheet/navlink to the respective mode. 17 | */ 18 | internal struct AutoPresentationViewModifier: ViewModifier 19 | where VC: ViewController 20 | { 21 | 22 | @ObservedObject var viewController : VC 23 | 24 | fileprivate struct Present: View { 25 | 26 | @ObservedObject var presentingViewController : VC 27 | let mode : ViewControllerPresentationMode 28 | 29 | // Keep a handle to the VC being presented. We do this to avoid issue #6, 30 | // i.e. when a sheet is dismissed and transitions off-screen, the 31 | // "presentedViewController" is already gone (dismissed). 32 | // The `body` of this `Present` View would then evaluate to the 33 | // `TypeMismatchInfoView` during the dismiss. 34 | // So we keep the VC being presented around, to make sure we still have a 35 | // handle for the content-view while it is being dismissed. 36 | @State private var viewController : _ViewController? 37 | 38 | private var activeVC: _ViewController? { 39 | if let activeVC = viewController { return activeVC } 40 | 41 | if let presentation = 42 | presentingViewController.activePresentation(for: mode) 43 | { 44 | // Note: Do not modify `@State` in here! (i.e. do not push to the 45 | // `viewController` variable as part of the evaluation) 46 | // This happens if the VC is getting presented. 47 | return presentation.viewController 48 | } 49 | 50 | return nil 51 | } 52 | 53 | var body: some View { 54 | if let presentedViewController = activeVC { 55 | presentedViewController.anyControlledContentView 56 | .environment(\.viewControllerPresentationMode, mode) 57 | .navigationTitle(presentedViewController.navigationTitle) 58 | .onAppear { 59 | viewController = presentedViewController 60 | } 61 | .onDisappear { // This seems to be a proper onDidDisappear 62 | viewController = nil 63 | } 64 | } 65 | else { 66 | #if DEBUG 67 | TypeMismatchInfoView( 68 | parent: presentingViewController, expectedMode: mode 69 | ) 70 | #endif 71 | } 72 | } 73 | } 74 | 75 | func body(content: Content) -> some View { 76 | // Note: Also used internally during presentation. 77 | content 78 | // Note: The `VC` class is that of the "parent" ViewController, not of the 79 | // ViewController being presented! 80 | .sheet( 81 | isPresented: viewController.isPresentingMode(.sheet), 82 | content: { 83 | Present(presentingViewController: viewController, mode: .sheet) 84 | } 85 | ) 86 | .background( 87 | NavigationLink( 88 | isActive: viewController.isPresentingMode(.navigation), 89 | destination: { 90 | Present(presentingViewController: viewController, mode: .navigation) 91 | }, 92 | label: { Color.clear } // TBD: EmptyView? 93 | ) 94 | ) 95 | 96 | .viewControllerDebugOverlay() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/ViewController/Presentations/Presentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presentation.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension ViewController { 12 | 13 | @inlinable 14 | func willAppear() {} 15 | 16 | @inlinable 17 | func willDisappear() {} 18 | 19 | @inlinable 20 | var presentedViewController : _ViewController? { 21 | // Really a set in our case ... 22 | activePresentations.first?.viewController 23 | } 24 | } 25 | 26 | 27 | // MARK: - Lookup Presentations 28 | public extension _ViewController { 29 | 30 | /** 31 | * Returns the active ``ViewControllerPresentation`` for a given 32 | * ``ViewController/PresentationMode`` (or the first, if no mode 33 | * is specified. 34 | * 35 | * - Parameter mode: An optional presentation mode that has to match. 36 | * - Returns: An active presentation for the mode, if there is one. 37 | * Or the first active presentation if `mode` is `nil`. 38 | */ 39 | @inlinable 40 | func activePresentation(for mode: PresentationMode?) 41 | -> ViewControllerPresentation? 42 | { 43 | guard let mode = mode else { return activePresentations.first } 44 | return activePresentations.first(where: { $0.mode == mode }) 45 | } 46 | 47 | /** 48 | * Returns the active ``ViewControllerPresentation`` for a specific 49 | * ``ViewController`` object. 50 | * 51 | * - Parameter presentedViewController: The ``ViewController`` to check for. 52 | * - Returns: A presentation for the ``ViewController``, if it is indeed 53 | * being presented. 54 | */ 55 | @inlinable 56 | func activePresentation(for presentedViewController: _ViewController) 57 | -> ViewControllerPresentation? 58 | { 59 | activePresentations.first { $0.viewController === presentedViewController } 60 | } 61 | 62 | /** 63 | * Lookup a presented ``ViewController`` of a particular type. Returns nil 64 | * if there is none such (or the mode doesn't match). 65 | * 66 | * Example: 67 | * ```swift 68 | * let settingsVC = presentedViewController(Settings.self) 69 | * ``` 70 | * 71 | * - Parameters: 72 | * - type: The type of the ViewController to lookup 73 | * - mode: Optionally the mode the viewcontroller is presented in 74 | * (e.g. `sheet`, `navigation` or `custom`) 75 | * - Returns: A ``ViewController`` of the specified type, if one exists. 76 | */ 77 | @inlinable 78 | func presentedViewController(_ type: VC.Type, 79 | mode: ViewControllerPresentationMode?) 80 | -> VC? 81 | where VC: ViewController 82 | { 83 | guard let presentation = activePresentation(for: mode) else { return nil } 84 | if let mode = mode, mode != presentation.mode { return nil } 85 | return presentation.viewController as? VC 86 | } 87 | } 88 | 89 | 90 | // MARK: - Bindings 91 | public extension _ViewController { 92 | 93 | /** 94 | * This allows us to check whether a particular VC is being presented, 95 | * e.g. in case the presentation should be done differently (e.g. sheet vs 96 | * navigation). 97 | */ 98 | @inlinable 99 | func isPresenting(mode: ViewControllerPresentationMode?, 100 | _ condition: @escaping ( _ViewController ) -> Bool) 101 | -> Binding 102 | { 103 | Binding( 104 | get: { 105 | guard let presentation = self.activePresentation(for: mode) else { 106 | return false 107 | } 108 | if let mode = mode, mode != presentation.mode { return false } 109 | return condition(presentation.viewController) 110 | }, 111 | set: { isShowing in 112 | // We cannot make VCs "appear", that would require a factory. 113 | // Instead, the factory part is provided using the presentation mode 114 | // helpers. 115 | guard !isShowing else { return } // isShowing=true would be activation 116 | 117 | guard let presentation = self.activePresentation(for: mode) else { 118 | // This is fine, could have happened by other means. 119 | return logger.debug("Did not find VC to deactivate in: \(self)") 120 | } 121 | 122 | assert(mode != .automatic) 123 | assert(presentation.mode != .automatic) 124 | if let mode = mode, presentation.mode != mode { return } 125 | 126 | /// Does our condition match? 127 | guard condition(presentation.viewController) else { return } 128 | 129 | logger.debug( 130 | "Binding dismiss: \(presentation.viewController.description)") 131 | presentation.viewController.dismiss() 132 | } 133 | ) 134 | } 135 | 136 | /** 137 | * A Binding that represents whether a presentation in a particular mode is 138 | * active (e.g. `sheet`, `navigation` or `custom`). 139 | * 140 | * Used for the internally supported "auto" modes (`.sheet` and 141 | * `.navigation`). 142 | * 143 | * - Parameters: 144 | * - mode: The mode the viewcontroller is presented in 145 | * (e.g. `sheet`, `navigation` or `custom`) 146 | * - Returns: A `Bool` `Binding` that can be used w/ an `isActive` parameter 147 | * of a `sheet` or `NavigationLink`. 148 | */ 149 | @inlinable 150 | func isPresentingMode(_ mode: ViewControllerPresentationMode) 151 | -> Binding 152 | { 153 | // In here, `.pushLink` and `.navigation` must be treated differently! 154 | // Only during presentation, we may need to dismiss the other type! 155 | Binding( 156 | get: { 157 | if self.activePresentations.contains(where: { $0.mode == mode }) { 158 | logger.debug("Is presenting in mode \(mode): \(self)") 159 | return true 160 | } 161 | else { 162 | logger.debug("Not presenting in mode \(mode): \(self)") 163 | return false 164 | } 165 | }, 166 | set: { isShowing in 167 | // We cannot make VCs "appear", that would require a factory. 168 | // Instead, the factory part is provided using the presentation mode 169 | // helpers. 170 | guard !isShowing else { 171 | // isShowing=true would be activation 172 | if self.activePresentations.contains(where: { $0.mode == mode }) { 173 | logger.debug( 174 | "Attempt to activate VC via Binding, mode already active!") 175 | } 176 | else { 177 | // FIXME: This can sometimes be seen in sheets, figure out why 178 | logger.warning( 179 | "Attempt to activate VC via Binding, won't work \(self)!") 180 | } 181 | return 182 | } 183 | 184 | // Dismiss if the presentation mode matches 185 | 186 | guard let presentation = self.activePresentation(for: mode) else { 187 | // This is fine, could have happened by other means. 188 | return logger.debug( 189 | "did not find VC for mode \(mode) to deactivate in: \(self)?") 190 | } 191 | 192 | /// If a mode was requested, make sure it is the right one. 193 | /// TBD: what about "automatic" and such? Never active in an actual 194 | /// presentation! 195 | assert(presentation.mode == mode, "internal inconsistency!") 196 | guard presentation.mode == mode else { return } 197 | 198 | logger.debug( 199 | "Dismiss by mode binding: \(presentation.viewController.description)") 200 | presentation.viewController.dismiss() 201 | } 202 | ) 203 | } 204 | 205 | /** 206 | * A Binding that represents whether a particular type of ``ViewController`` 207 | * is being presented. 208 | * 209 | * CAREFUL: This only checks the type, there could be multiple presentations 210 | * with the same type! (leading to multiple Bindings being true, 211 | * and different ContentViews being active, potentially capturing the 212 | * wrong environment). 213 | * 214 | * - Parameters: 215 | * - type: The type of the ViewController to lookup 216 | * - mode: Optionally the mode the viewcontroller is presented in 217 | * (e.g. `sheet`, `navigation` or `custom`) 218 | * - Returns: A `Bool` `Binding` that can be used w/ an `isActive` parameter 219 | * of a `sheet` or `NavigationLink`. 220 | */ 221 | func isPresenting(_ controllerType: VC.Type, 222 | mode: ViewControllerPresentationMode?) 223 | -> Binding 224 | where VC: ViewController 225 | { 226 | isPresenting(mode: mode) { $0 is VC } 227 | } 228 | } 229 | 230 | // MARK: - API Methods 231 | public extension _ViewController { 232 | 233 | @inlinable 234 | func show(_ viewController: VC) { 235 | show(viewController, in: self) 236 | } 237 | @inlinable 238 | func showDetail(_ viewController: VC) { 239 | showDetail(viewController, in: self) 240 | } 241 | 242 | @inlinable 243 | func show(_ viewController: VC) 244 | where VC.ContentView == DefaultViewControllerView 245 | { 246 | present(viewController) 247 | } 248 | 249 | @inlinable 250 | func showDetail(_ viewController: VC) 251 | where VC.ContentView == DefaultViewControllerView 252 | { 253 | show(viewController) 254 | } 255 | 256 | @inlinable 257 | func present(_ viewController: VC) { 258 | defaultPresent(viewController, mode: .automatic) 259 | } 260 | 261 | @inlinable 262 | func present(_ viewController: VC, mode: PresentationMode) 263 | where VC: ViewController 264 | { 265 | defaultPresent(viewController, mode: mode) 266 | } 267 | 268 | @inlinable 269 | func present(_ viewController: VC) 270 | where VC.ContentView == DefaultViewControllerView 271 | { 272 | // Requires a custom `PushPresentation` or `SheetPresentation` 273 | logger.debug( 274 | "Presenting(.custom) a VC w/o an explicit ContentView: \(viewController)") 275 | defaultPresent(viewController, mode: .custom) 276 | } 277 | 278 | 279 | // MARK: - Present Implementation 280 | 281 | @inlinable 282 | func show(_ viewController: VC, in owner: OwnerVC) 283 | where VC: ViewController, OwnerVC: _ViewController 284 | { 285 | if let handler = presentingViewController ?? parent { 286 | return handler.show(viewController, in: owner) 287 | } 288 | owner.present(viewController) // fallback to `present` 289 | } 290 | @inlinable 291 | func showDetail(_ viewController: VC, in owner: OwnerVC) 292 | where VC: ViewController, OwnerVC: _ViewController 293 | { 294 | if let handler = presentingViewController ?? parent { 295 | return handler.showDetail(viewController, in: owner) 296 | } 297 | owner.show(viewController) // fallback to `show` 298 | } 299 | 300 | func modalPresentationMode(for viewController: VC) -> PresentationMode 301 | where VC: ViewController 302 | { 303 | // Check if the ViewController being presented explicitly requested a 304 | // certain style 305 | let desiredStyle = viewController.modalPresentationStyle 306 | if desiredStyle != .automatic { return desiredStyle } 307 | 308 | if VC.ContentView.self == DefaultViewControllerView.self { 309 | // Requires an explicit ``PushPresentation`` or ``SheetPresentation`` in 310 | // the associated `View`. 311 | logger.debug( 312 | "modalPresentationMode(.custom) for custom VC: \(viewController)") 313 | return .custom 314 | } 315 | 316 | return .sheet 317 | } 318 | 319 | func defaultPresent(_ viewController: VC, mode: PresentationMode) 320 | where VC: ViewController 321 | { 322 | guard viewController !== self else { 323 | logger.error("Attempt to present a VC in itself: \(self), \(mode)") 324 | assert(viewController !== self, "attempt to present a VC in itself") 325 | return 326 | } 327 | 328 | let mode = mode != .automatic 329 | ? mode // an explicit mode was requested 330 | : modalPresentationMode(for: viewController) 331 | 332 | // the very same VC is already being presented 333 | guard presentedViewController !== viewController else { 334 | logger.warning("Already presenting VC: \(viewController) in: \(self)") 335 | assertionFailure("already presenting VC") 336 | return 337 | } 338 | 339 | if let activePresentation = self.activePresentation(for: mode) { 340 | // This is OK. On iPad landscape activation is out of order. E.g. when 341 | // switching from link A to B, the binding first sets B to true! before 342 | // setting A to false. 343 | logger.debug("Already presenting VC in: \(self) new: \(viewController)") 344 | activePresentation.viewController.dismiss() 345 | } 346 | 347 | activePresentations.append(ViewControllerPresentation( 348 | viewController: viewController, 349 | mode: mode 350 | )) 351 | viewController.presentingViewController = self 352 | viewController.willAppear() // it is still not on screen 353 | 354 | logger.debug("Presented: \(viewController) in: \(self)") 355 | } 356 | 357 | func dismiss() { 358 | guard let parentVC = presentingViewController else { 359 | logger.warning("Dismiss of: \(self) but no `presentingViewController`?") 360 | assertionFailure("No parent VC?") 361 | return 362 | } 363 | 364 | defer { presentingViewController = nil } 365 | 366 | guard let presentation = parentVC.activePresentation(for: self) else { 367 | logger.warning( 368 | "Dismiss of: \(self), but not being presented in parent") 369 | assertionFailure("VC dismiss not being presented") 370 | return 371 | } 372 | 373 | assert(presentation.viewController === self, 374 | "VC dismiss other being presented") 375 | 376 | self.willDisappear() 377 | parentVC.activePresentations.removeAll { 378 | assert($0.viewController !== self || $0.mode == presentation.mode, 379 | "Same VC active in different modes!") 380 | return $0.viewController === self 381 | } 382 | logger.debug("Dismissed: \(self) from: \(parentVC.description)") 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /Sources/ViewController/Presentations/PresentationMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationMode.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * The active presentation mode for the ViewController. Can be accessed in the 13 | * environment using: 14 | * ```swift 15 | * public struct ContentView: View { 16 | * @Environment(\.viewControllerPresentationMode) private var mode 17 | * 18 | * var body: some View { 19 | * if mode == .sheet { 20 | * Text("I'm in a sheet!") 21 | * } 22 | * else { 23 | * Text("I'm in a sheet, NOT!") 24 | * } 25 | * } 26 | * } 27 | * ``` 28 | */ 29 | public enum ViewControllerPresentationMode: Hashable { 30 | // FIXME: Used in two different ways, for accessing the actual presentation, 31 | // and for deciding what presentation to use. 32 | 33 | /** 34 | * The ``ViewController`` will decide on an appropriate presentation mode. 35 | */ 36 | case automatic 37 | 38 | /** 39 | * The ``ViewController`` won't do the presentation automagically, 40 | * the user needs to handle it explicitly 41 | * (e.g. using the `.sheet` modifier or a programmatic `NavigationLink` with 42 | * the `isActive` bound to the `presentedViewController`). 43 | */ 44 | case custom 45 | 46 | /** 47 | * The presentation is done in a sheet that is handled automatically by the 48 | * framework. 49 | */ 50 | case sheet 51 | 52 | /** 53 | * The presentation is done using a programmatic `NavigationLink` that is 54 | * handled automatically by the framework. 55 | */ 56 | case navigation 57 | 58 | /** 59 | * The presentation is done using the ``PushLink``, an wrapped, 60 | * in-View `NavigationLink`. 61 | */ 62 | case pushLink 63 | 64 | // TODO: popover 65 | } 66 | 67 | extension ViewControllerPresentationMode: CustomStringConvertible { 68 | 69 | public var description: String { 70 | switch self { 71 | case .automatic : return "PM:auto" 72 | case .custom : return "PM:custom" 73 | case .sheet : return "PM:sheet" 74 | case .navigation : return "PM:nav" 75 | case .pushLink : return "PM:push" 76 | } 77 | } 78 | } 79 | 80 | public extension ViewController { 81 | 82 | /** 83 | * The active presentation mode for the ViewController. Can be accessed in the 84 | * environment using: 85 | * ```swift 86 | * public struct ContentView: View { 87 | * @Environment(\.viewControllerPresentationMode) private var mode 88 | * } 89 | * ``` 90 | */ 91 | typealias PresentationMode = ViewControllerPresentationMode 92 | } 93 | 94 | public extension EnvironmentValues { 95 | 96 | @usableFromInline 97 | internal struct ViewControllerPresentationModeKey: EnvironmentKey { 98 | public static let defaultValue = ViewController.PresentationMode.custom 99 | } 100 | 101 | /** 102 | * Access the means by which the current ``ViewController`` got presented, 103 | * i.e. `sheet` or `navigation`. 104 | * 105 | * This is set properly by either the `presentInSheet`, `presentInNavigation` 106 | * and the likes. 107 | * 108 | * This is sometimes useful to define how "inner" UI should look like. 109 | * Example: 110 | * ```swift 111 | * public struct ContentView: View { 112 | * @EnvironmentObject private var viewController : ViewController 113 | * @Environment(\.viewControllerPresentationMode) private var mode 114 | * 115 | * var body: some View { 116 | * Text("Hi!") 117 | * 118 | * // show explicit dismiss button for ``NavigationView`` pushes only. 119 | * if mode == .navigation { 120 | * Button("Dismiss", action: viewController.dismiss) 121 | * } 122 | * } 123 | * } 124 | * ``` 125 | */ 126 | @inlinable 127 | var viewControllerPresentationMode : ViewController.PresentationMode { 128 | set { self[ViewControllerPresentationModeKey.self] = newValue } 129 | get { self[ViewControllerPresentationModeKey.self] } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/ViewController/Presentations/ViewControllerPresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerPresentation.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * A `ViewControllerPresentation` holds the currently active presentation 13 | * within a ``ViewController``. 14 | * 15 | * It gets created when the user calls ``ViewController/present`` and friends. 16 | * 17 | * Note that a controller may hold multiple presentations, e.g. it may present 18 | * a detail in a `NavigationView` while also doing a presentation in a sheet. 19 | */ 20 | public struct ViewControllerPresentation { 21 | 22 | public let viewController : _ViewController 23 | public let mode : ViewControllerPresentationMode 24 | 25 | init(viewController: VC, mode: ViewControllerPresentationMode) 26 | where VC: ViewController 27 | { 28 | self.viewController = viewController 29 | self.mode = mode 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ViewController/ReExports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReExports.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | @_exported import Foundation 10 | @_exported import SwiftUI 11 | @_exported import struct Combine.Published 12 | @_exported import protocol Combine.ObservableObject 13 | -------------------------------------------------------------------------------- /Sources/ViewController/RenderContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenderContentView.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * Accesses the `view` of the current viewController of the specific 13 | * ``ViewController`` class in the environment. 14 | * 15 | * User level code usually doesn't need to work with this. 16 | * 17 | * Example: 18 | * ```swift 19 | * var body: some View { 20 | * // Note: The environment needs to have `SettingsPage`! 21 | * RenderContentView() 22 | * } 23 | * ``` 24 | */ 25 | public struct RenderContentView: View { 26 | 27 | @usableFromInline 28 | @EnvironmentObject var viewController : VC 29 | 30 | @inlinable 31 | public init() {} 32 | 33 | #if DEBUG 34 | @inlinable 35 | public var body: some View { 36 | if viewController.view is EmptyView { 37 | Text(verbatim: "Embedding EmptyView?") 38 | .foregroundColor(.red) 39 | } 40 | viewController.view 41 | } 42 | #else 43 | @inlinable 44 | public var body: some View { 45 | viewController.view 46 | } 47 | #endif 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | /** 13 | * A ``ViewController``. 14 | * 15 | * TODO: lotsa more documentation 16 | * 17 | * In WebObjects those would be called `WOComponent`s and are accessible 18 | * using the Environment (`WOContext` in WebObjects). 19 | * I.e. the SwiftUI environment always tracks the "active" VC. 20 | * 21 | * The lifecycle events also do not reflect whether the VC is "really" on 22 | * screen, just whether it has been presented. 23 | * 24 | * ### Custom Presentation 25 | * 26 | * There are two parts to presenting a ViewController in a custom way: 27 | * - Call `present` on the active viewController with the instance of the new, 28 | * child ViewController. The active VC can be accessed using 29 | * `@EnvironmentObject private var viewController : ViewController` 30 | * (or the specific VC subclass) 31 | * - To choose the presentation style, attach it to the View, for example: 32 | * `.presentInNavigation(ChildVC.self) { ChildVC.ContentView() }` 33 | */ 34 | public protocol ViewController: _ViewController, ObservableObject, Identifiable 35 | { 36 | 37 | // Note: We can't use an own View w/o crashing swiftc 5.6? Or is the issue 38 | // limited to a single module? 39 | typealias DefaultViewControllerView = EmptyView 40 | 41 | /** 42 | * The primary View associated with the ViewController. 43 | * 44 | * There doesn't have to be just one View associated with the ViewController, 45 | * the ViewController itself can be decoupled from a specific `ContentView`. 46 | * E.g. there could be a different main View for macOS and for iOS. 47 | * 48 | * But having a single associated `ContentView` allows for more convenient 49 | * APIs for that common case. 50 | * 51 | * Implicit View via `view` accessor: 52 | * ```swift 53 | * class Contacts: ViewController { 54 | * 55 | * var view: some View { 56 | * Text("The Contacts!") 57 | * } 58 | * } 59 | * ``` 60 | * 61 | * Implicit View, explicit class: 62 | * ```swift 63 | * class Contacts: ViewController { 64 | * 65 | * struct ContentView: View { 66 | * 67 | * @EnvironmentObject var viewController: Contacts 68 | * 69 | * var body: some View { 70 | * Text("The Contacts!") 71 | * } 72 | * } 73 | * } 74 | * ``` 75 | */ 76 | associatedtype ContentView : SwiftUI.View = DefaultViewControllerView 77 | 78 | /** 79 | * Dirty trick to let the user avoid the need to explicitly specify the 80 | * `ViewControllerView` when declaring Views within the scope of a 81 | * ViewController. 82 | * 83 | * Example: 84 | * ```swift 85 | * class Contacts: ViewController { 86 | * 87 | * struct ContentView: View { // <== this is really a ViewControllerView 88 | * ... 89 | * } 90 | * } 91 | * ``` 92 | */ 93 | typealias View = ViewControllerView 94 | 95 | /** 96 | * Returns the ``ContentView`` associated with the ``ViewController``. 97 | * 98 | * One way to specify an associated ``View`` for the controller is by 99 | * overriding this property, for example: 100 | * ```swift 101 | * class Contacts: ViewController { 102 | * 103 | * var view: some View { 104 | * Text("The Contacts!") 105 | * } 106 | * } 107 | * ``` 108 | * 109 | * Another way is to use a ``ViewControllerView`` (just a plain `View` w/ 110 | * an `init` method w/o arguments, used to instantiate the `View`): 111 | * ```swift 112 | * class Contacts: ViewController { 113 | * 114 | * struct ContentView: View { 115 | * 116 | * @EnvironmentObject var viewController: Contacts 117 | * 118 | * var body: some View { 119 | * Text("The Contacts!") 120 | * } 121 | * } 122 | * } 123 | * ``` 124 | */ 125 | @ViewBuilder var view : ContentView { get } 126 | 127 | 128 | // MARK: - Represented Object 129 | 130 | associatedtype RepresentedValue = Any 131 | 132 | /** 133 | * Get or set a value represented by the ViewController. 134 | * 135 | * Quite often VCs are used to deal with one primary model object. 136 | * This can be used to directly associated that w/ the View. 137 | * 138 | * The default implementation is going to subscribe and republish the 139 | * `willChange` notification if the ``RepresentedValue`` is an 140 | * `ObservableObject` itself. 141 | */ 142 | var representedObject : RepresentedValue? { get set } 143 | 144 | 145 | // MARK: - Titles 146 | 147 | /** 148 | * Get or set a title associated with a ViewController. 149 | */ 150 | var title : String? { set get } 151 | 152 | /** 153 | * Returns the ``title`` of the ``ViewController``, 154 | * but falls back to a default title in case that's not available. 155 | * 156 | * Suitable for use in `navigationTitle`, `navigationBarTitle` and similar 157 | * SwiftUI `View` modifiers. 158 | */ 159 | var navigationTitle : String { get } 160 | 161 | 162 | // MARK: - Presentation 163 | 164 | /** 165 | * Defines the default style in which a ``ViewController`` wants to be 166 | * presented in. 167 | */ 168 | var modalPresentationStyle : ViewControllerPresentationMode { set get } 169 | 170 | /// An internal property to track the ViewController presentation. 171 | var activePresentations : [ ViewControllerPresentation ] { set get } 172 | 173 | /** 174 | * If the ``ViewController`` is presenting another, this property returns 175 | * the presented ViewController. 176 | */ 177 | var presentedViewController : _ViewController? { get } 178 | 179 | /** 180 | * If the ``ViewController`` got presented by another, this property returns 181 | * the ViewController doing the presentation. 182 | */ 183 | var presentingViewController : _ViewController? { set get } 184 | 185 | /** 186 | * This is called if the VC was added as the presented VC, but SwiftUI 187 | * still needs a tick to diff and actually display the related view. 188 | */ 189 | func willAppear() 190 | 191 | /** 192 | * This is called if the VC was removed as a presented VC. SwiftUI will still 193 | * need a tick to diff and remove the related view. 194 | */ 195 | func willDisappear() 196 | 197 | /** 198 | * Make the ``ViewController`` the currently presented ``ViewController`` for 199 | * the given mode. 200 | */ 201 | func present(_ viewController: VC, mode: PresentationMode) 202 | where VC: ViewController 203 | /** 204 | * Make the ``ViewController`` the currently presented ``ViewController``, 205 | * in `.automatic` mode. 206 | */ 207 | func present(_ viewController: VC) 208 | /** 209 | * Present a ``ViewController`` that doesn't have a 210 | * ``ViewController/ContentView`` assigned. 211 | */ 212 | func present(_ viewController: VC) 213 | where VC.ContentView == DefaultViewControllerView 214 | 215 | /** 216 | * Present the ``ViewController`` in a context aware mode. 217 | * E.g. if it is within a ``NavigationController``, it'll get presented as 218 | * a navigation. 219 | * By default ``ViewController``s are presented as sheets. 220 | * 221 | * - Parameter viewController: The ``ViewController`` to present. 222 | */ 223 | func show(_ viewController: VC) 224 | /** 225 | * Present a ``ViewController`` that doesn't specify an explicit 226 | * ``ViewController/ContentView`` type (i.e. doesn't implement `view` or 227 | * typealias/nest a `ContentView` type). 228 | * Unless specified otherwise in the presentationMode, this will end up in 229 | * a ``ViewControllerPresentationMode/custom`` (i.e. the user has to deal 230 | * with the presentation himself). 231 | * 232 | * - Parameter viewController: The ``ViewController`` to present. 233 | */ 234 | func show(_ viewController: VC) 235 | where VC.ContentView == DefaultViewControllerView 236 | 237 | /** 238 | * Present the ``ViewController`` in a context aware, "detail", mode. 239 | * 240 | * If the container ViewController doesn't support an explicit "detail" mode, 241 | * this acts like ``ViewController/show``. 242 | * 243 | * - Parameter viewController: The ``ViewController`` to present. 244 | */ 245 | func showDetail(_ viewController: VC) 246 | /** 247 | * Present a ``ViewController`` that doesn't specify an explicit 248 | * ``ViewController/ContentView`` type (i.e. doesn't implement `view` or 249 | * typealias/nest a `ContentView` type). 250 | * Unless specified otherwise in the presentationMode, this will end up in 251 | * a ``ViewControllerPresentationMode/custom`` (i.e. the user has to deal 252 | * with the presentation himself). 253 | * 254 | * - Parameter viewController: The ``ViewController`` to present. 255 | */ 256 | func showDetail(_ viewController: VC) 257 | where VC.ContentView == DefaultViewControllerView 258 | 259 | /// Internal method which allows presenting view controllers the actual 260 | /// presentation further down in the stack. 261 | func show(_ viewController: VC, in owner: OwnerVC) 262 | where VC: ViewController, OwnerVC: _ViewController 263 | /// Internal method which allows presenting view controllers the actual 264 | /// presentation further down in the stack. 265 | func showDetail(_ viewController: VC, in owner: OwnerVC) 266 | where VC: ViewController, OwnerVC: _ViewController 267 | 268 | /** 269 | * Remove the ViewController from being presented in its presenting 270 | * ViewController. 271 | * 272 | * Will call `willDisappear` (only) if it actually was presented by the 273 | * parent. 274 | */ 275 | func dismiss() 276 | 277 | 278 | // MARK: - Hierarchy 279 | 280 | /** 281 | * The array of contained view controllers (children). 282 | * 283 | * Use ``addChild`` to add children to this array and use ``removeFromParent`` 284 | * to remove a child from its parent. 285 | * 286 | * Not to be confused w/ ``presentedViewControllers``. 287 | */ 288 | var children : [ _ViewController ] { set get } 289 | /** 290 | * The parent of a contained ``ViewController`` (if it is actually contained). 291 | * 292 | * Use ``addChild`` to add children to a parent and use ``removeFromParent`` 293 | * to remove a child from its parent. 294 | * 295 | * Not to be confused w/ ``presentingViewController``. 296 | */ 297 | var parent : _ViewController? { set get } 298 | 299 | /** 300 | * This is called once the ViewController will be added or removed as a child. 301 | */ 302 | func willMove(toParent parent: _ViewController?) 303 | 304 | /** 305 | * This is called once the ViewController was added or removed as a child. 306 | */ 307 | func didMove(toParent parent: _ViewController?) 308 | 309 | /** 310 | * Add the specified ``ViewController`` as a child ("contained") 311 | * ViewController. 312 | * 313 | * This will add the `viewController` to the ``children`` array and set its 314 | * ``parent`` property. 315 | */ 316 | func addChild(_ viewController: VC) 317 | 318 | /** 319 | * Remove the ``ViewController`` from its parent. 320 | */ 321 | func removeFromParent() 322 | } 323 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/Containment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Containment.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension ViewController { 12 | 13 | @inlinable 14 | func willMove(toParent parent: _ViewController?) {} 15 | @inlinable 16 | func didMove (toParent parent: _ViewController?) {} 17 | } 18 | 19 | public extension ViewController { 20 | 21 | @inlinable 22 | func addChild(_ viewController: VC) { 23 | assert(!children.contains(where: { $0 === viewController }), 24 | "ViewController already contained as a child!") 25 | assert(viewController.parent !== self, 26 | "ViewController is already the parent of the child!") 27 | 28 | // Do nothing if it is the same parent (i.e. do NOT re-add) 29 | guard viewController.parent !== self else { return } 30 | 31 | // Remove from old parent 32 | if viewController.parent != nil { removeFromParent() } 33 | 34 | viewController.willMove(toParent: self) 35 | children.append(viewController) 36 | viewController.parent = self 37 | viewController.didMove(toParent: self) 38 | } 39 | 40 | @inlinable 41 | func removeFromParent() { 42 | guard let parent = parent else { 43 | return logger.warning( 44 | "removeFromParent() called w/o a parent being active: \(self)") 45 | } 46 | 47 | guard let idx = parent.children.firstIndex(where: { $0 === self }) else { 48 | logger.warning( 49 | "removeFromParent() w/o parent having the VC: \(self) \(parent.description))") 50 | assertionFailure("parent set to a VC, but missing from the children!") 51 | return 52 | } 53 | 54 | willMove(toParent: nil) 55 | parent.children.remove(at: idx) 56 | self.parent = nil 57 | didMove(toParent: nil) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß on 27.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension ViewController where ContentView: ViewControllerView { 11 | 12 | /** 13 | * The default implementation of the user's ``ViewController`` doesn't 14 | * implement the ``ViewController/view`` method, 15 | * but _does_ have an associated ``View/ContentView`` specified. In this case 16 | * the `ContentView` needs to be a ``ViewControllerView`` (which just adds 17 | * the empty `init` to `View`). 18 | */ 19 | @inlinable 20 | @ViewBuilder var view : ContentView { ContentView() } 21 | } 22 | 23 | /** 24 | * A view used to properly refresh the contentview on VC changes. 25 | * 26 | * The ``ViewController.ContentView`` doesn't necessarily subscribe the VC 27 | * for changes, i.e. if used using the plain `var view` override. 28 | * This makes sure the `view` actually gets re-evaluated. 29 | */ 30 | @usableFromInline 31 | struct ControlWrapper: View { 32 | 33 | @ObservedObject fileprivate var viewController : VC 34 | 35 | @usableFromInline 36 | init(viewController: VC) { self.viewController = viewController } 37 | 38 | @usableFromInline 39 | var body : some View { 40 | viewController.view 41 | .controlled(by: viewController) 42 | } 43 | } 44 | 45 | public extension ViewController { 46 | 47 | @inlinable 48 | var controlledContentView : some SwiftUI.View { 49 | ControlWrapper(viewController: self) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/DefaultDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultDescription.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | extension _ViewController { 10 | 11 | internal var oidString : String { 12 | String(UInt(bitPattern: ObjectIdentifier(self)), radix: 16) 13 | } 14 | 15 | internal var typeName : String { String(describing: type(of: self)) } 16 | } 17 | 18 | extension ViewController { // MARK: - Description 19 | 20 | public var description: String { 21 | var ms = "<\(typeName)[\(oidString)]:" 22 | appendAttributes(to: &ms) 23 | ms += ">" 24 | return ms 25 | } 26 | 27 | @inlinable 28 | public func appendAttributes(to description: inout String) { 29 | defaultAppendAttributes(to: &description) 30 | } 31 | 32 | public func defaultAppendAttributes(to description: inout String) { 33 | // public, so that subclasses can call this "super" implementation! 34 | if let v = title { description += " '\(v)'" } 35 | assert(self !== presentedViewController) 36 | 37 | if activePresentations.count == 1, let v = activePresentations.first { 38 | let vc = "\(v.viewController.typeName)[\(v.viewController.oidString)]" 39 | switch v.mode { 40 | case .automatic: 41 | assertionFailure("Unexpected presentation mode: \(v)") 42 | description += " presenting[AUTO!!]=\(vc)" 43 | case .custom : description += " presenting[CUSTOM]=\(vc)" 44 | case .sheet : description += " presenting[sheet]=\(vc)" 45 | case .navigation : description += " presenting[nav]=\(vc)" 46 | case .pushLink : description += " presenting[link]=\(vc)" 47 | } 48 | } 49 | else if !activePresentations.isEmpty { 50 | description += " presenting=#\(activePresentations.count)" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/RepresentedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepresentedObject.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | public extension ViewController { 12 | 13 | @inlinable 14 | var representedObject : RepresentedValue? { 15 | set { 16 | if let storage = storageIfAvailable { 17 | storage.representedObject = newValue 18 | } 19 | else if let value = newValue { 20 | self.storage.representedObject = value 21 | } 22 | } 23 | get { storageIfAvailable?.representedObject } 24 | } 25 | } 26 | 27 | public extension ViewController 28 | where RepresentedValue: ObservableObject, 29 | RepresentedValue.ObjectWillChangePublisher == ObservableObjectPublisher 30 | { 31 | // A variant which subscribes to the representedObject if it is an 32 | // ObservableObject. Needs a test. 33 | 34 | @inlinable 35 | var representedObject : RepresentedValue? { 36 | set { 37 | if let storage = storageIfAvailable { 38 | guard newValue !== storage.representedObject else { return } // same 39 | storage.representedObjectSubscription = nil 40 | storage.representedObject = newValue 41 | storage.representedObjectSubscription = 42 | newValue?.objectWillChange.sink { [weak self] in 43 | self?.objectWillChange.send() 44 | } 45 | } 46 | else if let value = newValue { 47 | storage.representedObject = value 48 | storage.representedObjectSubscription = 49 | newValue?.objectWillChange.sink { [weak self] in 50 | self?.objectWillChange.send() 51 | } 52 | } 53 | } 54 | get { storageIfAvailable?.representedObject } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/Subscriptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subscriptions.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | public extension ViewController { 12 | 13 | /** 14 | * Subscribes the ViewController to changes in another ``ObservableObject``, 15 | * usually a "model" object. 16 | * If the other object emits a `willChange` event, so will the ViewController. 17 | * 18 | * Example: 19 | * ```swift 20 | * class Contacts: ViewController { 21 | * 22 | * let contacts = ContactsStore.shared 23 | * 24 | * init() { 25 | * willChange(with: contacts) 26 | * } 27 | * } 28 | * ``` 29 | */ 30 | @inlinable 31 | func willChange(with model: T) { 32 | 33 | let subscription = model.objectWillChange.sink { [weak self] _ in 34 | guard let me = self else { return } 35 | me.objectWillChange.send() 36 | } 37 | 38 | if storage.subscriptions == nil { 39 | storage.subscriptions = Set() 40 | } 41 | storage.subscriptions?.insert(subscription) 42 | } 43 | 44 | /** 45 | * Subscribes the ViewController to changes in other ``ObservableObject``s, 46 | * usually "model" objects. 47 | * If the other object emits a `willChange` event, so will the ViewController. 48 | * 49 | * Example: 50 | * ```swift 51 | * class Contacts: ViewController { 52 | * 53 | * let contacts = ContactsStore.shared 54 | * let calendars = CalendarsStore.shared 55 | * let tasks = TasksStore.shared 56 | * 57 | * init() { 58 | * willChange(with: contacts, calendars, tasks) 59 | * } 60 | * } 61 | * ``` 62 | */ 63 | @inlinable 64 | func willChange(with model1: T1, model2: T2, model3: T3?, 65 | model4: T4?, model5: T5?) 66 | where T1: ObservableObject, T2: ObservableObject, T3: ObservableObject, 67 | T4: ObservableObject, T5: ObservableObject 68 | { 69 | willChange(with: model1) 70 | willChange(with: model2) 71 | if let model = model3 { willChange(with: model) } 72 | if let model = model4 { willChange(with: model) } 73 | if let model = model5 { willChange(with: model) } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/Title.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Title.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | public extension ViewController { 10 | 11 | @inlinable 12 | var navigationTitle : String { title ?? defaultNavigationTitle } 13 | 14 | /** 15 | * Returns a navigation title based on the ``ViewController``s type name. 16 | */ 17 | @inlinable 18 | var defaultNavigationTitle: String { 19 | let typeName = "\(type(of: self))" 20 | 21 | let cutName : String = { 22 | if typeName.hasSuffix("ViewController") { 23 | return String(typeName.dropLast(14)) 24 | } 25 | else if typeName.hasSuffix("VC") { 26 | return String(typeName.dropLast(14)) 27 | } 28 | else { 29 | return typeName 30 | } 31 | }() 32 | 33 | if let idx = cutName.firstIndex(of: ".") { 34 | return String(cutName[cutName.index(after: idx)...]) 35 | } 36 | 37 | return cutName 38 | } 39 | 40 | 41 | @available(*, deprecated, renamed: "navigationTitle") 42 | @inlinable 43 | var navigationBarTitle : String { navigationTitle } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/TypeErasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeErasure.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß on 26.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension ViewController { 11 | 12 | @inlinable 13 | var anyControlledContentView : AnyView { AnyView(controlledContentView) } 14 | 15 | @inlinable 16 | var anyRepresentedObject : Any? { 17 | set { representedObject = newValue as? RepresentedValue } 18 | get { representedObject } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/ViewControllerStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerStorage.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | // Why all this Storage mess? Because we'd really like to keep `ViewController` 13 | // a protocol and not introduce a `ViewControllerBase` like object to provide 14 | // persistence for implementors. 15 | 16 | 17 | // MARK: - Forwarders (Peek and pop from the associated storage class) 18 | 19 | public extension ViewController { 20 | 21 | // TODO: Make a nice keypath subscript for the repetitive code 22 | 23 | @inlinable 24 | var title : String? { 25 | set { 26 | if let storage = storageIfAvailable { storage.title = newValue } 27 | else if let value = newValue { storage.title = value } 28 | } 29 | get { storageIfAvailable?.title } 30 | } 31 | 32 | 33 | // MARK: - Presentation 34 | 35 | @inlinable 36 | var modalPresentationStyle: ViewControllerPresentationMode { 37 | set { 38 | if let storage = storageIfAvailable { 39 | storage.modalPresentationStyle = newValue 40 | } 41 | else if newValue != .automatic { 42 | storage.modalPresentationStyle = newValue 43 | } 44 | } 45 | get { storageIfAvailable?.modalPresentationStyle ?? .automatic } 46 | } 47 | 48 | @inlinable 49 | var activePresentations : [ ViewControllerPresentation ] { 50 | set { 51 | if let storage = storageIfAvailable { storage.presentations = newValue } 52 | else if !newValue.isEmpty { storage.presentations = newValue } 53 | } 54 | get { storageIfAvailable?.presentations ?? [] } 55 | } 56 | 57 | @inlinable 58 | var presentingViewController : _ViewController? { 59 | set { 60 | if let storage = storageIfAvailable { 61 | storage.presentingViewController = newValue 62 | } 63 | else if let value = newValue { 64 | storage.presentingViewController = value 65 | } 66 | } 67 | get { storageIfAvailable?.presentingViewController } 68 | } 69 | 70 | 71 | // MARK: - Hierarchy 72 | 73 | @inlinable 74 | var children : [ _ViewController ] { 75 | set { 76 | if let storage = storageIfAvailable { 77 | storage.childViewControllers = newValue 78 | } 79 | else if !newValue.isEmpty { 80 | self.storage.childViewControllers = newValue 81 | } 82 | } 83 | get { storageIfAvailable?.childViewControllers ?? [] } 84 | } 85 | 86 | @inlinable 87 | var parent : _ViewController? { 88 | set { 89 | if let storage = storageIfAvailable { 90 | storage.parentViewController = newValue 91 | } 92 | else if let value = newValue { 93 | self.storage.parentViewController = value 94 | } 95 | } 96 | get { storageIfAvailable?.parentViewController } 97 | } 98 | } 99 | 100 | 101 | // MARK: - Holder Class 102 | 103 | /** 104 | * An internal state holder class used to associated some default properties 105 | * with a VC. 106 | * This is done to keep ``ViewController`` as a protocol. 107 | * Alternative: Keep ``ViewController`` as a protocol, but add 108 | * ``ViewControllerBase`` as a base class. 109 | * 110 | * To access the storage of the ViewController, use 111 | * ``ViewController/storage`` or ``ViewController/storageIfAvailable``. 112 | */ 113 | @usableFromInline 114 | internal class ViewControllerStorage: NSObject where VC: ViewController { 115 | // So the important point here is to emit changes! 116 | 117 | weak var viewController : VC? 118 | 119 | init(_ viewController: VC) { self.viewController = viewController } 120 | 121 | @usableFromInline 122 | internal var representedObject : VC.RepresentedValue? { 123 | willSet { viewController?.objectWillChange.send() } 124 | } 125 | @usableFromInline 126 | internal var representedObjectSubscription: AnyCancellable? 127 | 128 | 129 | // MARK: - Title 130 | 131 | @usableFromInline 132 | internal var title : String? { 133 | willSet { viewController?.objectWillChange.send() } 134 | } 135 | 136 | 137 | // MARK: - Presentation 138 | 139 | // no change event needed for this? 140 | @usableFromInline 141 | internal var modalPresentationStyle = ViewControllerPresentationMode.automatic 142 | 143 | @usableFromInline 144 | internal var presentations : [ ViewControllerPresentation ] = [] { 145 | willSet { 146 | if presentations.count == newValue.count { 147 | if newValue.isEmpty { return } // both empty, do not emit 148 | 149 | if !zip(presentations, newValue).contains(where: { lhs, rhs in 150 | lhs.viewController !== rhs.viewController || lhs.mode != rhs.mode 151 | }) { return } // same 152 | } 153 | 154 | viewController?.objectWillChange.send() 155 | } 156 | } 157 | 158 | @usableFromInline 159 | internal weak var presentingViewController : _ViewController? 160 | 161 | 162 | // MARK: - Hierarchy 163 | 164 | @usableFromInline 165 | internal var childViewControllers = [ _ViewController ]() { 166 | willSet { 167 | guard childViewControllers.map({ ObjectIdentifier($0) }) 168 | != newValue.map({ ObjectIdentifier($0) }) else { return } 169 | viewController?.objectWillChange.send() 170 | } 171 | } 172 | 173 | @usableFromInline 174 | internal weak var parentViewController : _ViewController? 175 | 176 | 177 | // MARK: - Subscriptions 178 | 179 | /** 180 | * ViewController's as ObservableObject's very often depend on some model 181 | * objects or other publishers. 182 | * This provides a default storage for such. 183 | */ 184 | @usableFromInline 185 | internal var subscriptions : Set? 186 | } 187 | 188 | 189 | // MARK: - Associated Object 190 | 191 | fileprivate var associatedObjectToken = 42 192 | 193 | internal extension ViewController { 194 | 195 | /** 196 | * Returns the associated storage for the ``ViewController``, 197 | * i.e. the place where the protocol puts the tracking state. 198 | * 199 | * If the ViewController doesn't have storage yet, it gets allocated and 200 | * assigned. 201 | * 202 | * Use ``ViewController/storageIfAvailable`` to avoid storage allocation. 203 | */ 204 | @usableFromInline 205 | var storage : ViewControllerStorage { 206 | get { 207 | if let storage = storageIfAvailable { return storage } 208 | let storage = ViewControllerStorage(self) 209 | storageIfAvailable = storage 210 | return storage 211 | } 212 | } 213 | 214 | /** 215 | * Returns the associated storage for the ``ViewController``, 216 | * i.e. the place where the protocol puts the tracking state. 217 | * 218 | * If the ViewController doesn't have storage yet, nil is returned. 219 | * 220 | * Use ``ViewController/storage`` to allocate storage on demand. 221 | */ 222 | @usableFromInline 223 | var storageIfAvailable : ViewControllerStorage? { 224 | set { 225 | assert(newValue != nil, "Attempt to clear VC storage?!") 226 | objc_setAssociatedObject(self, &associatedObjectToken, newValue, 227 | .OBJC_ASSOCIATION_RETAIN) 228 | } 229 | get { 230 | guard let value = objc_getAssociatedObject(self, &associatedObjectToken) else { 231 | return nil 232 | } 233 | assert(value is ViewControllerStorage, 234 | "Unexpected storage associated w/ ViewController \(value) \(self)") 235 | return value as? ViewControllerStorage 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewController/_ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _ViewController.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | /** 13 | * The base protocol which can be used as an existential. 14 | * 15 | * Associated types added by ``ViewController``: 16 | * - ObjectWillChangePublisher 17 | * - ContentView 18 | * - RepresentedValue 19 | */ 20 | public protocol _ViewController: AnyObject, CustomStringConvertible { 21 | // Even just `ObservableObject` is a PAT. 22 | 23 | typealias ObjectWillChangePublisher = ObservableObjectPublisher 24 | 25 | typealias PresentationMode = ViewControllerPresentationMode 26 | 27 | // MARK: - Titles 28 | 29 | /** 30 | * Get or set a title associated with a ViewController. 31 | */ 32 | var title : String? { set get } 33 | 34 | /** 35 | * Returns the ``title`` of the ``ViewController``, 36 | * but falls back to a default title in case that's not available. 37 | * 38 | * Suitable for use in `navigationTitle`, `navigationBarTitle` and similar 39 | * SwiftUI `View` modifiers. 40 | */ 41 | var navigationTitle : String { get } 42 | 43 | 44 | // MARK: - Presentation 45 | 46 | /** 47 | * Defines the default style in which a ``ViewController`` wants to be 48 | * presented in. 49 | */ 50 | var modalPresentationStyle : ViewControllerPresentationMode { set get } 51 | 52 | /// An internal property to track the ViewController presentation. 53 | var activePresentations : [ ViewControllerPresentation ] { set get } 54 | 55 | /** 56 | * If the ``ViewController`` is presenting another, this property returns 57 | * the presented ViewController. 58 | */ 59 | var presentedViewController : _ViewController? { get } 60 | 61 | /** 62 | * If the ``ViewController`` got presented by another, this property returns 63 | * the ViewController doing the presentation. 64 | */ 65 | var presentingViewController : _ViewController? { set get } 66 | 67 | /** 68 | * This is called if the VC was added as the presented VC, but SwiftUI 69 | * still needs a tick to diff and actually display the related view. 70 | */ 71 | func willAppear() 72 | 73 | /** 74 | * This is called if the VC was removed as a presented VC. SwiftUI will still 75 | * need a tick to diff and remove the related view. 76 | */ 77 | func willDisappear() 78 | 79 | /** 80 | * Make the ``ViewController`` the currently presented ``ViewController`` for 81 | * the given mode. 82 | */ 83 | func present(_ viewController: VC, mode: PresentationMode) 84 | where VC: ViewController 85 | /** 86 | * Make the ``ViewController`` the currently presented ``ViewController``, 87 | * in `.automatic` mode. 88 | */ 89 | func present(_ viewController: VC) 90 | /** 91 | * Present a ``ViewController`` that doesn't have a 92 | * ``ViewController/ContentView`` assigned. 93 | */ 94 | func present(_ viewController: VC) 95 | where VC.ContentView == DefaultViewControllerView 96 | 97 | func show(_ viewController: VC) 98 | func show(_ viewController: VC) 99 | where VC.ContentView == DefaultViewControllerView 100 | 101 | func showDetail(_ viewController: VC) 102 | func showDetail(_ viewController: VC) 103 | where VC.ContentView == DefaultViewControllerView 104 | 105 | /// Internal method which allows presenting view controllers the actual 106 | /// presentation further down in the stack. 107 | func show(_ viewController: VC, in owner: OwnerVC) 108 | where VC: ViewController, OwnerVC: _ViewController 109 | /// Internal method which allows presenting view controllers the actual 110 | /// presentation further down in the stack. 111 | func showDetail(_ viewController: VC, in owner: OwnerVC) 112 | where VC: ViewController, OwnerVC: _ViewController 113 | 114 | /** 115 | * Remove the ViewController from being presented in its presenting 116 | * ViewController. 117 | * 118 | * Will call `willDisappear` (only) if it actually was presented by the 119 | * parent. 120 | */ 121 | func dismiss() 122 | 123 | 124 | // MARK: - Hierarchy 125 | 126 | /** 127 | * The array of contained view controllers (children). 128 | * 129 | * Use ``addChild`` to add children to this array and use ``removeFromParent`` 130 | * to remove a child from its parent. 131 | * 132 | * Not to be confused w/ ``presentedViewControllers``. 133 | */ 134 | var children : [ _ViewController ] { set get } 135 | /** 136 | * The parent of a contained ``ViewController`` (if it is actually contained). 137 | * 138 | * Use ``addChild`` to add children to a parent and use ``removeFromParent`` 139 | * to remove a child from its parent. 140 | * 141 | * Not to be confused w/ ``presentingViewController``. 142 | */ 143 | var parent : _ViewController? { set get } 144 | 145 | /** 146 | * This is called once the ViewController will be added or removed as a child. 147 | */ 148 | func willMove(toParent parent: _ViewController?) 149 | 150 | /** 151 | * This is called once the ViewController was added or removed as a child. 152 | */ 153 | func didMove(toParent parent: _ViewController?) 154 | 155 | /** 156 | * Add the specified ``ViewController`` as a child ("contained") 157 | * ViewController. 158 | * 159 | * This will add the `viewController` to the ``children`` array and set its 160 | * ``parent`` property. 161 | */ 162 | func addChild(_ viewController: VC) 163 | 164 | /** 165 | * Remove the ``ViewController`` from its parent. 166 | */ 167 | func removeFromParent() 168 | 169 | 170 | // MARK: - Better Description 171 | 172 | /** 173 | * Override in subclasses to add own properties to the VC description. 174 | */ 175 | func appendAttributes(to description: inout String) 176 | 177 | 178 | // MARK: - Type Erasure 179 | 180 | /** 181 | * Returns the type erased ``ContentView`` of the ``ViewController``, 182 | * with the ``ViewController`` being applied as the ``controlled(by:)`` 183 | * View. 184 | */ 185 | var anyControlledContentView : AnyView { get } 186 | 187 | /** 188 | * Returns the type erased represented object of the ``ViewController``. 189 | */ 190 | var anyRepresentedObject : Any? { set get } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewControllerEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerPresentationModifier.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension EnvironmentValues { 12 | 13 | @usableFromInline 14 | internal struct ViewControllerKey: EnvironmentKey { 15 | public static let defaultValue : _ViewController? = nil 16 | } 17 | 18 | /** 19 | * Allows access to the ``ViewController``, w/o having the `View` refreshed if 20 | * the ViewController changes. 21 | * 22 | * I.e. an "unobserved object". 23 | * 24 | * Can be used like this: 25 | * ```swift 26 | * struct ContentView: View { 27 | * @Environment(\.viewController) private var viewController 28 | * var body: some View { 29 | * Text(verbatim: "VC: \(viewController)") 30 | * } 31 | * } 32 | * ``` 33 | */ 34 | @inlinable 35 | var viewController : _ViewController? { 36 | set { self[ViewControllerKey.self] = newValue } 37 | get { self[ViewControllerKey.self] } 38 | } 39 | } 40 | 41 | public extension View { 42 | 43 | /** 44 | * `.controlled(vc)` pushes the ``ViewController`` as an environment object, 45 | * both under its concrete type, and as the generic ``ViewController``. 46 | * 47 | * It also pushes the VC into the `viewController` environment key, which 48 | * allows access w/o having a View refresh on VC changes (kinda like an 49 | * `UnobservedEnvironmentObject`). 50 | * 51 | * This also sets up the sheet/programmatic `NavigationLink` for automatic 52 | * presentation. 53 | * 54 | * Externally this modifier is usually only used at the very top of the VC 55 | * stack, i.e. for the "SceneController": 56 | * ```swift 57 | * struct ContentView: View { 58 | * 59 | * @StateObject private var viewController : WidgetViewVC 60 | * 61 | * var body: some View { 62 | * NavigationView { 63 | * WidgetViewVC.ContentView() 64 | * .controlled(by: viewController) 65 | * } 66 | * } 67 | * } 68 | * ``` 69 | * 70 | * - Parameters: 71 | * - viewController: The (instantiated) view controller object to apply to 72 | * View. 73 | */ 74 | func controlled(by viewController: VC) 75 | -> some SwiftUI.View 76 | { 77 | // Note: Also used internally during presentation. 78 | self 79 | .modifier(AutoPresentationViewModifier(viewController: viewController)) 80 | .environmentObject(viewController) 81 | .environmentObject(AnyViewController(viewController)) 82 | .environment(\.viewController, viewController) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/ViewController/ViewControllerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerView.swift 3 | // ViewController 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2022 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /** 12 | * A View that can be instantiated w/o further arguments. 13 | * 14 | * This is used within ``ViewController``s, to instantiate their 15 | * ``ViewController/ContentView``. 16 | */ 17 | public protocol ViewControllerView: View { 18 | init() 19 | } 20 | 21 | extension EmptyView: ViewControllerView {} 22 | 23 | 24 | #if true // This Works 25 | 26 | public typealias DefaultViewControllerView = EmptyView 27 | 28 | #else // 2022-04-24: This Segfaults swiftc in Xcode 13.3 29 | 30 | #if DEBUG 31 | public struct DefaultViewControllerView: View { 32 | @Environment(\.viewController) private var viewController 33 | 34 | @usableFromInline 35 | init() { assertionFailure("No VC View is defined!") } 36 | 37 | public var body: some View { 38 | VStack { 39 | Label("Missing VC View", systemImage: "questionmark.circle") 40 | Spacer() 41 | if let viewController = viewController { 42 | Text(verbatim: "Class: \(viewController.typeName)") 43 | Text(verbatim: "ID: \(viewController.oidString)") 44 | Text(verbatim: "\(viewController)") 45 | } 46 | else { 47 | Text("No VC set in environment?!") 48 | } 49 | } 50 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 51 | } 52 | } 53 | #else 54 | public struct DefaultViewControllerView: View { 55 | public var body: some View { 56 | // TODO: report issue etc? (using bug reporter link in Info.plist?) 57 | Label("Missing VC View", systemImage: "questionmark.circle") 58 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 59 | } 60 | } 61 | #endif 62 | #endif 63 | -------------------------------------------------------------------------------- /ViewController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E800501A2816B00E00E4805C /* DebugMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050192816B00E00E4805C /* DebugMode.swift */; }; 11 | E800501B2816B00E00E4805C /* DebugMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050192816B00E00E4805C /* DebugMode.swift */; }; 12 | E800501D2816B09A00E4805C /* DebugOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501C2816B09A00E4805C /* DebugOverlay.swift */; }; 13 | E800501E2816B09A00E4805C /* DebugOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501C2816B09A00E4805C /* DebugOverlay.swift */; }; 14 | E80050202816C40600E4805C /* AutoPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501F2816C40600E4805C /* AutoPresentation.swift */; }; 15 | E80050212816C40600E4805C /* AutoPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E800501F2816C40600E4805C /* AutoPresentation.swift */; }; 16 | E80050232816EE1D00E4805C /* HierarchyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050222816EE1D00E4805C /* HierarchyView.swift */; }; 17 | E80050242816EE1D00E4805C /* HierarchyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050222816EE1D00E4805C /* HierarchyView.swift */; }; 18 | E80050262816EE3400E4805C /* ViewControllerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050252816EE3400E4805C /* ViewControllerInfo.swift */; }; 19 | E80050272816EE3400E4805C /* ViewControllerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E80050252816EE3400E4805C /* ViewControllerInfo.swift */; }; 20 | E8283B432819629E00467F10 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8283B422819629E00467F10 /* ContentView.swift */; }; 21 | E8283B442819629E00467F10 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8283B422819629E00467F10 /* ContentView.swift */; }; 22 | E836E5AD280EECD50001B85E /* PushLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5AC280EECD50001B85E /* PushLink.swift */; }; 23 | E836E5AE280EECD50001B85E /* PushLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E836E5AC280EECD50001B85E /* PushLink.swift */; }; 24 | E83ADA662814267600D98D82 /* Containment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA652814267600D98D82 /* Containment.swift */; }; 25 | E83ADA672814267600D98D82 /* Containment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA652814267600D98D82 /* Containment.swift */; }; 26 | E83ADA78281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */; }; 27 | E83ADA79281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */; }; 28 | E88DCE6B2812C4CF00CD5203 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */; }; 29 | E88DCE6C2812C4CF00CD5203 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */; }; 30 | E88DCE702812C83C00CD5203 /* ReExports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6F2812C83C00CD5203 /* ReExports.swift */; }; 31 | E88DCE712812C83C00CD5203 /* ReExports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE6F2812C83C00CD5203 /* ReExports.swift */; }; 32 | E88DCE732812CEE700CD5203 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE722812CEE700CD5203 /* ViewController.swift */; }; 33 | E88DCE742812CEE700CD5203 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE722812CEE700CD5203 /* ViewController.swift */; }; 34 | E88DCE762812D10000CD5203 /* ViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE752812D10000CD5203 /* ViewControllerView.swift */; }; 35 | E88DCE772812D10000CD5203 /* ViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE752812D10000CD5203 /* ViewControllerView.swift */; }; 36 | E88DCE7A2812D1B900CD5203 /* DefaultDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE792812D1B900CD5203 /* DefaultDescription.swift */; }; 37 | E88DCE7B2812D1B900CD5203 /* DefaultDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE792812D1B900CD5203 /* DefaultDescription.swift */; }; 38 | E88DCE7D2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */; }; 39 | E88DCE7E2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */; }; 40 | E88DCE802812D6A700CD5203 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */; }; 41 | E88DCE812812D6A700CD5203 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */; }; 42 | E88DCE832812DD8B00CD5203 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE822812DD8B00CD5203 /* Title.swift */; }; 43 | E88DCE842812DD8B00CD5203 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE822812DD8B00CD5203 /* Title.swift */; }; 44 | E88DCE862812DF8200CD5203 /* RepresentedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE852812DF8200CD5203 /* RepresentedObject.swift */; }; 45 | E88DCE872812DF8200CD5203 /* RepresentedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE852812DF8200CD5203 /* RepresentedObject.swift */; }; 46 | E88DCE892812E9CD00CD5203 /* Presentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE882812E9CD00CD5203 /* Presentation.swift */; }; 47 | E88DCE8A2812E9CD00CD5203 /* Presentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE882812E9CD00CD5203 /* Presentation.swift */; }; 48 | E88DCE8F2812EAF800CD5203 /* AnyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */; }; 49 | E88DCE902812EAF800CD5203 /* AnyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */; }; 50 | E88DCE922812FA7A00CD5203 /* _ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE912812FA7A00CD5203 /* _ViewController.swift */; }; 51 | E88DCE932812FA7A00CD5203 /* _ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE912812FA7A00CD5203 /* _ViewController.swift */; }; 52 | E88DCE9B28130B8300CD5203 /* RenderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE9A28130B8300CD5203 /* RenderContentView.swift */; }; 53 | E88DCE9C28130B8300CD5203 /* RenderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E88DCE9A28130B8300CD5203 /* RenderContentView.swift */; }; 54 | E8BA6EBB281588D800FA6C5C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBA281588D800FA6C5C /* Logger.swift */; }; 55 | E8BA6EBC281588D800FA6C5C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBA281588D800FA6C5C /* Logger.swift */; }; 56 | E8BA6EBF2815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */; }; 57 | E8BA6EC02815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */; }; 58 | E8BA6EC32815956200FA6C5C /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EC22815956200FA6C5C /* NavigationController.swift */; }; 59 | E8BA6EC42815956200FA6C5C /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8BA6EC22815956200FA6C5C /* NavigationController.swift */; }; 60 | E8E7066F27F2014F00F50160 /* PresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066827F2014F00F50160 /* PresentationMode.swift */; }; 61 | E8E7067027F2014F00F50160 /* PresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066827F2014F00F50160 /* PresentationMode.swift */; }; 62 | E8E7067327F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */; }; 63 | E8E7067427F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */; }; 64 | E8FC0F12281848880051E640 /* TypeErasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FC0F11281848880051E640 /* TypeErasure.swift */; }; 65 | E8FC0F13281848880051E640 /* TypeErasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FC0F11281848880051E640 /* TypeErasure.swift */; }; 66 | /* End PBXBuildFile section */ 67 | 68 | /* Begin PBXFileReference section */ 69 | E80050192816B00E00E4805C /* DebugMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMode.swift; sourceTree = ""; }; 70 | E800501C2816B09A00E4805C /* DebugOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOverlay.swift; sourceTree = ""; }; 71 | E800501F2816C40600E4805C /* AutoPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoPresentation.swift; sourceTree = ""; }; 72 | E80050222816EE1D00E4805C /* HierarchyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HierarchyView.swift; sourceTree = ""; }; 73 | E80050252816EE3400E4805C /* ViewControllerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerInfo.swift; sourceTree = ""; }; 74 | E8283B422819629E00467F10 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 75 | E836E59E280EEA0A0001B85E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 76 | E836E5AC280EECD50001B85E /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; 77 | E83ADA652814267600D98D82 /* Containment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Containment.swift; sourceTree = ""; }; 78 | E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeMismatchInfoView.swift; sourceTree = ""; }; 79 | E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 80 | E88DCE6F2812C83C00CD5203 /* ReExports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReExports.swift; sourceTree = ""; }; 81 | E88DCE722812CEE700CD5203 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 82 | E88DCE752812D10000CD5203 /* ViewControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerView.swift; sourceTree = ""; }; 83 | E88DCE792812D1B900CD5203 /* DefaultDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDescription.swift; sourceTree = ""; }; 84 | E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerStorage.swift; sourceTree = ""; }; 85 | E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = ""; }; 86 | E88DCE822812DD8B00CD5203 /* Title.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Title.swift; sourceTree = ""; }; 87 | E88DCE852812DF8200CD5203 /* RepresentedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepresentedObject.swift; sourceTree = ""; }; 88 | E88DCE882812E9CD00CD5203 /* Presentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentation.swift; sourceTree = ""; }; 89 | E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyViewController.swift; sourceTree = ""; }; 90 | E88DCE912812FA7A00CD5203 /* _ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _ViewController.swift; sourceTree = ""; }; 91 | E88DCE9A28130B8300CD5203 /* RenderContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderContentView.swift; sourceTree = ""; }; 92 | E8BA6EBA281588D800FA6C5C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 93 | E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerPresentation.swift; sourceTree = ""; }; 94 | E8BA6EC22815956200FA6C5C /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 95 | E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libViewController-macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 96 | E8E7065627F1EEA600F50160 /* AppKitBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppKitBase.xcconfig; sourceTree = ""; }; 97 | E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MobileStaticLib.xcconfig; sourceTree = ""; }; 98 | E8E7065827F1EEA600F50160 /* UIKitBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UIKitBase.xcconfig; sourceTree = ""; }; 99 | E8E7065927F1EEA600F50160 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 100 | E8E7065A27F1EEA600F50160 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 101 | E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MacStaticLib.xcconfig; sourceTree = ""; }; 102 | E8E7065C27F1EEA600F50160 /* StaticLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = StaticLib.xcconfig; sourceTree = ""; }; 103 | E8E7065D27F1EEA600F50160 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; 104 | E8E7066527F1EF4300F50160 /* libViewController-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libViewController-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 105 | E8E7066827F2014F00F50160 /* PresentationMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationMode.swift; sourceTree = ""; }; 106 | E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerEnvironment.swift; sourceTree = ""; }; 107 | E8FC0F11281848880051E640 /* TypeErasure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeErasure.swift; sourceTree = ""; }; 108 | /* End PBXFileReference section */ 109 | 110 | /* Begin PBXFrameworksBuildPhase section */ 111 | E8E7064A27F1EE4700F50160 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | E8E7066127F1EF4300F50160 /* Frameworks */ = { 119 | isa = PBXFrameworksBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXFrameworksBuildPhase section */ 126 | 127 | /* Begin PBXGroup section */ 128 | E80050182816AFF000E4805C /* Debugging */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | E80050192816B00E00E4805C /* DebugMode.swift */, 132 | E800501C2816B09A00E4805C /* DebugOverlay.swift */, 133 | E83ADA77281444B000D98D82 /* TypeMismatchInfoView.swift */, 134 | E80050222816EE1D00E4805C /* HierarchyView.swift */, 135 | E80050252816EE3400E4805C /* ViewControllerInfo.swift */, 136 | ); 137 | path = Debugging; 138 | sourceTree = ""; 139 | }; 140 | E836E59D280EE9F40001B85E /* Presentations */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | E88DCE882812E9CD00CD5203 /* Presentation.swift */, 144 | E8BA6EBE2815914F00FA6C5C /* ViewControllerPresentation.swift */, 145 | E8E7066827F2014F00F50160 /* PresentationMode.swift */, 146 | E800501F2816C40600E4805C /* AutoPresentation.swift */, 147 | ); 148 | path = Presentations; 149 | sourceTree = ""; 150 | }; 151 | E836E5AB280EECC40001B85E /* NavigationLink */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | E836E5AC280EECD50001B85E /* PushLink.swift */, 155 | ); 156 | path = NavigationLink; 157 | sourceTree = ""; 158 | }; 159 | E8BA6EBD2815912300FA6C5C /* ViewController */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | E88DCE912812FA7A00CD5203 /* _ViewController.swift */, 163 | E88DCE7C2812D3A300CD5203 /* ViewControllerStorage.swift */, 164 | E88DCE792812D1B900CD5203 /* DefaultDescription.swift */, 165 | E88DCE7F2812D6A700CD5203 /* Subscriptions.swift */, 166 | E88DCE822812DD8B00CD5203 /* Title.swift */, 167 | E88DCE852812DF8200CD5203 /* RepresentedObject.swift */, 168 | E83ADA652814267600D98D82 /* Containment.swift */, 169 | E8FC0F11281848880051E640 /* TypeErasure.swift */, 170 | E8283B422819629E00467F10 /* ContentView.swift */, 171 | ); 172 | path = ViewController; 173 | sourceTree = ""; 174 | }; 175 | E8BA6EC12815954300FA6C5C /* ContainerViewControllers */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | E8BA6EC22815956200FA6C5C /* NavigationController.swift */, 179 | ); 180 | path = ContainerViewControllers; 181 | sourceTree = ""; 182 | }; 183 | E8E7064327F1EE4700F50160 = { 184 | isa = PBXGroup; 185 | children = ( 186 | E836E59E280EEA0A0001B85E /* README.md */, 187 | E8E7065327F1EEA100F50160 /* Sources */, 188 | E8E7065527F1EEA600F50160 /* xcconfig */, 189 | E8E7064D27F1EE4700F50160 /* Products */, 190 | ); 191 | sourceTree = ""; 192 | }; 193 | E8E7064D27F1EE4700F50160 /* Products */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */, 197 | E8E7066527F1EF4300F50160 /* libViewController-iOS.a */, 198 | ); 199 | name = Products; 200 | sourceTree = ""; 201 | }; 202 | E8E7065327F1EEA100F50160 /* Sources */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | E8E7065427F1EEA100F50160 /* ViewController */, 206 | ); 207 | path = Sources; 208 | sourceTree = ""; 209 | }; 210 | E8E7065427F1EEA100F50160 /* ViewController */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | E88DCE722812CEE700CD5203 /* ViewController.swift */, 214 | E88DCE8E2812EAF800CD5203 /* AnyViewController.swift */, 215 | E8BA6EBD2815912300FA6C5C /* ViewController */, 216 | E836E59D280EE9F40001B85E /* Presentations */, 217 | E836E5AB280EECC40001B85E /* NavigationLink */, 218 | E8BA6EC12815954300FA6C5C /* ContainerViewControllers */, 219 | E80050182816AFF000E4805C /* Debugging */, 220 | E8E7066A27F2014F00F50160 /* ViewControllerEnvironment.swift */, 221 | E88DCE752812D10000CD5203 /* ViewControllerView.swift */, 222 | E88DCE6A2812C4CF00CD5203 /* MainViewController.swift */, 223 | E88DCE9A28130B8300CD5203 /* RenderContentView.swift */, 224 | E8BA6EBA281588D800FA6C5C /* Logger.swift */, 225 | E88DCE6F2812C83C00CD5203 /* ReExports.swift */, 226 | ); 227 | path = ViewController; 228 | sourceTree = ""; 229 | }; 230 | E8E7065527F1EEA600F50160 /* xcconfig */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | E8E7065627F1EEA600F50160 /* AppKitBase.xcconfig */, 234 | E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */, 235 | E8E7065827F1EEA600F50160 /* UIKitBase.xcconfig */, 236 | E8E7065927F1EEA600F50160 /* Debug.xcconfig */, 237 | E8E7065A27F1EEA600F50160 /* Release.xcconfig */, 238 | E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */, 239 | E8E7065C27F1EEA600F50160 /* StaticLib.xcconfig */, 240 | E8E7065D27F1EEA600F50160 /* Base.xcconfig */, 241 | ); 242 | path = xcconfig; 243 | sourceTree = ""; 244 | }; 245 | /* End PBXGroup section */ 246 | 247 | /* Begin PBXHeadersBuildPhase section */ 248 | E8E7064827F1EE4700F50160 /* Headers */ = { 249 | isa = PBXHeadersBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | E8E7065F27F1EF4300F50160 /* Headers */ = { 256 | isa = PBXHeadersBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | /* End PBXHeadersBuildPhase section */ 263 | 264 | /* Begin PBXNativeTarget section */ 265 | E8E7064B27F1EE4700F50160 /* ViewController-macOS */ = { 266 | isa = PBXNativeTarget; 267 | buildConfigurationList = E8E7065027F1EE4700F50160 /* Build configuration list for PBXNativeTarget "ViewController-macOS" */; 268 | buildPhases = ( 269 | E8E7064827F1EE4700F50160 /* Headers */, 270 | E8E7064927F1EE4700F50160 /* Sources */, 271 | E8E7064A27F1EE4700F50160 /* Frameworks */, 272 | ); 273 | buildRules = ( 274 | ); 275 | dependencies = ( 276 | ); 277 | name = "ViewController-macOS"; 278 | productName = ViewController; 279 | productReference = E8E7064C27F1EE4700F50160 /* libViewController-macOS.a */; 280 | productType = "com.apple.product-type.library.static"; 281 | }; 282 | E8E7065E27F1EF4300F50160 /* ViewController-iOS */ = { 283 | isa = PBXNativeTarget; 284 | buildConfigurationList = E8E7066227F1EF4300F50160 /* Build configuration list for PBXNativeTarget "ViewController-iOS" */; 285 | buildPhases = ( 286 | E8E7065F27F1EF4300F50160 /* Headers */, 287 | E8E7066027F1EF4300F50160 /* Sources */, 288 | E8E7066127F1EF4300F50160 /* Frameworks */, 289 | ); 290 | buildRules = ( 291 | ); 292 | dependencies = ( 293 | ); 294 | name = "ViewController-iOS"; 295 | productName = ViewController; 296 | productReference = E8E7066527F1EF4300F50160 /* libViewController-iOS.a */; 297 | productType = "com.apple.product-type.library.static"; 298 | }; 299 | /* End PBXNativeTarget section */ 300 | 301 | /* Begin PBXProject section */ 302 | E8E7064427F1EE4700F50160 /* Project object */ = { 303 | isa = PBXProject; 304 | attributes = { 305 | BuildIndependentTargetsInParallel = 1; 306 | LastUpgradeCheck = 1320; 307 | TargetAttributes = { 308 | E8E7064B27F1EE4700F50160 = { 309 | CreatedOnToolsVersion = 13.2.1; 310 | LastSwiftMigration = 1320; 311 | }; 312 | E8E7065E27F1EF4300F50160 = { 313 | LastSwiftMigration = 1320; 314 | }; 315 | }; 316 | }; 317 | buildConfigurationList = E8E7064727F1EE4700F50160 /* Build configuration list for PBXProject "ViewController" */; 318 | compatibilityVersion = "Xcode 13.0"; 319 | developmentRegion = en; 320 | hasScannedForEncodings = 0; 321 | knownRegions = ( 322 | en, 323 | Base, 324 | ); 325 | mainGroup = E8E7064327F1EE4700F50160; 326 | productRefGroup = E8E7064D27F1EE4700F50160 /* Products */; 327 | projectDirPath = ""; 328 | projectRoot = ""; 329 | targets = ( 330 | E8E7064B27F1EE4700F50160 /* ViewController-macOS */, 331 | E8E7065E27F1EF4300F50160 /* ViewController-iOS */, 332 | ); 333 | }; 334 | /* End PBXProject section */ 335 | 336 | /* Begin PBXSourcesBuildPhase section */ 337 | E8E7064927F1EE4700F50160 /* Sources */ = { 338 | isa = PBXSourcesBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | E8BA6EC32815956200FA6C5C /* NavigationController.swift in Sources */, 342 | E8E7067327F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */, 343 | E88DCE762812D10000CD5203 /* ViewControllerView.swift in Sources */, 344 | E80050232816EE1D00E4805C /* HierarchyView.swift in Sources */, 345 | E88DCE6B2812C4CF00CD5203 /* MainViewController.swift in Sources */, 346 | E88DCE802812D6A700CD5203 /* Subscriptions.swift in Sources */, 347 | E8BA6EBF2815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */, 348 | E88DCE702812C83C00CD5203 /* ReExports.swift in Sources */, 349 | E83ADA662814267600D98D82 /* Containment.swift in Sources */, 350 | E800501D2816B09A00E4805C /* DebugOverlay.swift in Sources */, 351 | E83ADA78281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */, 352 | E8BA6EBB281588D800FA6C5C /* Logger.swift in Sources */, 353 | E88DCE7A2812D1B900CD5203 /* DefaultDescription.swift in Sources */, 354 | E80050262816EE3400E4805C /* ViewControllerInfo.swift in Sources */, 355 | E88DCE8F2812EAF800CD5203 /* AnyViewController.swift in Sources */, 356 | E8FC0F12281848880051E640 /* TypeErasure.swift in Sources */, 357 | E88DCE862812DF8200CD5203 /* RepresentedObject.swift in Sources */, 358 | E88DCE832812DD8B00CD5203 /* Title.swift in Sources */, 359 | E80050202816C40600E4805C /* AutoPresentation.swift in Sources */, 360 | E800501A2816B00E00E4805C /* DebugMode.swift in Sources */, 361 | E88DCE892812E9CD00CD5203 /* Presentation.swift in Sources */, 362 | E88DCE7D2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */, 363 | E8E7066F27F2014F00F50160 /* PresentationMode.swift in Sources */, 364 | E88DCE9B28130B8300CD5203 /* RenderContentView.swift in Sources */, 365 | E8283B432819629E00467F10 /* ContentView.swift in Sources */, 366 | E88DCE922812FA7A00CD5203 /* _ViewController.swift in Sources */, 367 | E836E5AD280EECD50001B85E /* PushLink.swift in Sources */, 368 | E88DCE732812CEE700CD5203 /* ViewController.swift in Sources */, 369 | ); 370 | runOnlyForDeploymentPostprocessing = 0; 371 | }; 372 | E8E7066027F1EF4300F50160 /* Sources */ = { 373 | isa = PBXSourcesBuildPhase; 374 | buildActionMask = 2147483647; 375 | files = ( 376 | E8BA6EC42815956200FA6C5C /* NavigationController.swift in Sources */, 377 | E8E7067427F2014F00F50160 /* ViewControllerEnvironment.swift in Sources */, 378 | E88DCE772812D10000CD5203 /* ViewControllerView.swift in Sources */, 379 | E80050242816EE1D00E4805C /* HierarchyView.swift in Sources */, 380 | E88DCE6C2812C4CF00CD5203 /* MainViewController.swift in Sources */, 381 | E88DCE812812D6A700CD5203 /* Subscriptions.swift in Sources */, 382 | E8BA6EC02815914F00FA6C5C /* ViewControllerPresentation.swift in Sources */, 383 | E88DCE712812C83C00CD5203 /* ReExports.swift in Sources */, 384 | E83ADA672814267600D98D82 /* Containment.swift in Sources */, 385 | E800501E2816B09A00E4805C /* DebugOverlay.swift in Sources */, 386 | E83ADA79281444B000D98D82 /* TypeMismatchInfoView.swift in Sources */, 387 | E8BA6EBC281588D800FA6C5C /* Logger.swift in Sources */, 388 | E88DCE7B2812D1B900CD5203 /* DefaultDescription.swift in Sources */, 389 | E80050272816EE3400E4805C /* ViewControllerInfo.swift in Sources */, 390 | E88DCE902812EAF800CD5203 /* AnyViewController.swift in Sources */, 391 | E8FC0F13281848880051E640 /* TypeErasure.swift in Sources */, 392 | E88DCE872812DF8200CD5203 /* RepresentedObject.swift in Sources */, 393 | E88DCE842812DD8B00CD5203 /* Title.swift in Sources */, 394 | E80050212816C40600E4805C /* AutoPresentation.swift in Sources */, 395 | E800501B2816B00E00E4805C /* DebugMode.swift in Sources */, 396 | E88DCE8A2812E9CD00CD5203 /* Presentation.swift in Sources */, 397 | E88DCE7E2812D3A300CD5203 /* ViewControllerStorage.swift in Sources */, 398 | E8E7067027F2014F00F50160 /* PresentationMode.swift in Sources */, 399 | E88DCE9C28130B8300CD5203 /* RenderContentView.swift in Sources */, 400 | E8283B442819629E00467F10 /* ContentView.swift in Sources */, 401 | E88DCE932812FA7A00CD5203 /* _ViewController.swift in Sources */, 402 | E836E5AE280EECD50001B85E /* PushLink.swift in Sources */, 403 | E88DCE742812CEE700CD5203 /* ViewController.swift in Sources */, 404 | ); 405 | runOnlyForDeploymentPostprocessing = 0; 406 | }; 407 | /* End PBXSourcesBuildPhase section */ 408 | 409 | /* Begin XCBuildConfiguration section */ 410 | E8E7064E27F1EE4700F50160 /* Debug */ = { 411 | isa = XCBuildConfiguration; 412 | baseConfigurationReference = E8E7065927F1EEA600F50160 /* Debug.xcconfig */; 413 | buildSettings = { 414 | MACOSX_DEPLOYMENT_TARGET = 11.0; 415 | SDKROOT = macosx; 416 | }; 417 | name = Debug; 418 | }; 419 | E8E7064F27F1EE4700F50160 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | baseConfigurationReference = E8E7065A27F1EEA600F50160 /* Release.xcconfig */; 422 | buildSettings = { 423 | MACOSX_DEPLOYMENT_TARGET = 11.0; 424 | SDKROOT = macosx; 425 | }; 426 | name = Release; 427 | }; 428 | E8E7065127F1EE4700F50160 /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | baseConfigurationReference = E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */; 431 | buildSettings = { 432 | CLANG_ENABLE_MODULES = YES; 433 | CODE_SIGN_STYLE = Automatic; 434 | DEVELOPMENT_TEAM = 4GXF3JAMM4; 435 | LD_RUNPATH_SEARCH_PATHS = ( 436 | "$(inherited)", 437 | "@executable_path/../Frameworks", 438 | "@loader_path/../Frameworks", 439 | ); 440 | PRODUCT_MODULE_NAME = ViewController; 441 | PRODUCT_NAME = "$(TARGET_NAME)"; 442 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 443 | SWIFT_VERSION = 5.0; 444 | }; 445 | name = Debug; 446 | }; 447 | E8E7065227F1EE4700F50160 /* Release */ = { 448 | isa = XCBuildConfiguration; 449 | baseConfigurationReference = E8E7065B27F1EEA600F50160 /* MacStaticLib.xcconfig */; 450 | buildSettings = { 451 | CLANG_ENABLE_MODULES = YES; 452 | CODE_SIGN_STYLE = Automatic; 453 | DEVELOPMENT_TEAM = 4GXF3JAMM4; 454 | LD_RUNPATH_SEARCH_PATHS = ( 455 | "$(inherited)", 456 | "@executable_path/../Frameworks", 457 | "@loader_path/../Frameworks", 458 | ); 459 | PRODUCT_MODULE_NAME = ViewController; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | SWIFT_VERSION = 5.0; 462 | }; 463 | name = Release; 464 | }; 465 | E8E7066327F1EF4300F50160 /* Debug */ = { 466 | isa = XCBuildConfiguration; 467 | baseConfigurationReference = E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */; 468 | buildSettings = { 469 | CLANG_ENABLE_MODULES = YES; 470 | CODE_SIGN_STYLE = Automatic; 471 | DEVELOPMENT_TEAM = 4GXF3JAMM4; 472 | LD_RUNPATH_SEARCH_PATHS = ( 473 | "$(inherited)", 474 | "@executable_path/Frameworks", 475 | "@loader_path/Frameworks", 476 | ); 477 | PRODUCT_MODULE_NAME = ViewController; 478 | PRODUCT_NAME = "$(TARGET_NAME)"; 479 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 480 | SWIFT_VERSION = 5.0; 481 | }; 482 | name = Debug; 483 | }; 484 | E8E7066427F1EF4300F50160 /* Release */ = { 485 | isa = XCBuildConfiguration; 486 | baseConfigurationReference = E8E7065727F1EEA600F50160 /* MobileStaticLib.xcconfig */; 487 | buildSettings = { 488 | CLANG_ENABLE_MODULES = YES; 489 | CODE_SIGN_STYLE = Automatic; 490 | DEVELOPMENT_TEAM = 4GXF3JAMM4; 491 | LD_RUNPATH_SEARCH_PATHS = ( 492 | "$(inherited)", 493 | "@executable_path/Frameworks", 494 | "@loader_path/Frameworks", 495 | ); 496 | PRODUCT_MODULE_NAME = ViewController; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | SWIFT_VERSION = 5.0; 499 | }; 500 | name = Release; 501 | }; 502 | /* End XCBuildConfiguration section */ 503 | 504 | /* Begin XCConfigurationList section */ 505 | E8E7064727F1EE4700F50160 /* Build configuration list for PBXProject "ViewController" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | E8E7064E27F1EE4700F50160 /* Debug */, 509 | E8E7064F27F1EE4700F50160 /* Release */, 510 | ); 511 | defaultConfigurationIsVisible = 0; 512 | defaultConfigurationName = Release; 513 | }; 514 | E8E7065027F1EE4700F50160 /* Build configuration list for PBXNativeTarget "ViewController-macOS" */ = { 515 | isa = XCConfigurationList; 516 | buildConfigurations = ( 517 | E8E7065127F1EE4700F50160 /* Debug */, 518 | E8E7065227F1EE4700F50160 /* Release */, 519 | ); 520 | defaultConfigurationIsVisible = 0; 521 | defaultConfigurationName = Release; 522 | }; 523 | E8E7066227F1EF4300F50160 /* Build configuration list for PBXNativeTarget "ViewController-iOS" */ = { 524 | isa = XCConfigurationList; 525 | buildConfigurations = ( 526 | E8E7066327F1EF4300F50160 /* Debug */, 527 | E8E7066427F1EF4300F50160 /* Release */, 528 | ); 529 | defaultConfigurationIsVisible = 0; 530 | defaultConfigurationName = Release; 531 | }; 532 | /* End XCConfigurationList section */ 533 | }; 534 | rootObject = E8E7064427F1EE4700F50160 /* Project object */; 535 | } 536 | -------------------------------------------------------------------------------- /ViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /ViewController.xcodeproj/xcshareddata/xcschemes/ViewController-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /xcconfig/AppKitBase.xcconfig: -------------------------------------------------------------------------------- 1 | // Base config file for Mac projects 2 | 3 | #include "Base.xcconfig" 4 | 5 | MACOSX_DEPLOYMENT_TARGET = 11 6 | SDKROOT = macosx 7 | 8 | -------------------------------------------------------------------------------- /xcconfig/Base.xcconfig: -------------------------------------------------------------------------------- 1 | // Base config 2 | 3 | // Signing 4 | CODE_SIGN_IDENTITY = - 5 | 6 | // Include 7 | ALWAYS_SEARCH_USER_PATHS = NO 8 | 9 | // Language 10 | GCC_C_LANGUAGE_STANDARD = gnu99 11 | GCC_NO_COMMON_BLOCKS = YES 12 | CLANG_ENABLE_MODULES = YES 13 | CLANG_ENABLE_OBJC_ARC = YES 14 | ENABLE_STRICT_OBJC_MSGSEND = YES 15 | CLANG_ENABLE_OBJC_WEAK = YES 16 | 17 | // Warnings 18 | CLANG_WARN_BOOL_CONVERSION = YES 19 | CLANG_WARN_CONSTANT_CONVERSION = YES 20 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 21 | CLANG_WARN_EMPTY_BODY = YES 22 | CLANG_WARN_ENUM_CONVERSION = YES 23 | CLANG_WARN_INT_CONVERSION = YES 24 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 25 | CLANG_WARN_UNREACHABLE_CODE = YES 26 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 27 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 28 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 29 | GCC_WARN_UNDECLARED_SELECTOR = YES 30 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 31 | GCC_WARN_UNUSED_FUNCTION = YES 32 | GCC_WARN_UNUSED_VARIABLE = YES 33 | 34 | CLANG_ANALYZER_NONNULL = YES 35 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 36 | CLANG_WARN_INFINITE_RECURSION = YES 37 | CLANG_WARN_SUSPICIOUS_MOVE = YES 38 | 39 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 40 | CLANG_WARN_COMMA = YES 41 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 42 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 43 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 44 | CLANG_WARN_STRICT_PROTOTYPES = YES 45 | 46 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 47 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 48 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES 49 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 50 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 51 | 52 | CLANG_CXX_LANGUAGE_STANDARD = gnu++17 53 | CLANG_CXX_LIBRARY = libc++ 54 | 55 | 56 | MTL_FAST_MATH = YES 57 | COMBINE_HIDPI_IMAGES = YES 58 | 59 | // For embedded frameworks. 60 | LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks 61 | LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 62 | LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) @executable_path/Frameworks @loader_path/Frameworks 63 | 64 | OTHER_LDFLAGS = $(inherited) -ObjC // to import categories in static libs 65 | 66 | OTHER_SWIFT_FLAGS = -DXcode 67 | 68 | // This is a little lame (Xcode shouldn't tie the code to a version ..) 69 | SWIFT_VERSION = 5.0 70 | -------------------------------------------------------------------------------- /xcconfig/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // Extra debug settings are always the same .. 2 | 3 | #include "Base.xcconfig" 4 | 5 | ONLY_ACTIVE_ARCH = YES 6 | 7 | // Debugging 8 | DEBUG_INFORMATION_FORMAT = dwarf 9 | ENABLE_TESTABILITY = YES 10 | COPY_PHASE_STRIP = NO 11 | GCC_OPTIMIZATION_LEVEL = 0 12 | ENABLE_NS_ASSERTIONS = YES 13 | GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 14 | MTL_ENABLE_DEBUG_INFO = YES 15 | GCC_DYNAMIC_NO_PIC = NO 16 | 17 | // Swift 18 | SWIFT_OPTIMIZATION_LEVEL = -Onone 19 | 20 | // -DXcode is set in Base, but this one seems to override it 21 | // OTHER_SWIFT_FLAGS[config=Debug] = $(inherited) -DDEBUG -DXcode 22 | OTHER_SWIFT_FLAGS = $(inherited) -DDEBUG -DXcode 23 | 24 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG 25 | -------------------------------------------------------------------------------- /xcconfig/MacStaticLib.xcconfig: -------------------------------------------------------------------------------- 1 | // Mac Static Lib 2 | 3 | #include "AppKitBase.xcconfig" 4 | #include "StaticLib.xcconfig" 5 | -------------------------------------------------------------------------------- /xcconfig/MobileStaticLib.xcconfig: -------------------------------------------------------------------------------- 1 | // UIKit Static Lib 2 | 3 | #include "UIKitBase.xcconfig" 4 | #include "StaticLib.xcconfig" 5 | 6 | -------------------------------------------------------------------------------- /xcconfig/Release.xcconfig: -------------------------------------------------------------------------------- 1 | // Platform agnostic release settings 2 | 3 | #include "Base.xcconfig" 4 | 5 | // Optimization 6 | GCC_OPTIMIZATION_LEVEL = s 7 | 8 | // Debugging 9 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 10 | ENABLE_TESTABILITY = YES 11 | COPY_PHASE_STRIP = YES 12 | ENABLE_NS_ASSERTIONS = NO 13 | MTL_ENABLE_DEBUG_INFO = NO 14 | 15 | GCC_PREPROCESSOR_DEFINITIONS = RELEASE=1 16 | 17 | SWIFT_OPTIMIZATION_LEVEL = -O 18 | SWIFT_COMPILATION_MODE = wholemodule 19 | -------------------------------------------------------------------------------- /xcconfig/StaticLib.xcconfig: -------------------------------------------------------------------------------- 1 | // Static Library for any platform 2 | 3 | EXECUTABLE_PREFIX = lib 4 | 5 | GCC_DYNAMIC_NO_PIC = NO 6 | 7 | DEFINES_MODULE = YES 8 | 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /xcconfig/UIKitBase.xcconfig: -------------------------------------------------------------------------------- 1 | // Base config for Phone projects 2 | 3 | #include "Base.xcconfig" 4 | 5 | IPHONEOS_DEPLOYMENT_TARGET = 14.0 6 | SDKROOT = iphoneos 7 | --------------------------------------------------------------------------------