From d469661a1d16ab2d82d6f43173a78924b5187734 Mon Sep 17 00:00:00 2001 From: David Scheutz Date: Sun, 18 Jun 2023 16:12:48 -0400 Subject: [PATCH 1/5] first draft implementation of sheet presentation styles --- .../Navigation/MyNavigation.swift | 2 +- .../Helper/View+Apply.swift | 12 +++ .../InfiniteNavContainer.swift | 87 +++++++++++++------ .../LegacyInfiniteNavContainer.swift | 9 +- .../InfiniteNavigation/NavDestination.swift | 7 +- 5 files changed, 87 insertions(+), 30 deletions(-) create mode 100644 Sources/InfiniteNavigation/Helper/View+Apply.swift diff --git a/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift b/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift index 89cd361..588385b 100644 --- a/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift +++ b/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift @@ -11,7 +11,7 @@ final class MyNavigation: HomeNavigation, SheetNavigation, DetailNavigation { // MARK: - HomeNavigation func showSheet() { - navigateTo.send(.show(.sheet(.sheet))) + navigateTo.send(.show(.sheet(.sheet, style: .modal))) } func showDetail() { diff --git a/Sources/InfiniteNavigation/Helper/View+Apply.swift b/Sources/InfiniteNavigation/Helper/View+Apply.swift new file mode 100644 index 0000000..4aa6044 --- /dev/null +++ b/Sources/InfiniteNavigation/Helper/View+Apply.swift @@ -0,0 +1,12 @@ +import SwiftUI + +extension View { + @ViewBuilder + func apply(_ value: T?, @ViewBuilder _ apply: (T, Self) -> Result) -> some View { + if let value = value { + apply(value, self) + } else { + self + } + } +} diff --git a/Sources/InfiniteNavigation/InfiniteNavContainer.swift b/Sources/InfiniteNavigation/InfiniteNavContainer.swift index 95b8595..ef752d8 100644 --- a/Sources/InfiniteNavigation/InfiniteNavContainer.swift +++ b/Sources/InfiniteNavigation/InfiniteNavContainer.swift @@ -5,9 +5,16 @@ import Combine internal struct Sheet: Identifiable { let id = UUID().uuidString var path = NavigationPath() + let style: SheetPresentationStlye let source: () -> AnyView } +@available(iOS 16.0, *) +internal struct RootContainer { + var path: NavigationPath + let source: () -> T +} + public typealias Environments = [any ObservableObject] @available(iOS 16.0, *) @@ -20,7 +27,8 @@ public struct InfiniteNavContainer: View { private let viewBuilder: NavDestinationBuilder private let environments: Environments - @State private var stack: [Sheet] + @State private var stack = [Sheet]() + @State private var root: RootContainer init( initialStack: [Destination] = [], @@ -29,9 +37,7 @@ public struct InfiniteNavContainer: View { viewBuilder: @escaping NavDestinationBuilder, root: @escaping () -> Root ) { - _stack = .init(initialValue: [ - .init(path: NavigationPath(initialStack), source: { root().toAnyView() }) - ]) + _root = .init(initialValue: .init(path: NavigationPath(initialStack), source: root)) self.navAction = navAction self.viewBuilder = viewBuilder @@ -39,12 +45,12 @@ public struct InfiniteNavContainer: View { } public var body: some View { - root + render(root: $root) .onReceive(navAction.receiveOnMain()) { switch $0 { case .show(let action): switch action { - case .sheet(let destination): stack.append(.init { viewBuilder(destination) }) + case .sheet(let destination, let style): stack.append(.init(style: style) { viewBuilder(destination) }) case .detail(let destination): mutateCurrentPath { $0.append(destination) } } case .setStack(let destinations): mutateCurrentPath { $0.append(contentsOf: destinations) } @@ -59,28 +65,52 @@ public struct InfiniteNavContainer: View { @available(iOS 16.0, *) extension InfiniteNavContainer { - private var root: some View { - guard let root = $stack.first else { - fatalError("Root view unexpectedly missing.") - } - return render(sheet: root) + private func render(root: Binding>) -> some View { + render(source: root.wrappedValue.source, path: root.path, id: nil) } private func render(sheet: Binding) -> AnyView { - NavigationStack(path: sheet.path) { - wrap(sheet.wrappedValue.source()) - .navigationDestination(for: Destination.self) { wrap(viewBuilder($0)) } - .fullScreenCover(item: Binding( - get: { next(after: sheet.wrappedValue) }, - set: { if $0 == nil && stack.last?.id == next(after: sheet.wrappedValue)?.id { dismiss() } } - )) { sheet in - render(sheet: .init( - get: { sheet }, - set: { if $0.id == sheet.id { update(sheet: $0) } } - )) + render(source: sheet.wrappedValue.source, path: sheet.path, id: sheet.wrappedValue.id) + .toAnyView() + } + + private func render(source: () -> some View, path: Binding, id: String?) -> some View { + let buildNextSheet: (SheetPresentationStlye) -> Binding = { style in + Binding( + get: { + let next = nextSheet(after: id) + return next?.style == style ? next : nil + }, + set: { + if $0 == nil && stack.last?.id == nextSheet(after: id)?.id { + dismiss() + } } + ) + } + + let updatableSheet: (Sheet) -> Binding = { sheet in + .init( + get: { sheet }, + set: { if $0.id == sheet.id { update(sheet: $0) } } + ) + } + + return NavigationStack(path: path) { + wrap(source()) + .navigationDestination(for: Destination.self) { wrap(viewBuilder($0)) } + .sheet(item: buildNextSheet(.modal)) { render(sheet: updatableSheet($0)) } + .fullScreenCover(item: buildNextSheet(.fullScreen)) { render(sheet: updatableSheet($0)) } + // TODO: make this work to enforce exhaustiveness +// .apply(nextSheetBinding.wrappedValue?.style) { style, view in +// switch style { +// case .fullScreen: +// view.fullScreenCover(item: nextSheetBinding) { render(sheet: updatableSheet($0)) } +// case .modal: +// view.sheet(item: nextSheetBinding) { render(sheet: updatableSheet($0)) } +// } +// } } - .toAnyView() } private func wrap(_ view: some View) -> some View { @@ -90,7 +120,11 @@ extension InfiniteNavContainer { } private func mutateCurrentPath(_ mutate: (inout NavigationPath) -> Void) { - mutate(&stack[stack.count - 1].path) + if stack.isEmpty { + mutate(&root.path) + } else { + mutate(&stack[stack.count - 1].path) + } } private func update(sheet: Sheet) { @@ -100,8 +134,9 @@ extension InfiniteNavContainer { stack[index] = sheet } - private func next(after sheet: Sheet) -> Sheet? { - guard let index = stack.firstIndex(where: { $0.id == sheet.id }) else { return nil } + private func nextSheet(after id: String? = nil) -> Sheet? { + guard let id = id else { return stack.first } + guard let index = stack.firstIndex(where: { $0.id == id }) else { return nil } return stack[safe: index + 1] } diff --git a/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift b/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift index 91f2bee..b09d085 100644 --- a/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift +++ b/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift @@ -80,11 +80,16 @@ public struct LegacyInfiniteNavContainer: UIViewContro case .detail(let detail): let vc = self.wrap(self.viewBuilder(detail)) self.navigationController?.pushViewController(vc, animated: true) - case .sheet(let sheet): + case .sheet(let sheet, let style): let vc = self.wrap(self.viewBuilder(sheet)) let navVc = UINavigationController(rootViewController: vc) navVc.navigationBar.isHidden = true - navVc.modalPresentationStyle = .fullScreen + switch style { + case .fullScreen: + navVc.modalPresentationStyle = .fullScreen + case .modal: + navVc.modalPresentationStyle = .overCurrentContext + } self.navigationController?.present(navVc, animated: true) } case .setStack(let stack): diff --git a/Sources/InfiniteNavigation/NavDestination.swift b/Sources/InfiniteNavigation/NavDestination.swift index 054d5b2..1b50630 100644 --- a/Sources/InfiniteNavigation/NavDestination.swift +++ b/Sources/InfiniteNavigation/NavDestination.swift @@ -1,6 +1,11 @@ import Foundation +public enum SheetPresentationStlye { + case fullScreen + case modal +} + public enum NavDestination { case detail(T) - case sheet(T) + case sheet(T, style: SheetPresentationStlye) } From c12f06a1114cea54685ce1e291c710d6d84f4598 Mon Sep 17 00:00:00 2001 From: David Scheutz Date: Sun, 18 Jun 2023 16:14:56 -0400 Subject: [PATCH 2/5] update demo app --- .../Navigation/MyNavigation.swift | 4 ++-- .../Views/HomeView.swift | 3 ++- .../Views/NavFooterView.swift | 23 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift b/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift index 588385b..995d273 100644 --- a/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift +++ b/InfiniteNavigationDemo/InfiniteNavigationDemo/Navigation/MyNavigation.swift @@ -10,8 +10,8 @@ final class MyNavigation: HomeNavigation, SheetNavigation, DetailNavigation { // MARK: - HomeNavigation - func showSheet() { - navigateTo.send(.show(.sheet(.sheet, style: .modal))) + func showSheet(style: SheetPresentationStlye) { + navigateTo.send(.show(.sheet(.sheet, style: style))) } func showDetail() { diff --git a/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/HomeView.swift b/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/HomeView.swift index e288c67..d945482 100644 --- a/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/HomeView.swift +++ b/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/HomeView.swift @@ -1,7 +1,8 @@ import SwiftUI +import InfiniteNavigation protocol HomeNavigation { - func showSheet() + func showSheet(style: SheetPresentationStlye) func showDetail() } diff --git a/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/NavFooterView.swift b/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/NavFooterView.swift index 6b8a71a..88b1831 100644 --- a/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/NavFooterView.swift +++ b/InfiniteNavigationDemo/InfiniteNavigationDemo/Views/NavFooterView.swift @@ -1,20 +1,27 @@ import SwiftUI +import InfiniteNavigation struct NavFooterView: View { typealias Completion = () -> Void - let showSheet: Completion + let showSheet: (SheetPresentationStlye) -> Void let showDetail: Completion var popDetails: Completion? var body: some View { - HStack { - Button("Show Detail", action: showDetail) - if let popDetails = popDetails { - Button("Pop Details", action: popDetails) + VStack { + HStack { + Button("Show Detail", action: showDetail) + if let popDetails = popDetails { + Button("Pop Details", action: popDetails) + } + } + + HStack { + Button("Show Full Sheet") { showSheet(.fullScreen) } + Button("Show Modal Sheet") { showSheet(.modal) } } - Button("Show Sheet", action: showSheet) } .buttonStyle(.bordered) } @@ -23,8 +30,8 @@ struct NavFooterView: View { #if DEBUG struct NavFooterView_Previews: PreviewProvider { static var previews: some View { - NavFooterView(showSheet: {}, showDetail: {}) - NavFooterView(showSheet: {}, showDetail: {}, popDetails: {}) + NavFooterView(showSheet: { _ in }, showDetail: {}) + NavFooterView(showSheet: { _ in }, showDetail: {}, popDetails: {}) } } #endif From ca906c1ddebe3c61d0c2c385387578ea7ae9effa Mon Sep 17 00:00:00 2001 From: David Scheutz Date: Sun, 18 Jun 2023 16:20:55 -0400 Subject: [PATCH 3/5] use correct legacy modal presentation style --- Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift b/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift index b09d085..eff7d6b 100644 --- a/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift +++ b/Sources/InfiniteNavigation/LegacyInfiniteNavContainer.swift @@ -88,7 +88,7 @@ public struct LegacyInfiniteNavContainer: UIViewContro case .fullScreen: navVc.modalPresentationStyle = .fullScreen case .modal: - navVc.modalPresentationStyle = .overCurrentContext + navVc.modalPresentationStyle = .popover } self.navigationController?.present(navVc, animated: true) } From f0233cf696863bedf802cac5feea0c55cc3926aa Mon Sep 17 00:00:00 2001 From: David Scheutz Date: Sun, 18 Jun 2023 16:38:30 -0400 Subject: [PATCH 4/5] enforce presentation style exhaustiveness --- .../InfiniteNavContainer.swift | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/Sources/InfiniteNavigation/InfiniteNavContainer.swift b/Sources/InfiniteNavigation/InfiniteNavContainer.swift index ef752d8..b5686a1 100644 --- a/Sources/InfiniteNavigation/InfiniteNavContainer.swift +++ b/Sources/InfiniteNavigation/InfiniteNavContainer.swift @@ -75,19 +75,14 @@ extension InfiniteNavContainer { } private func render(source: () -> some View, path: Binding, id: String?) -> some View { - let buildNextSheet: (SheetPresentationStlye) -> Binding = { style in - Binding( - get: { - let next = nextSheet(after: id) - return next?.style == style ? next : nil - }, - set: { - if $0 == nil && stack.last?.id == nextSheet(after: id)?.id { - dismiss() - } + let nextSheetBinding = Binding( + get: { nextSheet(after: id) }, + set: { + if $0 == nil && stack.last?.id == nextSheet(after: id)?.id { + dismiss() } - ) - } + } + ) let updatableSheet: (Sheet) -> Binding = { sheet in .init( @@ -97,19 +92,19 @@ extension InfiniteNavContainer { } return NavigationStack(path: path) { - wrap(source()) + let content = wrap(source()) .navigationDestination(for: Destination.self) { wrap(viewBuilder($0)) } - .sheet(item: buildNextSheet(.modal)) { render(sheet: updatableSheet($0)) } - .fullScreenCover(item: buildNextSheet(.fullScreen)) { render(sheet: updatableSheet($0)) } - // TODO: make this work to enforce exhaustiveness -// .apply(nextSheetBinding.wrappedValue?.style) { style, view in -// switch style { -// case .fullScreen: -// view.fullScreenCover(item: nextSheetBinding) { render(sheet: updatableSheet($0)) } -// case .modal: -// view.sheet(item: nextSheetBinding) { render(sheet: updatableSheet($0)) } -// } -// } + + if let style = nextSheet(after: id)?.style { + switch style { + case .fullScreen: + content.fullScreenCover(item: nextSheetBinding) { render(sheet: updatableSheet($0)) }.id(id) + case .modal: + content.sheet(item: nextSheetBinding) { render(sheet: updatableSheet($0)) }.id(id) + } + } else { + content.id(id) + } } } From ddc61e84752659672c895b26472f82ae70c7e8d9 Mon Sep 17 00:00:00 2001 From: David Scheutz Date: Sun, 18 Jun 2023 20:50:45 -0400 Subject: [PATCH 5/5] code cleanup --- Sources/InfiniteNavigation/Helper/View+Apply.swift | 12 ------------ .../InfiniteNavigation/InfiniteNavContainer.swift | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 Sources/InfiniteNavigation/Helper/View+Apply.swift diff --git a/Sources/InfiniteNavigation/Helper/View+Apply.swift b/Sources/InfiniteNavigation/Helper/View+Apply.swift deleted file mode 100644 index 4aa6044..0000000 --- a/Sources/InfiniteNavigation/Helper/View+Apply.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI - -extension View { - @ViewBuilder - func apply(_ value: T?, @ViewBuilder _ apply: (T, Self) -> Result) -> some View { - if let value = value { - apply(value, self) - } else { - self - } - } -} diff --git a/Sources/InfiniteNavigation/InfiniteNavContainer.swift b/Sources/InfiniteNavigation/InfiniteNavContainer.swift index b5686a1..40b924b 100644 --- a/Sources/InfiniteNavigation/InfiniteNavContainer.swift +++ b/Sources/InfiniteNavigation/InfiniteNavContainer.swift @@ -95,7 +95,7 @@ extension InfiniteNavContainer { let content = wrap(source()) .navigationDestination(for: Destination.self) { wrap(viewBuilder($0)) } - if let style = nextSheet(after: id)?.style { + if let style = nextSheetBinding.wrappedValue?.style { switch style { case .fullScreen: content.fullScreenCover(item: nextSheetBinding) { render(sheet: updatableSheet($0)) }.id(id)