From 2f1e4f0d53a2e51d90a1f9ba2348f1aacb391769 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 17 May 2022 17:27:22 +0200 Subject: [PATCH 01/16] wip --- Package.swift | 6 +++--- Sources/Helpers/View+AnyView.swift | 2 +- Sources/NavigationItem/NavigationItem.swift | 2 +- Sources/Reducers/DismissModalReducer.swift | 1 - Sources/Reducers/NavigationReducer.swift | 3 +-- Sources/Reducers/PopReducer.swift | 3 --- Sources/Reducers/PushReducer.swift | 5 ----- Sources/Reducers/ShowReducer.swift | 13 ++----------- Sources/State/Navigation.swift | 6 +++--- Sources/State/NavigationState.swift | 1 - Sources/Views/ContainerView.swift | 14 ++++---------- 11 files changed, 15 insertions(+), 41 deletions(-) diff --git a/Package.swift b/Package.swift index 613ac96..147e7b5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "ReMVVMExt", - platforms: [.iOS(.v13)], + platforms: [.iOS(.v14)], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( @@ -25,7 +25,7 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ReMVVMExt", - dependencies: ["ReMVVMCore", "ReMVVMSwiftUI"], + dependencies: [.product(name: "ReMVVMCore", package: "ReMVVM"), .product(name: "ReMVVMSwiftUI", package: "ReMVVM")], path: "Sources", exclude: []) ] diff --git a/Sources/Helpers/View+AnyView.swift b/Sources/Helpers/View+AnyView.swift index 07981fd..0e11468 100644 --- a/Sources/Helpers/View+AnyView.swift +++ b/Sources/Helpers/View+AnyView.swift @@ -9,5 +9,5 @@ import SwiftUI extension View { - public var any: AnyView { return AnyView(self) } + public var any: AnyView { AnyView(self) } } diff --git a/Sources/NavigationItem/NavigationItem.swift b/Sources/NavigationItem/NavigationItem.swift index 3d15574..6c07694 100644 --- a/Sources/NavigationItem/NavigationItem.swift +++ b/Sources/NavigationItem/NavigationItem.swift @@ -28,7 +28,7 @@ extension NavigationItem where Self: CaseIterable { extension NavigationItem where Self: Equatable { public func isEqual(to item: NavigationItem) -> Bool { - return (item as? Self) == self + (item as? Self) == self } } diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index 628f4d3..dc5c060 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -15,7 +15,6 @@ public enum DismissModalReducer: Reducer { } extension Stack where StackItem == Modal { - func dropLast() -> Self { guard items.count > 0 else { return self } return Stack(with: items.dropLast()) diff --git a/Sources/Reducers/NavigationReducer.swift b/Sources/Reducers/NavigationReducer.swift index c05ac65..abdd23f 100644 --- a/Sources/Reducers/NavigationReducer.swift +++ b/Sources/Reducers/NavigationReducer.swift @@ -9,7 +9,6 @@ import ReMVVMCore public enum NavigationReducer: Reducer { - static let composed = PushReducer .compose(with: PopReducer.self) .compose(with: ShowReducer.self) @@ -18,6 +17,6 @@ public enum NavigationReducer: Reducer { .compose(with: SynchronizeReducer.self) public static func reduce(state: Navigation, with action: StoreAction) -> Navigation { - return composed.reduce(state: state, with: action) + composed.reduce(state: state, with: action) } } diff --git a/Sources/Reducers/PopReducer.swift b/Sources/Reducers/PopReducer.swift index 4a21324..f48bf97 100644 --- a/Sources/Reducers/PopReducer.swift +++ b/Sources/Reducers/PopReducer.swift @@ -15,7 +15,6 @@ public enum PopReducer: Reducer { } extension Navigation { - func pop() -> Navigation { if modals.hasNavigation { return Navigation(root: root, modals: modals.pop()) @@ -39,7 +38,6 @@ extension Stack where StackItem == Modal { } extension Root { - func pop() -> Root { let stacks = self.stacks.enumerated().map { index, stack -> (NavigationItem, Stack) in guard index == current else { return stack } @@ -50,7 +48,6 @@ extension Root { } extension Stack where StackItem == Element { - func pop() -> Self { guard items.count > 1 else { return self } return Stack(with: items.dropLast()) diff --git a/Sources/Reducers/PushReducer.swift b/Sources/Reducers/PushReducer.swift index 5239078..11154ca 100644 --- a/Sources/Reducers/PushReducer.swift +++ b/Sources/Reducers/PushReducer.swift @@ -10,14 +10,12 @@ import ReMVVMCore import SwiftUI public enum PushReducer: Reducer { - public static func reduce(state: Navigation, with action: Push) -> Navigation { state.push(viewFactory: action.viewFactory, factory: action.factory) } } extension Navigation { - func push(viewFactory: @escaping ViewFactory, factory: ViewModelFactory?) -> Navigation { let factory = factory ?? viewModelFactory if modals.hasNavigation { @@ -29,7 +27,6 @@ extension Navigation { } extension Stack where StackItem == Modal { - func push(viewFactory: @escaping ViewFactory, factory: ViewModelFactory) -> Self { let items = self.items.reversed().drop { !$0.hasNavigation }.reversed() if case .navigation(let stack) = items.last { @@ -44,7 +41,6 @@ extension Stack where StackItem == Modal { } extension Root { - func push(viewFactory: @escaping ViewFactory, factory: ViewModelFactory) -> Root { let stacks = self.stacks.enumerated().map { index, stack -> (NavigationItem, Stack) in guard index == current else { return stack } @@ -55,7 +51,6 @@ extension Root { } extension Stack where StackItem == Element { - func push(viewFactory: @escaping ViewFactory, factory: ViewModelFactory) -> Self { let item = Element(with: id, viewFactory: viewFactory, factory: factory) return Stack(with: items + [item]) diff --git a/Sources/Reducers/ShowReducer.swift b/Sources/Reducers/ShowReducer.swift index 306a50f..5d59252 100644 --- a/Sources/Reducers/ShowReducer.swift +++ b/Sources/Reducers/ShowReducer.swift @@ -10,22 +10,13 @@ import ReMVVMCore public enum ShowReducer: Reducer { public static func reduce(state: Navigation, with action: Show) -> Navigation { - let current = action.item var stacks: [(NavigationItem, Stack)] let factory = action.factory ?? state.viewModelFactory if !action.item.isType(of: RootNavigationItem.self) && action.item.isEqualType(to: state.root.currentItem) { stacks = state.root.stacks.map { - if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { - guard $0.0.isEqual(to: current), $0.1.items.isEmpty else { return $0 } - return ($0.0, $0.1.push(viewFactory: action.viewFactory, factory: factory)) - } else { // clear tab on change (because of bug in SwiftUI 1) - guard $0.0.isEqual(to: current) else { return ($0.0, $0.1.empty()) } - guard $0.1.items.isEmpty else { - return $0 - } - return ($0.0, $0.1.push(viewFactory: action.viewFactory, factory: factory)) - } + guard $0.0.isEqual(to: current), $0.1.items.isEmpty else { return $0 } + return ($0.0, $0.1.push(viewFactory: action.viewFactory, factory: factory)) } } else { stacks = type(of: action.item).allItems.map { diff --git a/Sources/State/Navigation.swift b/Sources/State/Navigation.swift index 65c9436..57d03d6 100644 --- a/Sources/State/Navigation.swift +++ b/Sources/State/Navigation.swift @@ -20,7 +20,7 @@ public struct Navigation { } public var viewModelFactory: ViewModelFactory { - return modals.viewModelFactory ?? root.viewModelFactory ?? CompositeViewModelFactory() + modals.viewModelFactory ?? root.viewModelFactory ?? CompositeViewModelFactory() } public func item(with id: UUID) -> Element? { @@ -72,12 +72,12 @@ public struct Navigation { extension Stack where StackItem == Modal { var viewModelFactory: ViewModelFactory? { - return items.last?.viewModelFactory + items.last?.viewModelFactory } } extension Stack where StackItem == Element { var viewModelFactory: ViewModelFactory? { - return items.last?.viewModelFactory + items.last?.viewModelFactory } } diff --git a/Sources/State/NavigationState.swift b/Sources/State/NavigationState.swift index 7b261f9..69c9c2f 100644 --- a/Sources/State/NavigationState.swift +++ b/Sources/State/NavigationState.swift @@ -9,6 +9,5 @@ import ReMVVMCore public protocol NavigationState: StoreState { - var navigation: Navigation { get } } diff --git a/Sources/Views/ContainerView.swift b/Sources/Views/ContainerView.swift index 70aa79d..3bd4bf6 100644 --- a/Sources/Views/ContainerView.swift +++ b/Sources/Views/ContainerView.swift @@ -46,13 +46,10 @@ public struct ContainerView: View { private var cancellables = Set() init(id: UUID) { $state - .map { - $0.item(with: id) - } + .map { $0.item(with: id) } .filter { $0 != nil } .compactMap { $0?.view } - .assignNoRetain(to: \.view, on: self) - .store(in: &cancellables) + .assign(to: &$view) } } @@ -76,7 +73,6 @@ public struct ContainerView: View { @Published var view: ContainerView? @ReMVVM.State private var state: Navigation? - private var cancellables = Set() private let id: UUID init(id: UUID) { @@ -85,15 +81,13 @@ public struct ContainerView: View { $state .map { $0.nextItem(for: id)?.id } .removeDuplicates() - .assignNoRetain(to: \.active, on: self) - .store(in: &cancellables) + .assign(to: &$active) $state .compactMap { $0.nextId(for: id) } .removeDuplicates() .map { ContainerView(id: $0, synchronize: true) } - .assignNoRetain(to: \.view, on: self) - .store(in: &cancellables) + .assign(to: &$view) } } } From fcf4aaa85f4cb85671b9230cd5c252837302c10c Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 17 May 2022 17:30:06 +0200 Subject: [PATCH 02/16] removed cancellables --- Sources/Views/ContainerView.swift | 2 +- Sources/Views/MainView.swift | 4 +--- Sources/Views/ModalContainerView.swift | 7 ++----- Sources/Views/TabContainerView.swift | 10 +++------- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Sources/Views/ContainerView.swift b/Sources/Views/ContainerView.swift index 3bd4bf6..f475191 100644 --- a/Sources/Views/ContainerView.swift +++ b/Sources/Views/ContainerView.swift @@ -43,7 +43,7 @@ public struct ContainerView: View { @Published private(set) var view: AnyView = Text("No view in container").any @ReMVVM.State private var state: Navigation? - private var cancellables = Set() + init(id: UUID) { $state .map { $0.item(with: id) } diff --git a/Sources/Views/MainView.swift b/Sources/Views/MainView.swift index d3fd6cb..7cbec2f 100644 --- a/Sources/Views/MainView.swift +++ b/Sources/Views/MainView.swift @@ -27,7 +27,6 @@ public struct MainView: View { @Published var view: AnyView? @ReMVVM.State private var state: Navigation? - private var cancellables = Set() init() { $state @@ -43,8 +42,7 @@ public struct MainView: View { .combineLatest($isModalActive) { ($0, $1) } .filter { $0.1 == false } .map { type(of: $0.0.root.currentItem).viewFactory() } - .assignNoRetain(to: \.view, on: self) - .store(in: &cancellables) + .assign(to: &$view) } } } diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index c16a510..0f94d17 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -67,7 +67,6 @@ public struct ModalContainerView: View { @Published private(set) var view: AnyView = Text("Empty modal").any @ReMVVM.State private var state: Navigation? - private var cancellables = Set() init(viewType: ViewType) { @@ -84,8 +83,7 @@ public struct ModalContainerView: View { let view = $0.hasNavigation ? NavigationView { containerView }.any : containerView.any return view.any } - .assignNoRetain(to: \.view, on: self) - .store(in: &cancellables) + .assign(to: &$view) case .view(let view): modalPublisher = $state.map { $0.modals.items.first?.id }.eraseToAnyPublisher() @@ -98,8 +96,7 @@ public struct ModalContainerView: View { .map { $0.0 } // .filter { [unowned self] s in s != nil || childVisible == false } .removeDuplicates() - .assignNoRetain(to: \.modal, on: self) - .store(in: &cancellables) + .assign(to: &$modal) } } } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 03f18b0..9ccd569 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -12,7 +12,6 @@ import ReMVVMSwiftUI public struct TabContainerView: View { - @ReMVVM.ObservedObject private var viewState = ViewState() public init() { } @@ -41,7 +40,7 @@ public struct TabContainerView: View { } @Published var items: [ItemContainer] = [] - private var uuidFromState: UUID = UUID() { + @Published private var uuidFromState: UUID = UUID() { didSet { if uuidFromState != currentUUID { currentUUID = uuidFromState @@ -51,7 +50,6 @@ public struct TabContainerView: View { @ReMVVM.Dispatcher private var dispatcher @ReMVVM.State private var state: Navigation? - private var cancellables = Set() init() { @@ -61,8 +59,7 @@ public struct TabContainerView: View { } .compactMap { $0 } .removeDuplicates() - .assignNoRetain(to: \.uuidFromState, on: self) - .store(in: &cancellables) + .assign(to: &$uuidFromState) $state .compactMap { state -> [ItemContainer]? in @@ -73,8 +70,7 @@ public struct TabContainerView: View { } } .prefix(1) //take only first value, next value will be handled by parent view - .assignNoRetain(to: \.items, on: self) - .store(in: &cancellables) + .assign(to: &$items) } } } From 25ed6253aba298590e563755ce15d1c5d77036b4 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 17 May 2022 17:44:50 +0200 Subject: [PATCH 03/16] refactor --- Sources/Views/TabContainerView.swift | 38 ++++++++++++---------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 9ccd569..af3c2cc 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -17,7 +17,6 @@ public struct TabContainerView: View { public init() { } public var body: some View { - TabView(selection: $viewState.currentUUID) { ForEach(viewState.items, id: \.id) { item in NavigationView { @@ -30,34 +29,31 @@ public struct TabContainerView: View { } private class ViewState: ObservableObject { + @Published var currentUUID: UUID = UUID() - @Published var currentUUID: UUID = UUID() { - didSet { - if uuidFromState != currentUUID, let tabItem = items.element(for: currentUUID)?.item as? TabNavigationItem { // user tapped - dispatcher.dispatch(action: tabItem.action) - } - } - } @Published var items: [ItemContainer] = [] - @Published private var uuidFromState: UUID = UUID() { - didSet { - if uuidFromState != currentUUID { - currentUUID = uuidFromState - } - } - } + @Published private var uuidFromState: UUID = UUID() @ReMVVM.Dispatcher private var dispatcher @ReMVVM.State private var state: Navigation? + private var cancellables = Set() + init() { + $uuidFromState + .filter { [weak self] in $0 != self?.currentUUID } + .assign(to: &$currentUUID) + + $currentUUID + .filter { [weak self] in self?.uuidFromState != $0 } + .compactMap { [weak self] in self?.items.element(for: $0)?.item as? TabNavigationItem } + .map { $0.action } + .sink { [weak self] action in self?.dispatcher.dispatch(action: action) } + .store(in: &cancellables) $state - .combineLatest($items) { state, items in - items.element(for: state.root.currentItem)?.id ?? UUID() - } - .compactMap { $0 } + .map { [weak self] state in self?.items.element(for: state.root.currentItem)?.id ?? UUID() } .removeDuplicates() .assign(to: &$uuidFromState) @@ -82,10 +78,8 @@ private struct ItemContainer: Identifiable { } extension Array where Element == ItemContainer { - func element(for id: UUID?) -> Element? { - guard let id = id else { return nil } - return first { $0.id == id } + first { $0.id == id } } func element(for item: NavigationItem) -> Element? { From cb7ad40cf827809243cc7ce4f61d57cecaf58336 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Wed, 18 May 2022 10:55:43 +0200 Subject: [PATCH 04/16] added popToRoot and pop(count:) --- Sources/Actions/NavigationActions.swift | 13 ++++- Sources/Reducers/PopReducer.swift | 64 ++++++++++++++++------- Sources/Reducers/SynchronizeReducer.swift | 2 +- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/Sources/Actions/NavigationActions.swift b/Sources/Actions/NavigationActions.swift index e0aae16..ebdbcf3 100644 --- a/Sources/Actions/NavigationActions.swift +++ b/Sources/Actions/NavigationActions.swift @@ -22,8 +22,17 @@ public struct Push: StoreAction { } public struct Pop: StoreAction { - public init() { } -} //TODO: POP Type - to root, number of pop items + public let animated: Bool + public let mode: PopMode + public init(mode: PopMode = .pop(1), animated: Bool = true) { + self.mode = mode + self.animated = animated + } +} + +public enum PopMode { + case popToRoot, pop(Int) +} public struct Show: StoreAction { public let viewFactory: ViewFactory diff --git a/Sources/Reducers/PopReducer.swift b/Sources/Reducers/PopReducer.swift index f48bf97..aa0dd99 100644 --- a/Sources/Reducers/PopReducer.swift +++ b/Sources/Reducers/PopReducer.swift @@ -10,16 +10,27 @@ import ReMVVMCore public enum PopReducer: Reducer { public static func reduce(state: Navigation, with action: Pop) -> Navigation { - state.pop() + switch action.mode { + case .pop(let count): return state.pop(count) + case .popToRoot: return state.popToRoot() + } } } extension Navigation { - func pop() -> Navigation { + func pop(_ count: Int) -> Navigation { + if modals.hasNavigation { + return Navigation(root: root, modals: modals.pop(count)) + } else { + return Navigation(root: root.pop(count)) + } + } + + func popToRoot() -> Navigation { if modals.hasNavigation { - return Navigation(root: root, modals: modals.pop()) + return Navigation(root: root, modals: modals.popToRoot()) } else { - return Navigation(root: root.pop()) + return Navigation(root: root.popToRoot()) } } } @@ -28,38 +39,55 @@ extension Stack where StackItem == Modal { var hasNavigation: Bool { items.contains { $0.hasNavigation } } - func pop() -> Self { + func pop(_ count: Int) -> Self { + guard hasNavigation else { return self } + let items = self.items.reversed().drop { !$0.hasNavigation }.reversed() + guard case .navigation(let stack) = items.last else { return self } + let newItems = Array(items.dropLast()) + [.navigation(stack.pop(count))] + return Stack(with: newItems) + } + + func popToRoot() -> Self { guard hasNavigation else { return self } let items = self.items.reversed().drop { !$0.hasNavigation }.reversed() guard case .navigation(let stack) = items.last else { return self } - let newItems = Array(items.dropLast()) + [.navigation(stack.pop())] + let newItems = Array(items.dropLast()) + [.navigation(stack.popToRoot())] return Stack(with: newItems) } } extension Root { - func pop() -> Root { + func pop(_ count: Int) -> Root { let stacks = self.stacks.enumerated().map { index, stack -> (NavigationItem, Stack) in guard index == current else { return stack } - return (stack.0, stack.1.pop()) + return (stack.0, stack.1.pop(count)) + } + return Root(current: current, stacks: stacks) + } + + func popToRoot() -> Root { + let stacks = self.stacks.enumerated().map { index, stack -> (NavigationItem, Stack) in + guard index == current else { return stack } + return (stack.0, stack.1.popToRoot()) } return Root(current: current, stacks: stacks) } } extension Stack where StackItem == Element { - func pop() -> Self { + func pop(_ count: Int) -> Self { guard items.count > 1 else { return self } - return Stack(with: items.dropLast()) + guard items.count > count else { return Stack(with: items.dropLast(items.count - 1)) } + return Stack(with: items.dropLast(count)) } - func empty() -> Self { - guard items.count > 0 else { return self } - return Stack(id: items[0].id) - } - -// func popToRoot() -> Self { -// guard items.count > 1 else { return self } -// return Stack(with: [items[0]]) +// func empty() -> Self { +// guard items.count > 0 else { return self } +// return Stack(id: items[0].id) // } + + func popToRoot() -> Self { + guard items.count > 1 else { return self } + return Stack(with: [items[0]]) + } } diff --git a/Sources/Reducers/SynchronizeReducer.swift b/Sources/Reducers/SynchronizeReducer.swift index 6a339cc..0d52158 100644 --- a/Sources/Reducers/SynchronizeReducer.swift +++ b/Sources/Reducers/SynchronizeReducer.swift @@ -23,7 +23,7 @@ public enum SynchronizeReducer: Reducer { } if let index = stack.items.lastIndex(where: { $0.id == action.viewID }), index == stack.items.count - 1 { - return state.pop() + return state.pop(1) // TODO: What is going on here?? } return state From 42f4ae7b1b8e4e7cb22ca3c6e6ff5448d82def49 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Wed, 18 May 2022 13:59:45 +0200 Subject: [PATCH 05/16] wip --- Sources/Views/TabContainerView.swift | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index af3c2cc..d3c016b 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -11,7 +11,6 @@ import SwiftUI import ReMVVMSwiftUI public struct TabContainerView: View { - @ReMVVM.ObservedObject private var viewState = ViewState() public init() { } @@ -29,33 +28,38 @@ public struct TabContainerView: View { } private class ViewState: ObservableObject { - @Published var currentUUID: UUID = UUID() + + @Published var currentUUID: UUID = UUID() { + didSet { + if uuidFromState != currentUUID, let tabItem = items.element(for: currentUUID)?.item as? TabNavigationItem { // user tapped + dispatcher.dispatch(action: tabItem.action) + } + } + } @Published var items: [ItemContainer] = [] - @Published private var uuidFromState: UUID = UUID() + private var uuidFromState: UUID = UUID() { + didSet { + if uuidFromState != currentUUID { + currentUUID = uuidFromState + } + } + } @ReMVVM.Dispatcher private var dispatcher @ReMVVM.State private var state: Navigation? - private var cancellables = Set() init() { - $uuidFromState - .filter { [weak self] in $0 != self?.currentUUID } - .assign(to: &$currentUUID) - - $currentUUID - .filter { [weak self] in self?.uuidFromState != $0 } - .compactMap { [weak self] in self?.items.element(for: $0)?.item as? TabNavigationItem } - .map { $0.action } - .sink { [weak self] action in self?.dispatcher.dispatch(action: action) } - .store(in: &cancellables) - $state - .map { [weak self] state in self?.items.element(for: state.root.currentItem)?.id ?? UUID() } + .combineLatest($items) { state, items in + items.element(for: state.root.currentItem)?.id ?? UUID() + } + .compactMap { $0 } .removeDuplicates() - .assign(to: &$uuidFromState) + .assignNoRetain(to: \.uuidFromState, on: self) + .store(in: &cancellables) $state .compactMap { state -> [ItemContainer]? in From a45b422d241e4bad4dee76417f5cce44a0b249a8 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Mon, 23 May 2022 09:15:44 +0200 Subject: [PATCH 06/16] wip --- Sources/Actions/NavigationActions.swift | 13 ++++++++----- Sources/Reducers/DismissModalReducer.swift | 10 +++++++++- Sources/Views/TabContainerView.swift | 8 +++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Sources/Actions/NavigationActions.swift b/Sources/Actions/NavigationActions.swift index ebdbcf3..4747121 100644 --- a/Sources/Actions/NavigationActions.swift +++ b/Sources/Actions/NavigationActions.swift @@ -22,6 +22,10 @@ public struct Push: StoreAction { } public struct Pop: StoreAction { + public enum PopMode { + case popToRoot, pop(Int) + } + public let animated: Bool public let mode: PopMode public init(mode: PopMode = .pop(1), animated: Bool = true) { @@ -30,10 +34,6 @@ public struct Pop: StoreAction { } } -public enum PopMode { - case popToRoot, pop(Int) -} - public struct Show: StoreAction { public let viewFactory: ViewFactory public let factory: ViewModelFactory? @@ -74,7 +74,10 @@ public struct ShowModal: StoreAction { } public struct DismissModal: StoreAction { - public init() { } + public let dismissAllViews: Bool + public init(dismissAllViews: Bool = false) { + self.dismissAllViews = dismissAllViews + } } public struct Synchronize: StoreAction { diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index dc5c060..814d2ea 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -10,7 +10,11 @@ import ReMVVMCore public enum DismissModalReducer: Reducer { public static func reduce(state: Navigation, with action: DismissModal) -> Navigation { - Navigation(root: state.root, modals: state.modals.dropLast()) + if action.dismissAllViews { + return Navigation(root: state.root, modals: state.modals.dropAll()) + } else { + return Navigation(root: state.root, modals: state.modals.dropLast()) + } } } @@ -19,4 +23,8 @@ extension Stack where StackItem == Modal { guard items.count > 0 else { return self } return Stack(with: items.dropLast()) } + + func dropAll() -> Self { + Stack(with: []) + } } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index d3c016b..52e6039 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -18,11 +18,9 @@ public struct TabContainerView: View { public var body: some View { TabView(selection: $viewState.currentUUID) { ForEach(viewState.items, id: \.id) { item in - NavigationView { - ContainerView(id: item.id, synchronize: false) - } - .tag(item.id) - .tabItem { item.tabItem } + NavigationView { ContainerView(id: item.id, synchronize: false) } + .tag(item.id) + .tabItem { item.tabItem } } } } From 5e33d9a204d13783df809ce2311521ca4f7bdef5 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Mon, 23 May 2022 16:18:55 +0200 Subject: [PATCH 07/16] added full screen modals --- Sources/Actions/NavigationActions.swift | 25 +++++--- Sources/Reducers/DismissModalReducer.swift | 12 ++-- Sources/Reducers/ShowModalReducer.swift | 8 +-- Sources/Reducers/SynchronizeReducer.swift | 2 +- Sources/State/Element.swift | 6 +- Sources/State/Modal.swift | 10 ++- Sources/Views/ModalContainerView.swift | 75 ++++++++++++++++------ 7 files changed, 96 insertions(+), 42 deletions(-) diff --git a/Sources/Actions/NavigationActions.swift b/Sources/Actions/NavigationActions.swift index 4747121..90c6354 100644 --- a/Sources/Actions/NavigationActions.swift +++ b/Sources/Actions/NavigationActions.swift @@ -53,30 +53,39 @@ public struct Show: StoreAction { } public struct ShowModal: StoreAction { + public enum PresentationStyle { + case sheet + case fullScreenCover + } + public let viewFactory: ViewFactory public let factory: ViewModelFactory? public let navigation: Bool - + public let presentationStyle: PresentationStyle public init(view: @autoclosure @escaping () -> V, factory: ViewModelFactory? = nil, - navigation: Bool = false) + navigation: Bool = false, + presentationStyle: PresentationStyle = .fullScreenCover) //TODO: animated ? - //TODO: modal type (fullscreen) //TODO: navigation included? - where V: View { - self.viewFactory = { AnyView(view()) } self.factory = factory self.navigation = navigation + self.presentationStyle = presentationStyle } } public struct DismissModal: StoreAction { - public let dismissAllViews: Bool - public init(dismissAllViews: Bool = false) { - self.dismissAllViews = dismissAllViews + public enum DismissMode { + case dismiss(Int) + case all + } + + public let mode: DismissMode + public init(mode: DismissMode = .dismiss(1)) { + self.mode = mode } } diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index 814d2ea..11dcab9 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -10,18 +10,20 @@ import ReMVVMCore public enum DismissModalReducer: Reducer { public static func reduce(state: Navigation, with action: DismissModal) -> Navigation { - if action.dismissAllViews { + switch action.mode { + case .dismiss(let count): + return Navigation(root: state.root, modals: state.modals.dropLast(count)) + case .all: return Navigation(root: state.root, modals: state.modals.dropAll()) - } else { - return Navigation(root: state.root, modals: state.modals.dropLast()) } } } extension Stack where StackItem == Modal { - func dropLast() -> Self { + func dropLast(_ count: Int) -> Self { guard items.count > 0 else { return self } - return Stack(with: items.dropLast()) + guard items.count >= count else { return Stack(with: items.dropLast(items.count)) } + return Stack(with: items.dropLast(count)) } func dropAll() -> Self { diff --git a/Sources/Reducers/ShowModalReducer.swift b/Sources/Reducers/ShowModalReducer.swift index 2b96624..5fd52a9 100644 --- a/Sources/Reducers/ShowModalReducer.swift +++ b/Sources/Reducers/ShowModalReducer.swift @@ -13,15 +13,15 @@ public enum ShowModalReducer: Reducer { public static func reduce(state: Navigation, with action: ShowModal) -> Navigation { let modals = state.modals.append(viewFactory: action.viewFactory, factory: action.factory ?? state.viewModelFactory, - navigation: action.navigation) + navigation: action.navigation, + presentationStyle: action.presentationStyle) return Navigation(root: state.root, modals: modals) } } extension Stack where StackItem == Modal { - - func append(viewFactory: @escaping ViewFactory, factory: ViewModelFactory, navigation: Bool) -> Self { - let element = Element(with: id, viewFactory: viewFactory, factory: factory) + func append(viewFactory: @escaping ViewFactory, factory: ViewModelFactory, navigation: Bool, presentationStyle: ShowModal.PresentationStyle) -> Self { + let element = Element(with: id, viewFactory: viewFactory, factory: factory, modalPresentationStyle: presentationStyle) let item: Modal if navigation { item = .navigation(Stack(with: [element])) diff --git a/Sources/Reducers/SynchronizeReducer.swift b/Sources/Reducers/SynchronizeReducer.swift index 0d52158..d17c392 100644 --- a/Sources/Reducers/SynchronizeReducer.swift +++ b/Sources/Reducers/SynchronizeReducer.swift @@ -12,7 +12,7 @@ public enum SynchronizeReducer: Reducer { public static func reduce(state: Navigation, with action: Synchronize) -> Navigation { if let modal = state.modals.items.last, modal.id == action.viewID { //modal swiped down - return Navigation(root: state.root, modals: state.modals.dropLast()) + return Navigation(root: state.root, modals: state.modals.dropLast(1)) } let stack: Stack diff --git a/Sources/State/Element.swift b/Sources/State/Element.swift index 9bb0791..59ba93d 100644 --- a/Sources/State/Element.swift +++ b/Sources/State/Element.swift @@ -11,12 +11,13 @@ import ReMVVMCore import SwiftUI public struct Element: Identifiable { - public let id: UUID public let viewModelFactory: ViewModelFactory //public var viewFactory: ViewFactory { container.viewFactory } public var view: AnyView { container.view } + public let modalPresentationStyle: ShowModal.PresentationStyle? + private let container: ViewContainer private final class ViewContainer { let viewFactory: ViewFactory @@ -26,9 +27,10 @@ public struct Element: Identifiable { } } - public init(with id: UUID, viewFactory: @escaping ViewFactory, factory: ViewModelFactory) { + public init(with id: UUID, viewFactory: @escaping ViewFactory, factory: ViewModelFactory, modalPresentationStyle: ShowModal.PresentationStyle? = nil) { self.id = id self.viewModelFactory = factory self.container = ViewContainer(viewFactory) + self.modalPresentationStyle = modalPresentationStyle } } diff --git a/Sources/State/Modal.swift b/Sources/State/Modal.swift index cf676c9..cec25e3 100644 --- a/Sources/State/Modal.swift +++ b/Sources/State/Modal.swift @@ -11,7 +11,6 @@ import ReMVVMCore import SwiftUI public enum Modal: Identifiable { - case single(Element) case navigation(Stack) @@ -27,6 +26,15 @@ public enum Modal: Identifiable { return true } + public var presentationStyle: ShowModal.PresentationStyle? { + switch self { + case .navigation(let stack): + return stack.items.last?.modalPresentationStyle + case .single(let element): + return element.modalPresentationStyle + } + } + public var id: UUID { switch self { case .single(let item): return item.id diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index 0f94d17..6af8d08 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -38,43 +38,57 @@ public struct ModalContainerView: View { } public var body: some View { - - viewState.view.sheet(item: $viewState.modal, content: { id in - ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) - .source(from: _dispatcher) - .onAppear() { - self.isModalActive = true - } - .onDisappear(){ - self.isModalActive = false - } - }) - .onDisappear { - guard let id = synchronizeId else { return } - dispatcher.dispatch(action: Synchronize(viewID: id)) - } + viewState.view + .sheet(item: $viewState.modal) { id in + ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) + .source(from: _dispatcher) + .onAppear() { self.isModalActive = true } + .onDisappear() { self.isModalActive = false } + } + .fullScreenCover(item: $viewState.fullScreenCover) { id in + ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) + .source(from: _dispatcher) + .onAppear() { self.isModalActive = true } + .onDisappear() { self.isModalActive = false } + } + .onDisappear { + guard let id = synchronizeId else { return } + dispatcher.dispatch(action: Synchronize(viewID: id)) + } } private enum ViewType { - case view(_: AnyView) - case id(_: UUID) + case view(AnyView) + case id(UUID) } private class ViewState: ObservableObject { @Published var isChildModalActive: Bool = false @Published var modal: UUID? + @Published var fullScreenCover: UUID? @Published private(set) var view: AnyView = Text("Empty modal").any @ReMVVM.State private var state: Navigation? init(viewType: ViewType) { - let modalPublisher: AnyPublisher + let fullScreenPublisher: AnyPublisher switch viewType { case .id(let id): - modalPublisher = $state.map { $0.modals.nextItem(for: id)?.id }.eraseToAnyPublisher() + modalPublisher = $state + .map { $0.modals.nextItem(for: id) } + .filter { $0?.presentationStyle == .sheet || $0 == nil } + .map { $0?.id } + .eraseToAnyPublisher() + + fullScreenPublisher = $state + .map { $0.modals.nextItem(for: id) } + .filter { $0?.presentationStyle == .fullScreenCover || $0 == nil } + .map { $0?.id } + .eraseToAnyPublisher() + $state .compactMap { $0.modals.item(with: id) } .removeDuplicates { $0.id == $1.id } @@ -86,7 +100,18 @@ public struct ModalContainerView: View { .assign(to: &$view) case .view(let view): - modalPublisher = $state.map { $0.modals.items.first?.id }.eraseToAnyPublisher() + modalPublisher = $state + .map { $0.modals.items.first } + .filter { $0?.presentationStyle == .sheet || $0 == nil } + .map { $0?.id } + .eraseToAnyPublisher() + + fullScreenPublisher = $state + .map { $0.modals.items.first } + .filter { $0?.presentationStyle == .fullScreenCover || $0 == nil } + .map { $0?.id } + .eraseToAnyPublisher() + self.view = view } @@ -94,9 +119,17 @@ public struct ModalContainerView: View { .combineLatest($isChildModalActive) { ($0, $1) } .filter { $0.0 != nil || $0.1 == false } .map { $0.0 } - // .filter { [unowned self] s in s != nil || childVisible == false } + // .filter { [unowned self] s in s != nil || childVisible == false } .removeDuplicates() .assign(to: &$modal) + + fullScreenPublisher + .combineLatest($isChildModalActive) { ($0, $1) } + .filter { $0.0 != nil || $0.1 == false } + .map { $0.0 } + // .filter { [unowned self] s in s != nil || childVisible == false } + .removeDuplicates() + .assign(to: &$fullScreenCover) } } } From b7fd7beae874a552e02f99031dd64ff113400b47 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Mon, 23 May 2022 16:26:47 +0200 Subject: [PATCH 08/16] wip --- Sources/Views/ModalContainerView.swift | 64 ++++++++++++-------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index 6af8d08..209c28f 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -39,7 +39,7 @@ public struct ModalContainerView: View { public var body: some View { viewState.view - .sheet(item: $viewState.modal) { id in + .sheet(item: $viewState.sheet) { id in ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) .source(from: _dispatcher) .onAppear() { self.isModalActive = true } @@ -64,7 +64,8 @@ public struct ModalContainerView: View { private class ViewState: ObservableObject { @Published var isChildModalActive: Bool = false - @Published var modal: UUID? + + @Published var sheet: UUID? @Published var fullScreenCover: UUID? @Published private(set) var view: AnyView = Text("Empty modal").any @@ -72,22 +73,19 @@ public struct ModalContainerView: View { @ReMVVM.State private var state: Navigation? init(viewType: ViewType) { - let modalPublisher: AnyPublisher - let fullScreenPublisher: AnyPublisher - switch viewType { case .id(let id): - modalPublisher = $state + $state .map { $0.modals.nextItem(for: id) } - .filter { $0?.presentationStyle == .sheet || $0 == nil } + .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } - .eraseToAnyPublisher() + .assign(to: &$sheet) - fullScreenPublisher = $state + $state .map { $0.modals.nextItem(for: id) } - .filter { $0?.presentationStyle == .fullScreenCover || $0 == nil } + .filter { $0?.presentationStyle != .sheet } .map { $0?.id } - .eraseToAnyPublisher() + .assign(to: &$fullScreenCover) $state .compactMap { $0.modals.item(with: id) } @@ -98,38 +96,36 @@ public struct ModalContainerView: View { return view.any } .assign(to: &$view) - case .view(let view): - modalPublisher = $state + $state .map { $0.modals.items.first } - .filter { $0?.presentationStyle == .sheet || $0 == nil } + .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } - .eraseToAnyPublisher() + .assign(to: &$sheet) - fullScreenPublisher = $state + $state .map { $0.modals.items.first } - .filter { $0?.presentationStyle == .fullScreenCover || $0 == nil } + .filter { $0?.presentationStyle != .sheet } .map { $0?.id } - .eraseToAnyPublisher() + .assign(to: &$fullScreenCover) self.view = view } - - modalPublisher - .combineLatest($isChildModalActive) { ($0, $1) } - .filter { $0.0 != nil || $0.1 == false } - .map { $0.0 } - // .filter { [unowned self] s in s != nil || childVisible == false } - .removeDuplicates() - .assign(to: &$modal) - - fullScreenPublisher - .combineLatest($isChildModalActive) { ($0, $1) } - .filter { $0.0 != nil || $0.1 == false } - .map { $0.0 } - // .filter { [unowned self] s in s != nil || childVisible == false } - .removeDuplicates() - .assign(to: &$fullScreenCover) +// sheetPublisher +// .combineLatest($isChildModalActive) { ($0, $1) } +// .filter { $0.0 != nil || $0.1 == false } +// .map { $0.0 } +// // .filter { [unowned self] s in s != nil || childVisible == false } +// .removeDuplicates() +// .assign(to: &$sheet) +// +// fullScreenPublisher +// .combineLatest($isChildModalActive) { ($0, $1) } +// .filter { $0.0 != nil || $0.1 == false } +// .map { $0.0 } +// // .filter { [unowned self] s in s != nil || childVisible == false } +// .removeDuplicates() +// .assign(to: &$fullScreenCover) } } } From 99deef1e76a54dea0437bd24aad44f5cbb7a1cd7 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 24 May 2022 12:03:15 +0200 Subject: [PATCH 09/16] wip --- Sources/Actions/NavigationActions.swift | 12 +++--- Sources/State/Navigation.swift | 51 ++++++++++++++++------- Sources/Views/ContainerView.swift | 24 +++++------ Sources/Views/ModalContainerView.swift | 54 +++++++++++++------------ 4 files changed, 81 insertions(+), 60 deletions(-) diff --git a/Sources/Actions/NavigationActions.swift b/Sources/Actions/NavigationActions.swift index 90c6354..6c4379f 100644 --- a/Sources/Actions/NavigationActions.swift +++ b/Sources/Actions/NavigationActions.swift @@ -43,10 +43,9 @@ public struct Show: StoreAction { view: @autoclosure @escaping () -> V, factory: ViewModelFactory? = nil, animated: Bool = true, // TODO - navigationBarHidden: Bool = true) // TODO + navigationBarHidden: Bool = true) where N: CaseIterableNavigationItem, V: View { - - self.viewFactory = { AnyView(view()) } + self.viewFactory = { AnyView(view().navigationBarHidden(navigationBarHidden)) } self.factory = factory self.item = navigationItem } @@ -68,7 +67,6 @@ public struct ShowModal: StoreAction { navigation: Bool = false, presentationStyle: PresentationStyle = .fullScreenCover) //TODO: animated ? - //TODO: navigation included? where V: View { self.viewFactory = { AnyView(view()) } self.factory = factory @@ -78,13 +76,13 @@ public struct ShowModal: StoreAction { } public struct DismissModal: StoreAction { - public enum DismissMode { + public enum Mode { case dismiss(Int) case all } - public let mode: DismissMode - public init(mode: DismissMode = .dismiss(1)) { + public let mode: Mode + public init(mode: Mode = .dismiss(1)) { self.mode = mode } } diff --git a/Sources/State/Navigation.swift b/Sources/State/Navigation.swift index 57d03d6..8a2dd0e 100644 --- a/Sources/State/Navigation.swift +++ b/Sources/State/Navigation.swift @@ -28,11 +28,8 @@ public struct Navigation { return element } - //TODO move to stack extension where Modal ? - for modal in modals.items { - if let element = modal.item(with: id) { - return element - } + if let element = modals.element(with: id) { + return element } return nil @@ -43,12 +40,10 @@ public struct Navigation { return nextId } - //TODO move to stack extension where Modal ? - for modal in modals.items { - if let nextId = modal.nextId(for: id) { - return nextId - } + if let nextId = modals.nextId(for: id) { + return nextId } + return nil } @@ -57,11 +52,8 @@ public struct Navigation { return item } - //TODO move to stack extension where Modal ? - for modal in modals.items { - if let item = modal.nextItem(for: id) { - return item - } + if let item = modals.nextElement(with: id) { + return item } return nil @@ -74,6 +66,35 @@ extension Stack where StackItem == Modal { var viewModelFactory: ViewModelFactory? { items.last?.viewModelFactory } + + func element(with id: UUID) -> Element? { + for modal in items { + if let element = modal.item(with: id) { + return element + } + } + return nil + } + + func nextElement(with id: UUID) -> Element? { + for modal in items { + if let item = modal.nextItem(for: id) { + return item + } + } + + return nil + } + + func nextId(for id: UUID) -> UUID? { + for modal in items { + if let nextId = modal.nextId(for: id) { + return nextId + } + } + + return nil + } } extension Stack where StackItem == Element { diff --git a/Sources/Views/ContainerView.swift b/Sources/Views/ContainerView.swift index f475191..0d3661c 100644 --- a/Sources/Views/ContainerView.swift +++ b/Sources/Views/ContainerView.swift @@ -28,15 +28,13 @@ public struct ContainerView: View { } public var body: some View { - VStack { - viewState.view - .onDisappear { - if synchronize { - dispatcher.dispatch(action: Synchronize(viewID: id)) - } + viewState.view + .background(linkView) + .onDisappear { + if synchronize { + dispatcher.dispatch(action: Synchronize(viewID: id)) } - linkView - } + } } private class ViewState: ObservableObject { @@ -62,12 +60,14 @@ public struct ContainerView: View { } var body: some View { - guard let view = viewState.view else { return EmptyView().any } - return NavigationLink(destination: view, tag: view.id, selection: $viewState.active) { + if let view = viewState.view { + NavigationLink(destination: view, tag: view.id, selection: $viewState.active) { EmptyView() } + .isDetailLink(false) + } else { EmptyView() - }.isDetailLink(false).any + } } - + private class ViewState: ObservableObject { @Published var active: UUID? @Published var view: ContainerView? diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index 209c28f..458f3a2 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -39,7 +39,7 @@ public struct ModalContainerView: View { public var body: some View { viewState.view - .sheet(item: $viewState.sheet) { id in + .sheet(item: $viewState.modal) { id in ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) .source(from: _dispatcher) .onAppear() { self.isModalActive = true } @@ -64,8 +64,7 @@ public struct ModalContainerView: View { private class ViewState: ObservableObject { @Published var isChildModalActive: Bool = false - - @Published var sheet: UUID? + @Published var modal: UUID? @Published var fullScreenCover: UUID? @Published private(set) var view: AnyView = Text("Empty modal").any @@ -73,19 +72,22 @@ public struct ModalContainerView: View { @ReMVVM.State private var state: Navigation? init(viewType: ViewType) { + let modalPublisher: AnyPublisher + let fullScreenPublisher: AnyPublisher + switch viewType { case .id(let id): - $state + modalPublisher = $state .map { $0.modals.nextItem(for: id) } .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } - .assign(to: &$sheet) + .eraseToAnyPublisher() - $state + fullScreenPublisher = $state .map { $0.modals.nextItem(for: id) } .filter { $0?.presentationStyle != .sheet } .map { $0?.id } - .assign(to: &$fullScreenCover) + .eraseToAnyPublisher() $state .compactMap { $0.modals.item(with: id) } @@ -96,36 +98,36 @@ public struct ModalContainerView: View { return view.any } .assign(to: &$view) + case .view(let view): - $state + modalPublisher = $state .map { $0.modals.items.first } .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } - .assign(to: &$sheet) + .eraseToAnyPublisher() - $state + fullScreenPublisher = $state .map { $0.modals.items.first } .filter { $0?.presentationStyle != .sheet } .map { $0?.id } - .assign(to: &$fullScreenCover) + .eraseToAnyPublisher() self.view = view } -// sheetPublisher -// .combineLatest($isChildModalActive) { ($0, $1) } -// .filter { $0.0 != nil || $0.1 == false } -// .map { $0.0 } -// // .filter { [unowned self] s in s != nil || childVisible == false } -// .removeDuplicates() -// .assign(to: &$sheet) -// -// fullScreenPublisher -// .combineLatest($isChildModalActive) { ($0, $1) } -// .filter { $0.0 != nil || $0.1 == false } -// .map { $0.0 } -// // .filter { [unowned self] s in s != nil || childVisible == false } -// .removeDuplicates() -// .assign(to: &$fullScreenCover) + + modalPublisher + .combineLatest($isChildModalActive) { ($0, $1) } + .filter { $0.0 != nil || $0.1 == false } + .map { $0.0 } + .removeDuplicates() + .assign(to: &$modal) + + fullScreenPublisher + .combineLatest($isChildModalActive) { ($0, $1) } + .filter { $0.0 != nil || $0.1 == false } + .map { $0.0 } + .removeDuplicates() + .assign(to: &$fullScreenCover) } } } From a3851df23c873d0e7a4e6876b0a8f46b4396ef18 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 24 May 2022 15:02:00 +0200 Subject: [PATCH 10/16] wip --- Sources/Views/ContainerView.swift | 4 +++- Sources/Views/ModalContainerView.swift | 14 +++++++------- Sources/Views/TabContainerView.swift | 18 +++++++++++++----- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Sources/Views/ContainerView.swift b/Sources/Views/ContainerView.swift index 0d3661c..010c129 100644 --- a/Sources/Views/ContainerView.swift +++ b/Sources/Views/ContainerView.swift @@ -61,7 +61,9 @@ public struct ContainerView: View { var body: some View { if let view = viewState.view { - NavigationLink(destination: view, tag: view.id, selection: $viewState.active) { EmptyView() } + NavigationLink(destination: view, + tag: view.id, + selection: $viewState.active) { EmptyView() } .isDetailLink(false) } else { EmptyView() diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index 458f3a2..635a16e 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -39,7 +39,7 @@ public struct ModalContainerView: View { public var body: some View { viewState.view - .sheet(item: $viewState.modal) { id in + .sheet(item: $viewState.sheet) { id in ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) .source(from: _dispatcher) .onAppear() { self.isModalActive = true } @@ -64,7 +64,7 @@ public struct ModalContainerView: View { private class ViewState: ObservableObject { @Published var isChildModalActive: Bool = false - @Published var modal: UUID? + @Published var sheet: UUID? @Published var fullScreenCover: UUID? @Published private(set) var view: AnyView = Text("Empty modal").any @@ -72,12 +72,12 @@ public struct ModalContainerView: View { @ReMVVM.State private var state: Navigation? init(viewType: ViewType) { - let modalPublisher: AnyPublisher + let sheetPublisher: AnyPublisher let fullScreenPublisher: AnyPublisher switch viewType { case .id(let id): - modalPublisher = $state + sheetPublisher = $state .map { $0.modals.nextItem(for: id) } .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } @@ -100,7 +100,7 @@ public struct ModalContainerView: View { .assign(to: &$view) case .view(let view): - modalPublisher = $state + sheetPublisher = $state .map { $0.modals.items.first } .filter { $0?.presentationStyle != .fullScreenCover } .map { $0?.id } @@ -115,12 +115,12 @@ public struct ModalContainerView: View { self.view = view } - modalPublisher + sheetPublisher .combineLatest($isChildModalActive) { ($0, $1) } .filter { $0.0 != nil || $0.1 == false } .map { $0.0 } .removeDuplicates() - .assign(to: &$modal) + .assign(to: &$sheet) fullScreenPublisher .combineLatest($isChildModalActive) { ($0, $1) } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 52e6039..96c8d00 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -26,11 +26,10 @@ public struct TabContainerView: View { } private class ViewState: ObservableObject { - @Published var currentUUID: UUID = UUID() { didSet { - if uuidFromState != currentUUID, let tabItem = items.element(for: currentUUID)?.item as? TabNavigationItem { // user tapped - dispatcher.dispatch(action: tabItem.action) + if oldValue == currentUUID { + self.dispatcher.dispatch(action: Pop(mode: .popToRoot)) } } } @@ -50,11 +49,20 @@ public struct TabContainerView: View { private var cancellables = Set() init() { + $currentUUID + .removeDuplicates() + .filter { self.uuidFromState != $0 } + .compactMap { self.items.element(for: $0)?.item as? TabNavigationItem } + .map { $0.action } + .sink { action in self.dispatcher.dispatch(action: action) } + .store(in: &cancellables) + $state .combineLatest($items) { state, items in - items.element(for: state.root.currentItem)?.id ?? UUID() + items.element(for: state.root.currentItem) } - .compactMap { $0 } + .filter { ($0?.item as? TabNavigationItem) != nil } + .compactMap { $0?.id } .removeDuplicates() .assignNoRetain(to: \.uuidFromState, on: self) .store(in: &cancellables) From 3782f098aee71252307c42b73f9fcec45508a6fa Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Wed, 25 May 2022 11:08:34 +0200 Subject: [PATCH 11/16] wip --- Sources/NavigationItem/TabNavigationItem.swift | 8 ++++---- Sources/Views/TabContainerView.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/NavigationItem/TabNavigationItem.swift b/Sources/NavigationItem/TabNavigationItem.swift index 53c6742..d64beaa 100644 --- a/Sources/NavigationItem/TabNavigationItem.swift +++ b/Sources/NavigationItem/TabNavigationItem.swift @@ -13,14 +13,14 @@ public typealias HashableTabNavigationItem = TabNavigationItem & Hashable public protocol TabNavigationItem: NavigationItem { var action: StoreAction { get } - var tabItemFactory: ViewFactory { get } - - var tabViewFactory: ViewFactory { get } + var tabItemFactory: AnyView { get } + + var tabViewFactory: AnyView { get } var viewModelFactory: ViewModelFactory? { get } } extension TabNavigationItem where Self: CaseIterableNavigationItem { - public var action: StoreAction { Show(on: self, view: tabViewFactory()) } + public var action: StoreAction { Show(on: self, view: tabViewFactory) } public var viewModelFactory: ViewModelFactory? { nil } public var title: String? { nil } public var icon: Image? { nil } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 96c8d00..68f7dcf 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -17,7 +17,7 @@ public struct TabContainerView: View { public var body: some View { TabView(selection: $viewState.currentUUID) { - ForEach(viewState.items, id: \.id) { item in + ForEach(viewState.items) { item in NavigationView { ContainerView(id: item.id, synchronize: false) } .tag(item.id) .tabItem { item.tabItem } @@ -26,6 +26,8 @@ public struct TabContainerView: View { } private class ViewState: ObservableObject { + + @Published var items: [ItemContainer] = [] @Published var currentUUID: UUID = UUID() { didSet { if oldValue == currentUUID { @@ -34,8 +36,6 @@ public struct TabContainerView: View { } } - @Published var items: [ItemContainer] = [] - private var uuidFromState: UUID = UUID() { didSet { if uuidFromState != currentUUID { @@ -71,7 +71,7 @@ public struct TabContainerView: View { .compactMap { state -> [ItemContainer]? in state.root.stacks.map { navItem, stack in let id = stack.items.first?.id ?? stack.id - let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory() ?? EmptyView().any + let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory ?? EmptyView().any return ItemContainer(item: navItem, tabItem: tabItem, id: id) } } From 8bb822095f48ba0fd058959d9bfaa747c4d0bb3d Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Mon, 30 May 2022 15:42:51 +0200 Subject: [PATCH 12/16] minor --- Sources/NavigationItem/TabNavigationItem.swift | 8 ++++---- Sources/Views/TabContainerView.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/NavigationItem/TabNavigationItem.swift b/Sources/NavigationItem/TabNavigationItem.swift index d64beaa..c3723e4 100644 --- a/Sources/NavigationItem/TabNavigationItem.swift +++ b/Sources/NavigationItem/TabNavigationItem.swift @@ -13,14 +13,14 @@ public typealias HashableTabNavigationItem = TabNavigationItem & Hashable public protocol TabNavigationItem: NavigationItem { var action: StoreAction { get } - var tabItemFactory: AnyView { get } + var tabItemFactory: () -> AnyView { get } - var tabViewFactory: AnyView { get } + var tabViewFactory: () -> AnyView { get } var viewModelFactory: ViewModelFactory? { get } } -extension TabNavigationItem where Self: CaseIterableNavigationItem { - public var action: StoreAction { Show(on: self, view: tabViewFactory) } +extension TabNavigationItem where Self: CaseIterableNavigationItem { + public var action: StoreAction { Show(on: self, view: tabViewFactory()) } public var viewModelFactory: ViewModelFactory? { nil } public var title: String? { nil } public var icon: Image? { nil } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 68f7dcf..798a9a8 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -71,7 +71,7 @@ public struct TabContainerView: View { .compactMap { state -> [ItemContainer]? in state.root.stacks.map { navItem, stack in let id = stack.items.first?.id ?? stack.id - let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory ?? EmptyView().any + let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory() ?? EmptyView().any return ItemContainer(item: navItem, tabItem: tabItem, id: id) } } From b5ad76c48252a40b1190223f733f17a8b4caaa8d Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Wed, 1 Jun 2022 14:00:35 +0200 Subject: [PATCH 13/16] wip --- Sources/ReMVVMExtension.swift | 51 +++++++++++++++---- Sources/Reducers/DismissModalReducer.swift | 1 + Sources/State/AppNavigationState.swift | 6 ++- Sources/State/NavigationState.swift | 1 + Sources/Views/ModalContainerView.swift | 26 +++++----- Sources/Views/RootContainerView.swift | 6 +-- Sources/Views/TabContainerView.swift | 59 +++++++++++++++++++++- 7 files changed, 120 insertions(+), 30 deletions(-) diff --git a/Sources/ReMVVMExtension.swift b/Sources/ReMVVMExtension.swift index 3b94505..17296ce 100644 --- a/Sources/ReMVVMExtension.swift +++ b/Sources/ReMVVMExtension.swift @@ -7,33 +7,33 @@ // import ReMVVMCore +import SwiftUI private enum AppNavigationReducer: Reducer where R: Reducer, R.State == State, R.Action == StoreAction { - static func reduce(state: AppNavigationState, with action: StoreAction) -> AppNavigationState { - AppNavigationState( - appState: R.reduce(state: state.appState, with: action), - navigation: NavigationReducer.reduce(state: state.navigation, with: action) - ) + AppNavigationState(appState: R.reduce(state: state.appState, with: action), + navigation: NavigationReducer.reduce(state: state.navigation, with: action), + uiConfig: state.uiConfig) } } extension ReMVVM { - public static func initialize(with state: State, - stateMappers: [StateMapper] = [], - reducer: R.Type, - middleware: [AnyMiddleware] = []) -> Store> + stateMappers: [StateMapper] = [], + uiStateConfig: UIStateConfig, + reducer: R.Type, + middleware: [AnyMiddleware] = []) -> Store> where R: Reducer, R.Action == StoreAction, R.State == State { let mappers: [StateMapper>] = [ StateMapper(for: \.appState), - StateMapper(for: \.navigation) + StateMapper(for: \.navigation), + StateMapper(for: \.uiConfig) ] let stateMappers = mappers + stateMappers.map { $0.map(with: \.appState) } - return self.initialize(with: AppNavigationState(appState: state), + return self.initialize(with: AppNavigationState(appState: state, uiConfig: uiStateConfig), stateMappers: stateMappers, reducer: AppNavigationReducer.self, middleware: middleware) @@ -54,3 +54,32 @@ extension ReMVVM { return store } } + + +public struct UIStateConfig { + //let initialController: () -> UIViewController + //let navigationController: () -> UINavigationController + let navigationConfigs: NavigationConfig + //let navigationBarHidden: Bool + + public init(//initialController: @escaping @autoclosure () -> UIViewController, + //navigationController: (() -> UINavigationController)? = nil, + navigationConfigs: NavigationConfig + //navigationBarHidden: Bool = true + ) { + //self.initialController = initialController + //self.navigationController = navigationController ?? { UINavigationController() } + self.navigationConfigs = navigationConfigs + //self.navigationBarHidden = navigationBarHidden + } +} + +public struct NavigationConfig { + public typealias TabBarFactory = (_ items: [AnyView], _ selectedIndex: Binding) -> AnyView + + public var tabBarFactory: TabBarFactory + + public init(tabBarFactory: @escaping TabBarFactory) { + self.tabBarFactory = tabBarFactory + } +} diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index 11dcab9..4facff8 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -7,6 +7,7 @@ // import ReMVVMCore +import UIKit public enum DismissModalReducer: Reducer { public static func reduce(state: Navigation, with action: DismissModal) -> Navigation { diff --git a/Sources/State/AppNavigationState.swift b/Sources/State/AppNavigationState.swift index 807f30c..c2ed66b 100644 --- a/Sources/State/AppNavigationState.swift +++ b/Sources/State/AppNavigationState.swift @@ -9,8 +9,8 @@ import ReMVVMCore public struct AppNavigationState: NavigationState { - public let navigation: Navigation + public let uiConfig: UIStateConfig public let appState: State @@ -26,8 +26,10 @@ public struct AppNavigationState: NavigationState { } public init(appState: State, - navigation: Navigation = Navigation(root: Root(stack: Stack()))) { + navigation: Navigation = Navigation(root: Root(stack: Stack())), + uiConfig: UIStateConfig) { self.appState = appState self.navigation = navigation + self.uiConfig = uiConfig } } diff --git a/Sources/State/NavigationState.swift b/Sources/State/NavigationState.swift index 69c9c2f..ca86626 100644 --- a/Sources/State/NavigationState.swift +++ b/Sources/State/NavigationState.swift @@ -10,4 +10,5 @@ import ReMVVMCore public protocol NavigationState: StoreState { var navigation: Navigation { get } + var uiConfig: UIStateConfig { get } } diff --git a/Sources/Views/ModalContainerView.swift b/Sources/Views/ModalContainerView.swift index 635a16e..c683791 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -39,18 +39,20 @@ public struct ModalContainerView: View { public var body: some View { viewState.view - .sheet(item: $viewState.sheet) { id in - ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) - .source(from: _dispatcher) - .onAppear() { self.isModalActive = true } - .onDisappear() { self.isModalActive = false } - } - .fullScreenCover(item: $viewState.fullScreenCover) { id in - ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) - .source(from: _dispatcher) - .onAppear() { self.isModalActive = true } - .onDisappear() { self.isModalActive = false } - } + .background(EmptyView() + .sheet(item: $viewState.sheet) { id in + ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) + .source(from: _dispatcher) + .onAppear() { self.isModalActive = true } + .onDisappear() { self.isModalActive = false } + }) + .background(EmptyView() + .fullScreenCover(item: $viewState.fullScreenCover) { id in + ModalContainerView(id: id, isModalActive: $viewState.isChildModalActive) + .source(from: _dispatcher) + .onAppear() { self.isModalActive = true } + .onDisappear() { self.isModalActive = false } + }) .onDisappear { guard let id = synchronizeId else { return } dispatcher.dispatch(action: Synchronize(viewID: id)) diff --git a/Sources/Views/RootContainerView.swift b/Sources/Views/RootContainerView.swift index 01298cf..dc9cc5a 100644 --- a/Sources/Views/RootContainerView.swift +++ b/Sources/Views/RootContainerView.swift @@ -18,10 +18,8 @@ public struct RootContainerView: View { public init() { } public var body: some View { - VStack { - NavigationView { - ContainerView(id: id, synchronize: false) - } + NavigationView { + ContainerView(id: id, synchronize: false) } } } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 798a9a8..ef4d7e3 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -13,6 +13,8 @@ import ReMVVMSwiftUI public struct TabContainerView: View { @ReMVVM.ObservedObject private var viewState = ViewState() + @Namespace private var tabBarNamespace + public init() { } public var body: some View { @@ -20,9 +22,13 @@ public struct TabContainerView: View { ForEach(viewState.items) { item in NavigationView { ContainerView(id: item.id, synchronize: false) } .tag(item.id) - .tabItem { item.tabItem } + .navigationViewStyle(.stack) } } + .overlay(viewState.tabBarFactory?(viewState.items.map { $0.tabItem }, + $viewState.currentIndex), + alignment: .bottom) + .ignoresSafeArea() } private class ViewState: ObservableObject { @@ -35,6 +41,9 @@ public struct TabContainerView: View { } } } + @Published var currentIndex: Int = 0 + + @Published var tabBarFactory: NavigationConfig.TabBarFactory? = nil private var uuidFromState: UUID = UUID() { didSet { @@ -46,9 +55,28 @@ public struct TabContainerView: View { @ReMVVM.Dispatcher private var dispatcher @ReMVVM.State private var state: Navigation? + @ReMVVM.State private var uiStateConfig: UIStateConfig? + private var cancellables = Set() init() { + $currentUUID + .removeDuplicates() + .combineLatest($items) { id, items in + items.firstIndex { $0.id == id } + } + .compactMap { $0 } + .assign(to: &$currentIndex) + + $currentIndex + .removeDuplicates() + .combineLatest($items) { index, items -> UUID? in + guard items.indices.contains(index) else { return nil } + return items[index].id + } + .compactMap { $0 } + .assign(to: &$currentUUID) + $currentUUID .removeDuplicates() .filter { self.uuidFromState != $0 } @@ -77,6 +105,10 @@ public struct TabContainerView: View { } .prefix(1) //take only first value, next value will be handled by parent view .assign(to: &$items) + + $uiStateConfig + .compactMap { $0.navigationConfigs.tabBarFactory } + .assign(to: &$tabBarFactory) } } } @@ -85,6 +117,10 @@ private struct ItemContainer: Identifiable { let item: NavigationItem let tabItem: AnyView let id: UUID + +// static var preview: ItemContainer { +// .init(item: PreviewNavigationTab.home, tabItem: PreviewNavigationTab.home.tabItemFactory(), id: UUID()) +// } } extension Array where Element == ItemContainer { @@ -96,3 +132,24 @@ extension Array where Element == ItemContainer { first { $0.item.isEqual(to: item) } } } + +enum PreviewNavigationTab: String, TabNavigationItem, CaseIterableNavigationItem { + case home = "Home" + case tests = "Tests" + case profile = "Profile" + + + public var tabViewFactory: () -> AnyView { + switch self { + case .home: return { Text("HOME").any } + case .tests: return { Text("TESTS").any } + case .profile: return { Text("PROFILE").any } + } + } + + public var tabItemFactory: () -> AnyView { + switch self { + default: return { Image(uiImage: .add).any } + } + } +} From 79a3e28d80e7daae57b8786acf4a4ee12a8fb418 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 7 Jun 2022 09:10:48 +0200 Subject: [PATCH 14/16] custom tab bar added --- Sources/Helpers/Binding.swift | 23 ++++++++++ Sources/ReMVVMExtension.swift | 22 +++------- Sources/Views/TabContainerView.swift | 63 ++++++---------------------- 3 files changed, 42 insertions(+), 66 deletions(-) create mode 100644 Sources/Helpers/Binding.swift diff --git a/Sources/Helpers/Binding.swift b/Sources/Helpers/Binding.swift new file mode 100644 index 0000000..6d8a763 --- /dev/null +++ b/Sources/Helpers/Binding.swift @@ -0,0 +1,23 @@ +// +// File.swift +// +// +// Created by Paweł Zgoda-Ferchmin on 01/06/2022. +// + +import Combine +import SwiftUI + +extension Binding { + func map(get: @escaping (Value) -> T?, set: @escaping (T?) -> Value?) -> Binding { + Binding(get: { get(wrappedValue) }, + set: { wrappedValue = set($0) ?? wrappedValue }) + } +} + +extension Array { + func with(_ index: Array.Index?) -> Array.Element? { + guard let index = index, indices.contains(index) else { return nil } + return self[index] + } +} diff --git a/Sources/ReMVVMExtension.swift b/Sources/ReMVVMExtension.swift index 17296ce..a6ec988 100644 --- a/Sources/ReMVVMExtension.swift +++ b/Sources/ReMVVMExtension.swift @@ -57,29 +57,19 @@ extension ReMVVM { public struct UIStateConfig { - //let initialController: () -> UIViewController - //let navigationController: () -> UINavigationController - let navigationConfigs: NavigationConfig - //let navigationBarHidden: Bool + let navigationConfig: NavigationConfig - public init(//initialController: @escaping @autoclosure () -> UIViewController, - //navigationController: (() -> UINavigationController)? = nil, - navigationConfigs: NavigationConfig - //navigationBarHidden: Bool = true - ) { - //self.initialController = initialController - //self.navigationController = navigationController ?? { UINavigationController() } - self.navigationConfigs = navigationConfigs - //self.navigationBarHidden = navigationBarHidden + public init(navigationConfig: NavigationConfig) { + self.navigationConfig = navigationConfig } } public struct NavigationConfig { - public typealias TabBarFactory = (_ items: [AnyView], _ selectedIndex: Binding) -> AnyView + public typealias TabBarFactory = (_ items: [TabNavigationItem], _ selectedIndex: Binding) -> AnyView public var tabBarFactory: TabBarFactory - public init(tabBarFactory: @escaping TabBarFactory) { - self.tabBarFactory = tabBarFactory + public init(tabBarFactory: @escaping (_ items: [TabNavigationItem], _ selectedIndex: Binding) -> V) where V: View { + self.tabBarFactory = { tabBarFactory($0, $1).any } } } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index ef4d7e3..e847837 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -15,24 +15,30 @@ public struct TabContainerView: View { @Namespace private var tabBarNamespace + private var currentIndex: Binding { + $viewState.currentUUID + .map(get: { id in viewState.items.firstIndex { $0.id == id } }, + set: { viewState.items.with($0)?.id }) + } + public init() { } public var body: some View { TabView(selection: $viewState.currentUUID) { - ForEach(viewState.items) { item in + ForEach(viewState.items) { item in NavigationView { ContainerView(id: item.id, synchronize: false) } .tag(item.id) + .tabItem { viewState.tabBarFactory == nil ? item.tabItem : nil } .navigationViewStyle(.stack) } } - .overlay(viewState.tabBarFactory?(viewState.items.map { $0.tabItem }, - $viewState.currentIndex), + .overlay(viewState.tabBarFactory?(viewState.items.compactMap { $0.item as? TabNavigationItem }, + currentIndex), alignment: .bottom) .ignoresSafeArea() } private class ViewState: ObservableObject { - @Published var items: [ItemContainer] = [] @Published var currentUUID: UUID = UUID() { didSet { @@ -41,7 +47,6 @@ public struct TabContainerView: View { } } } - @Published var currentIndex: Int = 0 @Published var tabBarFactory: NavigationConfig.TabBarFactory? = nil @@ -60,23 +65,6 @@ public struct TabContainerView: View { private var cancellables = Set() init() { - $currentUUID - .removeDuplicates() - .combineLatest($items) { id, items in - items.firstIndex { $0.id == id } - } - .compactMap { $0 } - .assign(to: &$currentIndex) - - $currentIndex - .removeDuplicates() - .combineLatest($items) { index, items -> UUID? in - guard items.indices.contains(index) else { return nil } - return items[index].id - } - .compactMap { $0 } - .assign(to: &$currentUUID) - $currentUUID .removeDuplicates() .filter { self.uuidFromState != $0 } @@ -99,7 +87,7 @@ public struct TabContainerView: View { .compactMap { state -> [ItemContainer]? in state.root.stacks.map { navItem, stack in let id = stack.items.first?.id ?? stack.id - let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory() ?? EmptyView().any + let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory() return ItemContainer(item: navItem, tabItem: tabItem, id: id) } } @@ -107,7 +95,7 @@ public struct TabContainerView: View { .assign(to: &$items) $uiStateConfig - .compactMap { $0.navigationConfigs.tabBarFactory } + .compactMap { $0.navigationConfig.tabBarFactory } .assign(to: &$tabBarFactory) } } @@ -115,12 +103,8 @@ public struct TabContainerView: View { private struct ItemContainer: Identifiable { let item: NavigationItem - let tabItem: AnyView + let tabItem: AnyView? let id: UUID - -// static var preview: ItemContainer { -// .init(item: PreviewNavigationTab.home, tabItem: PreviewNavigationTab.home.tabItemFactory(), id: UUID()) -// } } extension Array where Element == ItemContainer { @@ -132,24 +116,3 @@ extension Array where Element == ItemContainer { first { $0.item.isEqual(to: item) } } } - -enum PreviewNavigationTab: String, TabNavigationItem, CaseIterableNavigationItem { - case home = "Home" - case tests = "Tests" - case profile = "Profile" - - - public var tabViewFactory: () -> AnyView { - switch self { - case .home: return { Text("HOME").any } - case .tests: return { Text("TESTS").any } - case .profile: return { Text("PROFILE").any } - } - } - - public var tabItemFactory: () -> AnyView { - switch self { - default: return { Image(uiImage: .add).any } - } - } -} From 4433080019c5c04e701c00af12b91fa4111399bb Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Thu, 11 Aug 2022 15:23:34 +0200 Subject: [PATCH 15/16] Cleanup --- Sources/ReMVVMExtension.swift | 4 ++-- Sources/Views/TabContainerView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ReMVVMExtension.swift b/Sources/ReMVVMExtension.swift index a6ec988..52a8b64 100644 --- a/Sources/ReMVVMExtension.swift +++ b/Sources/ReMVVMExtension.swift @@ -57,9 +57,9 @@ extension ReMVVM { public struct UIStateConfig { - let navigationConfig: NavigationConfig + let navigationConfig: NavigationConfig? - public init(navigationConfig: NavigationConfig) { + public init(navigationConfig: NavigationConfig?) { self.navigationConfig = navigationConfig } } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index e847837..34534bb 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -95,7 +95,7 @@ public struct TabContainerView: View { .assign(to: &$items) $uiStateConfig - .compactMap { $0.navigationConfig.tabBarFactory } + .compactMap { $0.navigationConfig?.tabBarFactory } .assign(to: &$tabBarFactory) } } From f643c282d51f54a01be6b62a413db091cbff6ff1 Mon Sep 17 00:00:00 2001 From: Pawel Zgoda-Ferchmin Date: Tue, 23 Aug 2022 15:01:27 +0200 Subject: [PATCH 16/16] added mock tab bar + tab bar improvements --- Sources/NavigationItem/TabNavigationItem.swift | 5 ++--- Sources/Reducers/DismissModalReducer.swift | 2 +- Sources/Reducers/SynchronizeReducer.swift | 10 ++++------ Sources/State/AppNavigationState.swift | 9 +-------- Sources/State/Root.swift | 4 ++-- Sources/Views/TabContainerView.swift | 13 +++++++------ 6 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Sources/NavigationItem/TabNavigationItem.swift b/Sources/NavigationItem/TabNavigationItem.swift index c3723e4..a898530 100644 --- a/Sources/NavigationItem/TabNavigationItem.swift +++ b/Sources/NavigationItem/TabNavigationItem.swift @@ -6,15 +6,14 @@ // Copyright © 2021 Dariusz Grzeszczak. All rights reserved. // -import SwiftUI import ReMVVMCore +import SwiftUI public typealias HashableTabNavigationItem = TabNavigationItem & Hashable public protocol TabNavigationItem: NavigationItem { var action: StoreAction { get } - var tabItemFactory: () -> AnyView { get } - + var tabItemFactory: (Bool) -> AnyView { get } var tabViewFactory: () -> AnyView { get } var viewModelFactory: ViewModelFactory? { get } } diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index 4facff8..4189f7d 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -22,7 +22,7 @@ public enum DismissModalReducer: Reducer { extension Stack where StackItem == Modal { func dropLast(_ count: Int) -> Self { - guard items.count > 0 else { return self } + guard !items.isEmpty else { return self } guard items.count >= count else { return Stack(with: items.dropLast(items.count)) } return Stack(with: items.dropLast(count)) } diff --git a/Sources/Reducers/SynchronizeReducer.swift b/Sources/Reducers/SynchronizeReducer.swift index d17c392..6e664f0 100644 --- a/Sources/Reducers/SynchronizeReducer.swift +++ b/Sources/Reducers/SynchronizeReducer.swift @@ -9,15 +9,14 @@ import ReMVVMCore public enum SynchronizeReducer: Reducer { - public static func reduce(state: Navigation, with action: Synchronize) -> Navigation { - - if let modal = state.modals.items.last, modal.id == action.viewID { //modal swiped down + public static func reduce(state: Navigation, with action: Synchronize) -> Navigation { + if let modal = state.modals.items.last, modal.id == action.viewID { // modal swiped down return Navigation(root: state.root, modals: state.modals.dropLast(1)) } let stack: Stack - if case .navigation(let s) = state.modals.items.last { - stack = s + if case .navigation(let navigationStack) = state.modals.items.last { + stack = navigationStack } else { stack = state.root.currentStack } @@ -29,4 +28,3 @@ public enum SynchronizeReducer: Reducer { return state } } - diff --git a/Sources/State/AppNavigationState.swift b/Sources/State/AppNavigationState.swift index c2ed66b..06b567a 100644 --- a/Sources/State/AppNavigationState.swift +++ b/Sources/State/AppNavigationState.swift @@ -15,14 +15,7 @@ public struct AppNavigationState: NavigationState { public let appState: State public var factory: ViewModelFactory { - let factory: CompositeViewModelFactory - if let f = navigation.viewModelFactory as? CompositeViewModelFactory { - factory = f - } else { - factory = CompositeViewModelFactory(with: navigation.viewModelFactory) - } - - return factory + (navigation.viewModelFactory as? CompositeViewModelFactory) ?? .init(with: navigation.viewModelFactory) } public init(appState: State, diff --git a/Sources/State/Root.swift b/Sources/State/Root.swift index e8dd611..7496a3e 100644 --- a/Sources/State/Root.swift +++ b/Sources/State/Root.swift @@ -26,8 +26,8 @@ public struct Root { } public init(current: Int, stacks: [(T, Stack)]) where T: CaseIterableNavigationItem { - if stacks.count > 0 { - self.init(stack : Stack()) + if !stacks.isEmpty { + self.init(stack: Stack()) } else { self.init(current: current, stacks: stacks.map { ($0, $1) }) } diff --git a/Sources/Views/TabContainerView.swift b/Sources/Views/TabContainerView.swift index 34534bb..6a7b250 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -28,14 +28,15 @@ public struct TabContainerView: View { ForEach(viewState.items) { item in NavigationView { ContainerView(id: item.id, synchronize: false) } .tag(item.id) - .tabItem { viewState.tabBarFactory == nil ? item.tabItem : nil } + .tabItem { + viewState.tabBarFactory == nil ? item.tabItem?(viewState.items.firstIndex { $0.id == item.id } == currentIndex.wrappedValue) : nil + } .navigationViewStyle(.stack) } } .overlay(viewState.tabBarFactory?(viewState.items.compactMap { $0.item as? TabNavigationItem }, currentIndex), alignment: .bottom) - .ignoresSafeArea() } private class ViewState: ObservableObject { @@ -48,7 +49,7 @@ public struct TabContainerView: View { } } - @Published var tabBarFactory: NavigationConfig.TabBarFactory? = nil + @Published var tabBarFactory: NavigationConfig.TabBarFactory? private var uuidFromState: UUID = UUID() { didSet { @@ -87,8 +88,8 @@ public struct TabContainerView: View { .compactMap { state -> [ItemContainer]? in state.root.stacks.map { navItem, stack in let id = stack.items.first?.id ?? stack.id - let tabItem = (navItem as? TabNavigationItem)?.tabItemFactory() - return ItemContainer(item: navItem, tabItem: tabItem, id: id) + let tabItemFactory = (navItem as? TabNavigationItem)?.tabItemFactory + return ItemContainer(item: navItem, tabItem: tabItemFactory, id: id) } } .prefix(1) //take only first value, next value will be handled by parent view @@ -103,7 +104,7 @@ public struct TabContainerView: View { private struct ItemContainer: Identifiable { let item: NavigationItem - let tabItem: AnyView? + let tabItem: ((Bool) -> AnyView)? let id: UUID }