From 386a67593ed15baaeff2cbd8857bca9f89a645e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sat, 30 Apr 2022 15:38:59 +0200 Subject: [PATCH 1/6] Fix presentation hierarchy in debug view Was ordered the wrong way around. --- Sources/ViewController/Debugging/ViewControllerInfo.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ViewController/Debugging/ViewControllerInfo.swift b/Sources/ViewController/Debugging/ViewControllerInfo.swift index 5b04d81..e6fd857 100644 --- a/Sources/ViewController/Debugging/ViewControllerInfo.swift +++ b/Sources/ViewController/Debugging/ViewControllerInfo.swift @@ -44,6 +44,7 @@ struct ViewControllerInfo: View { let toRoot = sequence(first: viewController) { $0.presentingViewController } + .reversed() if let presented = viewController.presentedViewController { let downwards = sequence(first: presented) { $0.presentedViewController From 4e5a37bae88923583828fad0211968c41381e7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 May 2022 14:30:38 +0200 Subject: [PATCH 2/6] Derive loglevel from LOGLEVEL env variable DEBUG only, otherwise os_log is being used. Available levels: - error, debug, fault, info Non-matches default to OSLogType.default. --- Sources/ViewController/Logger.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/ViewController/Logger.swift b/Sources/ViewController/Logger.swift index 4608420..87fa57d 100644 --- a/Sources/ViewController/Logger.swift +++ b/Sources/ViewController/Logger.swift @@ -14,11 +14,20 @@ import os @usableFromInline struct PrintLogger { - - let level : OSLogType = .error // TODO: derive from LOGLEVEL env variable + static let logLevel : OSLogType = { + switch ProcessInfo.processInfo.environment["LOGLEVEL"]?.lowercased() { + case "error" : return .error + case "debug" : return .debug + case "fault" : return .fault + case "info" : return .info + default: return OSLogType.default + } + }() + var logLevel : OSLogType { Self.logLevel } + func log(_ level: OSLogType, _ prefix: String, _ message: () -> String) { - guard level.rawValue >= self.level.rawValue else { return } + guard level.rawValue >= self.logLevel.rawValue else { return } print(prefix + message()) } From e1f69d9c5e358d487db320561251d11d376673d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 May 2022 15:15:47 +0200 Subject: [PATCH 3/6] Improve debug logs and description Shorten the description. Show the active VC in the debug panel. --- .../Debugging/HierarchyView.swift | 15 +++++++++-- .../Debugging/ViewControllerInfo.swift | 12 +++++---- .../Presentations/Presentation.swift | 18 ++++++++++--- .../ViewController/DefaultDescription.swift | 25 +++++++++++++++---- .../ViewController/ViewControllerView.swift | 4 +-- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/Sources/ViewController/Debugging/HierarchyView.swift b/Sources/ViewController/Debugging/HierarchyView.swift index fa942f5..e3d1533 100644 --- a/Sources/ViewController/Debugging/HierarchyView.swift +++ b/Sources/ViewController/Debugging/HierarchyView.swift @@ -13,6 +13,11 @@ struct HierarchyView: View { let title : String let controllers : [ _ViewController ] + let active : _ViewController + + private func titleForController(_ vc: _ViewController) -> String { + "\(vc.typeName)[\(vc.oidString)]" + } var body: some View { if controllers.count > 1 { @@ -24,11 +29,17 @@ struct HierarchyView: View { VStack(alignment: .leading, spacing: 12) { // Don't do this at home ForEach(Array(zip(controllers.indices, controllers)), id: \.0) { - ( idx, parent ) in + ( idx, vc ) in HStack(alignment: .firstTextBaseline) { Text("\(idx)") - Text(verbatim: "\(parent)") + Text(verbatim: titleForController(vc)) } + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder() + .padding(-8) + .opacity(vc === active ? 1.0 : 0.0) + ) } } .padding(.horizontal) diff --git a/Sources/ViewController/Debugging/ViewControllerInfo.swift b/Sources/ViewController/Debugging/ViewControllerInfo.swift index e6fd857..285ac32 100644 --- a/Sources/ViewController/Debugging/ViewControllerInfo.swift +++ b/Sources/ViewController/Debugging/ViewControllerInfo.swift @@ -67,7 +67,7 @@ struct ViewControllerInfo: View { return "AnyVC: " + addressString } else { - return String(describing: type(of: viewController)) + return viewController.typeName } } @@ -112,10 +112,12 @@ struct ViewControllerInfo: View { } .padding() - HierarchyView(title: "Parent Hierarchy", - controllers: parentHierarchy) - HierarchyView(title: "Presentation Hierarchy", - controllers: presentationHierarchy) + HierarchyView(title : "Parent Hierarchy", + controllers : parentHierarchy, + active : viewController) + HierarchyView(title : "Presentation Hierarchy", + controllers : presentationHierarchy, + active : viewController) Spacer() } diff --git a/Sources/ViewController/Presentations/Presentation.swift b/Sources/ViewController/Presentations/Presentation.swift index 3801cb5..d5ae363 100644 --- a/Sources/ViewController/Presentations/Presentation.swift +++ b/Sources/ViewController/Presentations/Presentation.swift @@ -116,7 +116,7 @@ public extension _ViewController { guard let presentation = self.activePresentation(for: mode) else { // This is fine, could have happened by other means. - return logger.debug("Did not find VC to deactivate: \(self)") + return logger.debug("Did not find VC to deactivate in: \(self)") } assert(mode != .automatic) @@ -154,7 +154,14 @@ public extension _ViewController { // Only during presentation, we may need to dismiss the other type! Binding( get: { - self.activePresentations.contains(where: { $0.mode == mode }) + if self.activePresentations.contains(where: { $0.mode == mode }) { + logger.debug("Is presenting in mode \(mode): \(self)") + return true + } + else { + logger.debug("Not presenting in mode \(mode): \(self)") + return false + } }, set: { isShowing in // We cannot make VCs "appear", that would require a factory. @@ -178,7 +185,8 @@ public extension _ViewController { guard let presentation = self.activePresentation(for: mode) else { // This is fine, could have happened by other means. - return logger.debug("did not find VC to deactivate \(self)?") + return logger.debug( + "did not find VC for mode \(mode) to deactivate in: \(self)?") } /// If a mode was requested, make sure it is the right one. @@ -262,6 +270,8 @@ public extension _ViewController { where VC.ContentView == DefaultViewControllerView { // Requires a custom `PushPresentation` or `SheetPresentation` + logger.debug( + "Presenting(.custom) a VC w/o an explicit ContentView: \(viewController)") defaultPresent(viewController, mode: .custom) } @@ -298,6 +308,8 @@ public extension _ViewController { if VC.ContentView.self == DefaultViewControllerView.self { // Requires an explicit ``PushPresentation`` or ``SheetPresentation`` in // the associated `View`. + logger.debug( + "modalPresentationMode(.custom) for custom VC: \(viewController)") return .custom } diff --git a/Sources/ViewController/ViewController/DefaultDescription.swift b/Sources/ViewController/ViewController/DefaultDescription.swift index cf70065..1c627d5 100644 --- a/Sources/ViewController/ViewController/DefaultDescription.swift +++ b/Sources/ViewController/ViewController/DefaultDescription.swift @@ -6,12 +6,19 @@ // Copyright © 2022 ZeeZide GmbH. All rights reserved. // +extension _ViewController { + + internal var oidString : String { + String(UInt(bitPattern: ObjectIdentifier(self)), radix: 16) + } + + internal var typeName : String { String(describing: type(of: self)) } +} + extension ViewController { // MARK: - Description - @inlinable public var description: String { - let addr = String(UInt(bitPattern: ObjectIdentifier(self)), radix: 16) - var ms = "<\(type(of: self))[\(addr)]:" + var ms = "<\(typeName)[\(oidString)]:" appendAttributes(to: &ms) ms += ">" return ms @@ -22,14 +29,22 @@ extension ViewController { // MARK: - Description defaultAppendAttributes(to: &description) } - @inlinable public func defaultAppendAttributes(to description: inout String) { // public, so that subclasses can call this "super" implementation! if let v = title { description += " '\(v)'" } assert(self !== presentedViewController) if activePresentations.count == 1, let v = activePresentations.first { - description += " presenting=\(v.viewController)[\(v.mode)]" + let vc = "\(v.viewController.typeName)[\(v.viewController.oidString)]" + switch v.mode { + case .automatic: + assertionFailure("Unexpected presentation mode: \(v)") + description += " presenting[AUTO!!]=\(vc)" + case .custom : description += " presenting[CUSTOM]=\(vc)" + case .sheet : description += " presenting[sheet]=\(vc)" + case .navigation : description += " presenting[nav]=\(vc)" + case .pushLink : description += " presenting[link]=\(vc)" + } } else if !activePresentations.isEmpty { description += " presenting=#\(activePresentations.count)" diff --git a/Sources/ViewController/ViewControllerView.swift b/Sources/ViewController/ViewControllerView.swift index 5246f3f..3907c69 100644 --- a/Sources/ViewController/ViewControllerView.swift +++ b/Sources/ViewController/ViewControllerView.swift @@ -39,8 +39,8 @@ extension EmptyView: ViewControllerView {} Label("Missing VC View", systemImage: "questionmark.circle") Spacer() if let viewController = viewController { - Text(verbatim: "Class: \(type(of: viewController))") - Text(verbatim: "ID: \(ObjectIdentifier(viewController))") + Text(verbatim: "Class: \(viewController.typeName)") + Text(verbatim: "ID: \(viewController.oidString)") Text(verbatim: "\(viewController)") } else { From 3a9cb434644709a7a65f588ea5f9777ec94982c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 May 2022 15:54:38 +0200 Subject: [PATCH 4/6] NavigationController: Support navigationViewStyle's Funny how things need to be re-added :-) --- .../NavigationController.swift | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Sources/ViewController/ContainerViewControllers/NavigationController.swift b/Sources/ViewController/ContainerViewControllers/NavigationController.swift index 07216e7..18a8961 100644 --- a/Sources/ViewController/ContainerViewControllers/NavigationController.swift +++ b/Sources/ViewController/ContainerViewControllers/NavigationController.swift @@ -137,7 +137,15 @@ open class NavigationController: ViewController, _NavigationController public let _rootViewController : RootVC public var rootViewController : _ViewController { _rootViewController } - + + public enum NavigationViewStyle: Equatable { + case automatic + case stack + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + case columns + } + @Published public var navigationViewStyle = NavigationViewStyle.automatic + public init(rootViewController: RootVC) { self._rootViewController = rootViewController markAsPresentingViewController() @@ -161,13 +169,28 @@ open class NavigationController: ViewController, _NavigationController // MARK: - View - public var view: some View { + private var _view: some View { NavigationView { _rootViewController.view .controlled(by: _rootViewController) .navigationTitle(_rootViewController.navigationTitle) } } + public var view: some View { + switch navigationViewStyle { + case .automatic : + _view + case .stack : + _view.navigationViewStyle(.stack) + case .columns : + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + _view.navigationViewStyle(.columns) + } + else { + _view + } + } + } } public extension AnyViewController { From 6907cec7234a84a3c06016479857dc0aefa43828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 May 2022 16:14:03 +0200 Subject: [PATCH 5/6] NavigationController: Fix macOS compilation macOS doesn't have the stack style. --- .../NavigationController.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/ViewController/ContainerViewControllers/NavigationController.swift b/Sources/ViewController/ContainerViewControllers/NavigationController.swift index 18a8961..e47998f 100644 --- a/Sources/ViewController/ContainerViewControllers/NavigationController.swift +++ b/Sources/ViewController/ContainerViewControllers/NavigationController.swift @@ -140,7 +140,11 @@ open class NavigationController: ViewController, _NavigationController public enum NavigationViewStyle: Equatable { case automatic + + @available(iOS 13.0, tvOS 13.0, watchOS 7.0, *) + @available(macOS, unavailable) case stack + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) case columns } @@ -181,7 +185,11 @@ open class NavigationController: ViewController, _NavigationController case .automatic : _view case .stack : - _view.navigationViewStyle(.stack) + #if os(macOS) + _view + #else + _view.navigationViewStyle(.stack) + #endif case .columns : if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { _view.navigationViewStyle(.columns) From b151f526166c988cb4d1a70ba7c04da233d0f822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 May 2022 16:18:27 +0200 Subject: [PATCH 6/6] Fix dismissing a sheet (issue #6) When dismissing a sheet, the sheets ContentView sometimes already reflects that change in presentation while the View slides off screen. Showing an error. Now we keep a handle to the actual VC being presented. --- .../Presentations/AutoPresentation.swift | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Sources/ViewController/Presentations/AutoPresentation.swift b/Sources/ViewController/Presentations/AutoPresentation.swift index c2076d7..63e7684 100644 --- a/Sources/ViewController/Presentations/AutoPresentation.swift +++ b/Sources/ViewController/Presentations/AutoPresentation.swift @@ -26,19 +26,48 @@ internal struct AutoPresentationViewModifier: ViewModifier @ObservedObject var presentingViewController : VC let mode : ViewControllerPresentationMode - var body: some View { + // Keep a handle to the VC being presented. We do this to avoid issue #6, + // i.e. when a sheet is dismissed and transitions off-screen, the + // "presentedViewController" is already gone (dismissed). + // The `body` of this `Present` View would then evaluate to the + // `TypeMismatchInfoView` during the dismiss. + // So we keep the VC being presented around, to make sure we still have a + // handle for the content-view while it is being dismissed. + @State private var viewController : _ViewController? + + private var activeVC: _ViewController? { + if let activeVC = viewController { return activeVC } + if let presentation = - presentingViewController.activePresentation(for: mode) + presentingViewController.activePresentation(for: mode) { - let presentedViewController = presentation.viewController + // Note: Do not modify `@State` in here! (i.e. do not push to the + // `viewController` variable as part of the evaluation) + // This happens if the VC is getting presented. + return presentation.viewController + } + + return nil + } + + var body: some View { + if let presentedViewController = activeVC { presentedViewController.anyControlledContentView .environment(\.viewControllerPresentationMode, mode) .navigationTitle(presentedViewController.navigationTitle) + .onAppear { + viewController = presentedViewController + } + .onDisappear { // This seems to be a proper onDidDisappear + viewController = nil + } } else { - TypeMismatchInfoView( - parent: presentingViewController, expectedMode: mode - ) + #if DEBUG + TypeMismatchInfoView( + parent: presentingViewController, expectedMode: mode + ) + #endif } } }