From ace502add33b64ff34cd269b2929875ee615ed27 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Thu, 10 Nov 2022 01:34:19 +0000 Subject: [PATCH] Deprecate split view modifiers (#29) After some experimentation I came to the conclusion that split view "transitions" are not meant to be modified directly. They're not even a thing in itself, really. Instead, it should be the stack navigation view *within* the split view that can and should have their transition modified directly. So I deprecated all the split view modifiers and flattened the 2 stack modifiers into one: `.navigationTransition`. In addition to this, a useful runtime warning will let you know when you're holding it wrong. This drastically simplifies the API surface to a single entry point and is, I believe, a step in the right direction to easing the entry barrier for this library. --- Demo/Demo/Pages.swift | 10 +- Demo/Demo/RootView.swift | 2 +- README.md | 36 +-- .../NavigationTransition+SwiftUI.swift | 285 ++++-------------- 4 files changed, 81 insertions(+), 252 deletions(-) diff --git a/Demo/Demo/Pages.swift b/Demo/Demo/Pages.swift index 52a1fc76..e69dce7a 100644 --- a/Demo/Demo/Pages.swift +++ b/Demo/Demo/Pages.swift @@ -35,13 +35,13 @@ struct PageOne: View { struct PageTwo: View { var body: some View { let content = Group { - Text("The library is fully compatible with **NavigationView** in iOS 13+, and the new **NavigationStack** and **NavigationSplitView** in iOS 16.") + Text("The library is fully compatible with **NavigationView** in iOS 13+, and the new **NavigationStack** in iOS 16.") Text("In fact, that entire transition you just saw can be implemented in **one line** of SwiftUI code:") Code(""" - NavigationView { + NavigationStack { ... } - .navigationViewStackTransition(.slide) + .navigationTransition(.slide) """ ) } @@ -62,7 +62,7 @@ struct PageThree: View { Text("The API is designed to resemble that of built-in SwiftUI Transitions for maximum **familiarity** and **ease of use**.") Text("You can apply **custom animations** just like with standard SwiftUI transitions:") Code(""" - .navigationViewStackTransition( + .navigationTransition( .fade(.in).animation( .easeInOut(duration: 0.3) ) @@ -71,7 +71,7 @@ struct PageThree: View { ) Text("... and you can even **combine** them too:") Code(""" - .navigationViewStackTransition( + .navigationTransition( .slide.combined(with: .fade(.in)) ) """ diff --git a/Demo/Demo/RootView.swift b/Demo/Demo/RootView.swift index 9abc8d4e..9cb47d3b 100644 --- a/Demo/Demo/RootView.swift +++ b/Demo/Demo/RootView.swift @@ -17,7 +17,7 @@ struct RootView: View { .navigationViewStyle(.stack) } } - .navigationViewStackTransition( + .navigationTransition( appState.transition().animation(appState.animation()), interactivity: appState.interactivity() ) diff --git a/README.md b/README.md index c8acb302..224783e4 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,16 @@

- **NavigationTransitions** is a library that integrates seamlessly with SwiftUI's **Navigation** views, allowing complete customization over **push and pop transitions**! The library is fully compatible with: -- **`NavigationView`** (iOS 13+) -- **`NavigationStack`** & **`NavigationSplitView`** (iOS 16) +- **`NavigationView`** (iOS 13, 14, 15) +- **`NavigationStack`** (iOS 16) ## Overview -Instead of reinventing entire navigation components in order to customize its transitions, `NavigationTransitions` ships as a simple set of 2 modifiers that can be applied directly to SwiftUI's very own first-party navigation components. +Instead of reinventing entire navigation components in order to customize its transitions, `NavigationTransitions` ships with a simple modifier that can be applied directly to SwiftUI's very own first-party navigation component. ### The Basics @@ -28,15 +27,7 @@ NavigationView { // ... } .navigationViewStyle(.stack) -.navigationViewStackTransition(.slide) -``` - -```swift -NavigationView { - // ... -} -.navigationViewStyle(.columns) -.navigationViewColumnTransition(.slide, forColumns: .all) +.navigationTransition(.slide) ``` #### iOS 16 @@ -45,14 +36,7 @@ NavigationView { NavigationStack { // ... } -.navigationStackTransition(.slide) -``` - -```swift -NavigationSplitView { - // ... -} -.navigationSplitViewTransition(.slide, forColumns: .all) +.navigationTransition(.slide) ``` --- @@ -62,7 +46,7 @@ The API is designed to resemble that of built-in SwiftUI Transitions for maximum You can apply **custom animations** just like with standard SwiftUI transitions: ```swift -.navigationViewStackTransition( +.navigationTransition( .fade(.in).animation(.easeInOut(duration: 0.3)) ) ``` @@ -70,7 +54,7 @@ You can apply **custom animations** just like with standard SwiftUI transitions: You can **combine** them: ```swift -.navigationViewStackTransition( +.navigationTransition( .slide.combined(with: .fade(.in)) ) ``` @@ -78,7 +62,7 @@ You can **combine** them: And you can **dynamically** choose between transitions based on logic: ```swift -.navigationViewStackTransition( +.navigationTransition( reduceMotion ? .fade(.in).animation(.linear) : .slide(.vertical) ) ``` @@ -129,13 +113,13 @@ The [**Demo**](Demo) app showcases some of these transitions in action. A sweet additional feature is the ability to override the behavior of the **pop gesture** on the navigation view: ```swift -.navigationViewStackTransition(.slide, interactivity: .pan) // full-pan screen gestures! +.navigationTransition(.slide, interactivity: .pan) // full-pan screen gestures! ``` This even works to override its behavior while maintaining the **default system transition** in iOS: ```swift -.navigationViewStackTransition(.default, interactivity: .pan) // ✨ +.navigationTransition(.default, interactivity: .pan) // ✨ ``` ## Documentation diff --git a/Sources/NavigationTransitions/NavigationTransition+SwiftUI.swift b/Sources/NavigationTransitions/NavigationTransition+SwiftUI.swift index 4ea98b2d..3a659ed1 100644 --- a/Sources/NavigationTransitions/NavigationTransition+SwiftUI.swift +++ b/Sources/NavigationTransitions/NavigationTransition+SwiftUI.swift @@ -3,14 +3,18 @@ import SwiftUI // MARK: iOS 16 -@available(iOS, introduced: 16) public struct NavigationSplitViewColumns: OptionSet { + @available(iOS, introduced: 16, deprecated, message: "Use 'navigationTransition' modifier instead") public static let sidebar = Self(rawValue: 1) + @available(iOS, introduced: 16, deprecated, message: "Use 'navigationTransition' modifier instead") public static let content = Self(rawValue: 1 << 1) + @available(iOS, introduced: 16, deprecated, message: "Use 'navigationTransition' modifier instead") public static let detail = Self(rawValue: 1 << 2) + @available(iOS, introduced: 16, deprecated, message: "Use 'navigationTransition' modifier instead") public static let compact = Self(rawValue: 1 << 3) + @available(iOS, introduced: 16, deprecated, message: "Use 'navigationTransition' modifier instead") public static let all: Self = [compact, sidebar, content, detail] public let rawValue: Int8 @@ -20,147 +24,99 @@ public struct NavigationSplitViewColumns: OptionSet { } } -@available(iOS, introduced: 16) -extension UISplitViewControllerColumns { - init(_ columns: NavigationSplitViewColumns) { - var _columns: Self = [] - if columns.contains(.sidebar) { - _columns.insert(.primary) - } - if columns.contains(.content) { - _columns.insert(.supplementary) - } - if columns.contains(.detail) { - _columns.insert(.secondary) - } - if columns.contains(.compact) { - _columns.insert(.compact) - } - self = _columns - } -} - extension View { - @available(iOS, introduced: 16) + @available(iOS, introduced: 16, deprecated, renamed: "navigationTransition") @ViewBuilder public func navigationSplitViewTransition( _ transition: AnyNavigationTransition, forColumns columns: NavigationSplitViewColumns, interactivity: AnyNavigationTransition.Interactivity = .default ) -> some View { - self.modifier( - NavigationSplitOrStackTransitionModifier( - transition: transition, - target: .navigationSplitView(columns), - interactivity: interactivity - ) - ) + self.navigationTransition(transition, interactivity: interactivity) } - @available(iOS, introduced: 16) + @available(iOS, introduced: 16, deprecated, renamed: "navigationTransition") @ViewBuilder public func navigationStackTransition( _ transition: AnyNavigationTransition, interactivity: AnyNavigationTransition.Interactivity = .default + ) -> some View { + self.navigationTransition(transition, interactivity: interactivity) + } +} + +extension View { + @ViewBuilder + public func navigationTransition( + _ transition: AnyNavigationTransition, + interactivity: AnyNavigationTransition.Interactivity = .default ) -> some View { self.modifier( - NavigationSplitOrStackTransitionModifier( + NavigationTransitionModifier( transition: transition, - target: .navigationStack, interactivity: interactivity ) ) } } -@available(iOS, introduced: 16) -struct NavigationSplitOrStackTransitionModifier: ViewModifier { - enum Target { - case navigationSplitView(NavigationSplitViewColumns) - case navigationStack - } - +struct NavigationTransitionModifier: ViewModifier { let transition: AnyNavigationTransition - let target: Target let interactivity: AnyNavigationTransition.Interactivity func body(content: Content) -> some View { - switch target { - case .navigationSplitView(let columns): - content.inject( - UIKitIntrospectionViewController { spy -> UISplitViewController? in - if let controller = Introspect.previousSibling(ofType: UISplitViewController.self, from: spy) { - return controller - } else { - runtimeWarn( - """ - Modifier "navigationSplitViewTransition" was applied to a view other than NavigationSplitView. This has no effect. - - Please make sure you're applying the modifier correctly: - - NavigationSplitView { - ... - } - .navigationSplitViewTransition(...) - - Otherwise, if you're using a NavigationStack, please apply the corresponding modifier "navigationStackTransition" instead. - """ - ) - return nil - } - } - customize: { (controller: UISplitViewController) in - controller.setNavigationTransition(transition, forColumns: .init(columns), interactivity: interactivity) - } - ) - - case .navigationStack: - content.inject( - UIKitIntrospectionViewController { spy -> UINavigationController? in - guard spy.parent != nil else { - return nil // don't evaluate view until it's on screen - } - if let controller = Introspect.previousSibling(ofType: UINavigationController.self, from: spy) { - return controller - } else if let controller = spy.navigationController { - return controller - } else { - runtimeWarn( - """ - Modifier "navigationStackTransition" was applied to a view other than NavigationStack. This has no effect. - - Please make sure you're applying the modifier correctly: - - NavigationStack { - ... - } - .navigationStackTransition(...) - - Otherwise, if you're using a NavigationSplitView, please apply the corresponding modifier "navigationSplitViewTransition" instead. - """ - ) - return nil - } + content.inject( + UIKitIntrospectionViewController { spy -> UINavigationController? in + guard spy.parent != nil else { + return nil // don't evaluate view until it's on screen } - customize: { (controller: UINavigationController) in - controller.setNavigationTransition(transition, interactivity: interactivity) + if let controller = Introspect.previousSibling(ofType: UINavigationController.self, from: spy) { + return controller + } else if let controller = spy.navigationController { + return controller + } else { + runtimeWarn( + """ + Modifier "navigationTransition" was applied to a view other than NavigationStack OR NavigationView with .navigationViewStyle(.stack). This has no effect. + + Please make sure you're applying the modifier correctly: + + NavigationStack { + ... + } + .navigationTransition(...) + + OR + + NavigationView { + ... + } + .navigationViewStyle(.stack) + .navigationTransition(...) + + If you're attempting to apply the modifier to NavigationSplitView OR NavigationView with .navigationViewStyle(.columns), this has no effect either. Instead, you should apply the modifier to the child stack navigation component within it (if any). + """ + ) + return nil } - ) - } + } + customize: { (controller: UINavigationController) in + controller.setNavigationTransition(transition, interactivity: interactivity) + } + ) } } // MARK: - Pre-iOS 16 -@available(iOS, introduced: 13, deprecated: 16, message: - """ - Use `NavigationSplitView` and `.navigationSplitViewTransition` with `NavigationSplitViewColumns` instead. - """ -) +@available(iOS, introduced: 13, deprecated, message: "Use 'navigationTransition' instead") public struct NavigationViewColumns: OptionSet { + @available(iOS, introduced: 13, deprecated, message: "Use 'navigationTransition' instead") public static let sidebar = Self(rawValue: 1) + @available(iOS, introduced: 13, deprecated, message: "Use 'navigationTransition' instead") public static let detail = Self(rawValue: 1 << 1) + @available(iOS, introduced: 13, deprecated, message: "Use 'navigationTransition' instead") public static let all: Self = [sidebar, detail] public let rawValue: Int8 @@ -170,134 +126,23 @@ public struct NavigationViewColumns: OptionSet { } } -extension UISplitViewControllerColumns { - init(_ columns: NavigationViewColumns) { - var _columns: Self = [] - if columns.contains(.sidebar) { - _columns.insert(.primary) - } - if columns.contains(.detail) { - _columns.insert(.secondary) - } - self = _columns - } -} - extension View { - @available(iOS, introduced: 13, deprecated: 16, renamed: "navigationSplitViewTransition", message: - """ - Use `NavigationSplitView` and `.navigationSplitViewTransition` instead. - """ - ) + @available(iOS, introduced: 13, deprecated, renamed: "navigationTransition") @ViewBuilder public func navigationViewColumnTransition( _ transition: AnyNavigationTransition, forColumns columns: NavigationViewColumns, interactivity: AnyNavigationTransition.Interactivity = .default ) -> some View { - self.modifier( - NavigationViewTransitionModifier( - transition: transition, - style: .columns(columns), - interactivity: interactivity - ) - ) + self.navigationTransition(transition, interactivity: interactivity) } - @available(iOS, introduced: 13, deprecated: 16, renamed: "navigationStackTransition", message: - """ - Use `NavigationStack` and `.navigationStackTransition` instead. - """ - ) + @available(iOS, introduced: 13, deprecated, renamed: "navigationTransition") @ViewBuilder public func navigationViewStackTransition( _ transition: AnyNavigationTransition, interactivity: AnyNavigationTransition.Interactivity = .default ) -> some View { - self.modifier( - NavigationViewTransitionModifier( - transition: transition, - style: .stack, - interactivity: interactivity - ) - ) - } -} - -struct NavigationViewTransitionModifier: ViewModifier { - enum Style { - case columns(NavigationViewColumns) - case stack - } - - let transition: AnyNavigationTransition - let style: Style - let interactivity: AnyNavigationTransition.Interactivity - - func body(content: Content) -> some View { - switch style { - case .columns(let columns): - content.inject( - UIKitIntrospectionViewController { spy -> UISplitViewController? in - if let controller = Introspect.previousSibling(ofType: UISplitViewController.self, from: spy) { - return controller - } else { - runtimeWarn( - """ - Modifier "navigationViewColumnTransition" was applied to a view other than NavigationView with .navigationViewStyle(.columns). This has no effect. - - Please make sure you're applying the modifier correctly: - - NavigationView { - ... - } - .navigationStyle(.columns) - .navigationViewTransition(...) - - Otherwise, if you're using a NavigationView with .navigationViewStyle(.stack), please apply the corresponding modifier "navigationViewStackTransition" instead. - """ - ) - return nil - } - } - customize: { (controller: UISplitViewController) in - controller.setNavigationTransition(transition, forColumns: .init(columns), interactivity: interactivity) - } - ) - - case .stack: - content.inject( - UIKitIntrospectionViewController { spy -> UINavigationController? in - guard spy.parent != nil else { - return nil // don't evaluate view until it's on screen - } - if let controller = Introspect.previousSibling(ofType: UINavigationController.self, from: spy) { - return controller - } else if let controller = spy.navigationController { - return controller - } else { - runtimeWarn( - """ - Modifier "navigationViewStackTransition" was applied to a view other than NavigationView with .navigationViewStyle(.stack). This has no effect. - - Please make sure you're applying the modifier correctly: - - NavigationStack { - ... - } - .navigationViewStyle(.stack) - .navigationStackTransition(...) - - Otherwise, if you're using a NavigationView with .navigationViewStyle(.columns), please apply the corresponding modifier "navigationViewColumnTransition" instead. - """ - ) - return nil - } - } - customize: { (controller: UINavigationController) in - controller.setNavigationTransition(transition, interactivity: interactivity) - } - ) - } + self.navigationTransition(transition, interactivity: interactivity) } }