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/Actions/NavigationActions.swift b/Sources/Actions/NavigationActions.swift index e0aae16..6c4379f 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 enum PopMode { + case popToRoot, pop(Int) + } + + public let animated: Bool + public let mode: PopMode + public init(mode: PopMode = .pop(1), animated: Bool = true) { + self.mode = mode + self.animated = animated + } +} public struct Show: StoreAction { public let viewFactory: ViewFactory @@ -34,38 +43,48 @@ 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 } } 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 init() { } + public enum Mode { + case dismiss(Int) + case all + } + + public let mode: Mode + public init(mode: Mode = .dismiss(1)) { + self.mode = mode + } } public struct Synchronize: StoreAction { 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/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/NavigationItem/TabNavigationItem.swift b/Sources/NavigationItem/TabNavigationItem.swift index 53c6742..a898530 100644 --- a/Sources/NavigationItem/TabNavigationItem.swift +++ b/Sources/NavigationItem/TabNavigationItem.swift @@ -6,20 +6,19 @@ // 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: ViewFactory { get } - - var tabViewFactory: ViewFactory { get } + var tabItemFactory: (Bool) -> AnyView { get } + var tabViewFactory: () -> AnyView { get } var viewModelFactory: ViewModelFactory? { get } } -extension TabNavigationItem where Self: CaseIterableNavigationItem { +extension TabNavigationItem where Self: CaseIterableNavigationItem { public var action: StoreAction { Show(on: self, view: tabViewFactory()) } public var viewModelFactory: ViewModelFactory? { nil } public var title: String? { nil } diff --git a/Sources/ReMVVMExtension.swift b/Sources/ReMVVMExtension.swift index 3b94505..52a8b64 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,22 @@ extension ReMVVM { return store } } + + +public struct UIStateConfig { + let navigationConfig: NavigationConfig? + + public init(navigationConfig: NavigationConfig?) { + self.navigationConfig = navigationConfig + } +} + +public struct NavigationConfig { + public typealias TabBarFactory = (_ items: [TabNavigationItem], _ selectedIndex: Binding) -> AnyView + + public var tabBarFactory: TabBarFactory + + public init(tabBarFactory: @escaping (_ items: [TabNavigationItem], _ selectedIndex: Binding) -> V) where V: View { + self.tabBarFactory = { tabBarFactory($0, $1).any } + } +} diff --git a/Sources/Reducers/DismissModalReducer.swift b/Sources/Reducers/DismissModalReducer.swift index 628f4d3..4189f7d 100644 --- a/Sources/Reducers/DismissModalReducer.swift +++ b/Sources/Reducers/DismissModalReducer.swift @@ -7,17 +7,27 @@ // import ReMVVMCore +import UIKit public enum DismissModalReducer: Reducer { public static func reduce(state: Navigation, with action: DismissModal) -> Navigation { - Navigation(root: state.root, modals: state.modals.dropLast()) + 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()) + } } } extension Stack where StackItem == Modal { + func dropLast(_ count: Int) -> 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)) + } - func dropLast() -> Self { - guard items.count > 0 else { return self } - return Stack(with: items.dropLast()) + func dropAll() -> Self { + Stack(with: []) } } 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..aa0dd99 100644 --- a/Sources/Reducers/PopReducer.swift +++ b/Sources/Reducers/PopReducer.swift @@ -10,17 +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(_ count: Int) -> Navigation { + if modals.hasNavigation { + return Navigation(root: root, modals: modals.pop(count)) + } else { + return Navigation(root: root.pop(count)) + } + } - func pop() -> Navigation { + 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()) } } } @@ -29,40 +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(_ 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(count)) + } + return Root(current: current, stacks: stacks) + } - func pop() -> Root { + 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.pop()) + 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/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/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/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/Reducers/SynchronizeReducer.swift b/Sources/Reducers/SynchronizeReducer.swift index 6a339cc..6e664f0 100644 --- a/Sources/Reducers/SynchronizeReducer.swift +++ b/Sources/Reducers/SynchronizeReducer.swift @@ -9,24 +9,22 @@ 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 - return Navigation(root: state.root, modals: state.modals.dropLast()) + 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 } 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 } } - diff --git a/Sources/State/AppNavigationState.swift b/Sources/State/AppNavigationState.swift index 807f30c..06b567a 100644 --- a/Sources/State/AppNavigationState.swift +++ b/Sources/State/AppNavigationState.swift @@ -9,25 +9,20 @@ import ReMVVMCore public struct AppNavigationState: NavigationState { - public let navigation: Navigation + public let uiConfig: UIStateConfig 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, - 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/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/State/Navigation.swift b/Sources/State/Navigation.swift index 65c9436..8a2dd0e 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? { @@ -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 @@ -72,12 +64,41 @@ public struct Navigation { extension Stack where StackItem == Modal { var viewModelFactory: ViewModelFactory? { - return items.last?.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 { var viewModelFactory: ViewModelFactory? { - return items.last?.viewModelFactory + items.last?.viewModelFactory } } diff --git a/Sources/State/NavigationState.swift b/Sources/State/NavigationState.swift index 7b261f9..ca86626 100644 --- a/Sources/State/NavigationState.swift +++ b/Sources/State/NavigationState.swift @@ -9,6 +9,6 @@ import ReMVVMCore public protocol NavigationState: StoreState { - var navigation: Navigation { get } + var uiConfig: UIStateConfig { get } } 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/ContainerView.swift b/Sources/Views/ContainerView.swift index 70aa79d..010c129 100644 --- a/Sources/Views/ContainerView.swift +++ b/Sources/Views/ContainerView.swift @@ -28,31 +28,26 @@ 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 { @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) - } + .map { $0.item(with: id) } .filter { $0 != nil } .compactMap { $0?.view } - .assignNoRetain(to: \.view, on: self) - .store(in: &cancellables) + .assign(to: &$view) } } @@ -65,18 +60,21 @@ 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? @ReMVVM.State private var state: Navigation? - private var cancellables = Set() private let id: UUID init(id: UUID) { @@ -85,15 +83,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) } } } 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..c683791 100644 --- a/Sources/Views/ModalContainerView.swift +++ b/Sources/Views/ModalContainerView.swift @@ -38,44 +38,59 @@ 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 + .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)) + } } 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 sheet: UUID? + @Published var fullScreenCover: UUID? @Published private(set) var view: AnyView = Text("Empty modal").any @ReMVVM.State private var state: Navigation? - private var cancellables = Set() init(viewType: ViewType) { - - let modalPublisher: AnyPublisher + let sheetPublisher: AnyPublisher + let fullScreenPublisher: AnyPublisher switch viewType { case .id(let id): - modalPublisher = $state.map { $0.modals.nextItem(for: id)?.id }.eraseToAnyPublisher() + sheetPublisher = $state + .map { $0.modals.nextItem(for: id) } + .filter { $0?.presentationStyle != .fullScreenCover } + .map { $0?.id } + .eraseToAnyPublisher() + + fullScreenPublisher = $state + .map { $0.modals.nextItem(for: id) } + .filter { $0?.presentationStyle != .sheet } + .map { $0?.id } + .eraseToAnyPublisher() + $state .compactMap { $0.modals.item(with: id) } .removeDuplicates { $0.id == $1.id } @@ -84,22 +99,37 @@ 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() + sheetPublisher = $state + .map { $0.modals.items.first } + .filter { $0?.presentationStyle != .fullScreenCover } + .map { $0?.id } + .eraseToAnyPublisher() + + fullScreenPublisher = $state + .map { $0.modals.items.first } + .filter { $0?.presentationStyle != .sheet } + .map { $0?.id } + .eraseToAnyPublisher() + self.view = view } - modalPublisher + sheetPublisher + .combineLatest($isChildModalActive) { ($0, $1) } + .filter { $0.0 != nil || $0.1 == false } + .map { $0.0 } + .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() - .assignNoRetain(to: \.modal, on: self) - .store(in: &cancellables) + .assign(to: &$fullScreenCover) } } } 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 03f18b0..6a7b250 100644 --- a/Sources/Views/TabContainerView.swift +++ b/Sources/Views/TabContainerView.swift @@ -11,35 +11,45 @@ import SwiftUI import ReMVVMSwiftUI public struct TabContainerView: View { + @ReMVVM.ObservedObject private var viewState = ViewState() + @Namespace private var tabBarNamespace - @ReMVVM.ObservedObject private var viewState = ViewState() + 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, id: \.id) { item in - NavigationView { - ContainerView(id: item.id, synchronize: false) - } - .tag(item.id) - .tabItem { item.tabItem } + ForEach(viewState.items) { item in + NavigationView { ContainerView(id: item.id, synchronize: false) } + .tag(item.id) + .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) } private class ViewState: ObservableObject { - + @Published var items: [ItemContainer] = [] @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)) } } } - @Published var items: [ItemContainer] = [] + + @Published var tabBarFactory: NavigationConfig.TabBarFactory? private var uuidFromState: UUID = UUID() { didSet { @@ -51,15 +61,25 @@ 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() + .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) @@ -68,28 +88,29 @@ 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 - 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 - .assignNoRetain(to: \.items, on: self) - .store(in: &cancellables) + .assign(to: &$items) + + $uiStateConfig + .compactMap { $0.navigationConfig?.tabBarFactory } + .assign(to: &$tabBarFactory) } } } private struct ItemContainer: Identifiable { let item: NavigationItem - let tabItem: AnyView + let tabItem: ((Bool) -> AnyView)? let id: UUID } 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? {