Skip to content

Commit

Permalink
Module Support (#45)
Browse files Browse the repository at this point in the history
Co-authored-by: Nick <[email protected]>
  • Loading branch information
SwiftfulThinking and swiftfulthinking-llc committed Jan 23, 2024
1 parent a31c28b commit f95e323
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 39 deletions.
64 changes: 64 additions & 0 deletions Sources/SwiftfulRouting/Components/ModuleSupportView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// ModuleSupportView.swift
//
//
// Created by Nick Sarno on 1/21/24.
//

import Foundation
import SwiftUI

struct ModuleSupportView<Content:View>: View {

let addNavigationView: Bool
let moduleDelegate: ModuleDelegate
let screens: Binding<[AnyDestination]>?

@Binding var selection: AnyTransitionWithDestination
let modules: [AnyTransitionWithDestination]
@ViewBuilder var content: (AnyRouter) -> Content
let currentTransition: TransitionOption

var body: some View {
ZStack {
LazyZStack(allowSimultaneous: false, selection: selection, items: modules) { data in
if data == modules.first {
RouterViewInternal(
addNavigationView: addNavigationView,
moduleDelegate: moduleDelegate,
screens: screens,
route: nil,
routes: nil,
environmentRouter: nil,
content: content
)
.transition(
.asymmetric(
insertion: currentTransition.insertion,
removal: .customRemoval(direction: currentTransition.reversed)
)
)
} else {
RouterViewInternal(
addNavigationView: addNavigationView,
moduleDelegate: moduleDelegate,
screens: screens,
route: nil,
routes: nil,
environmentRouter: nil,
content: { router in
data.destination(router).destination
}
)
.transition(
.asymmetric(
insertion: currentTransition.insertion,
removal: .customRemoval(direction: currentTransition.reversed)
)
)
}
}
.animation(.easeInOut, value: selection.id)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ struct TransitionSupportView<Content:View>: View {
let router: AnyRouter
@Binding var selection: AnyTransitionWithDestination
let transitions: [AnyTransitionWithDestination]
@ViewBuilder var content: Content
@ViewBuilder var content: (AnyRouter) -> Content
let currentTransition: TransitionOption

// problem is that when .transition changes, view re-renders and re-appears. Need to update the view's transition but not re-render the view

var body: some View {
ZStack {
LazyZStack(allowSimultaneous: false, selection: selection, items: transitions) { data in
if data == transitions.first {
content
content(router)
.transition(
.asymmetric(
insertion: currentTransition.insertion,
Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftfulRouting/Core/AnyRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ public struct AnyRouter: Router {
object.showSafari(url)
}

public func transitionModule<T>(id: String, _ option: TransitionOption, destination: @escaping (AnyRouter) -> T) where T : View {
object.transitionModule(id: id, option, destination: destination)
}

public func dismissModule() {
object.dismissModule()
}

public func dismissAllModules() {
object.dismissAllModules()
}
}

struct MockRouter: Router {
Expand Down Expand Up @@ -233,5 +244,16 @@ struct MockRouter: Router {
printError()
}

func transitionModule<T>(id: String, _ option: TransitionOption, destination: @escaping (AnyRouter) -> T) where T : View {
printError()
}

func dismissModule() {
printError()
}

func dismissAllModules() {
printError()
}

}
9 changes: 7 additions & 2 deletions Sources/SwiftfulRouting/Core/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SwiftUI
import Combine

public protocol Router {
public protocol Router: ModuleDelegate {
func enterScreenFlow(_ routes: [AnyRoute])
func showNextScreen() throws
func dismissScreen()
Expand All @@ -34,7 +34,12 @@ public protocol Router {
func transitionScreen<T>(_ option: TransitionOption, @ViewBuilder destination: @escaping (AnyRouter) -> T) where T : View
func dismissTransition()
func dismissAllTransitions()


func showSafari(_ url: @escaping () -> URL)
}

public protocol ModuleDelegate {
func transitionModule<T>(id: String, _ option: TransitionOption, destination: @escaping (AnyRouter) -> T) where T : View
func dismissModule()
func dismissAllModules()
}
147 changes: 114 additions & 33 deletions Sources/SwiftfulRouting/Core/RouterView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,105 @@

import SwiftUI

extension UserDefaults {

static var lastModuleId: String {
get {
standard.string(forKey: "last_module_id") ?? AnyTransitionWithDestination.root.id
}
set {
standard.set(newValue, forKey: "last_module_id")
}
}
}

/// RouterView adds modifiers for segues, alerts, and modals. If you are already within a Navigation heirarchy, set addNavigationView = false.
public struct RouterView<T:View>: View {
public struct RouterView<Content:View>: View, ModuleDelegate {

let addNavigationView: Bool
let screens: Binding<[AnyDestination]>?
let content: (AnyRouter) -> T
let content: (_ router: AnyRouter, _ lastModuleId: String) -> Content

// Modules
@State private var moduleTransition: TransitionOption = .trailing
@State private var selectedModule: AnyTransitionWithDestination = .root
@State private var allModules: [AnyTransitionWithDestination] = [.root]

public init(addNavigationView: Bool = true, screens: (Binding<[AnyDestination]>)? = nil, @ViewBuilder content: @escaping (AnyRouter) -> T) {
@State private var lastModuleId: String

public init(addNavigationView: Bool = true, screens: (Binding<[AnyDestination]>)? = nil, @ViewBuilder content: @escaping (AnyRouter, String) -> Content) {
self.addNavigationView = addNavigationView
self.screens = screens
self.content = content
self._lastModuleId = State(wrappedValue: UserDefaults.lastModuleId)
}

public var body: some View {
RouterViewInternal(
addNavigationView: addNavigationView,
ModuleSupportView(
addNavigationView: addNavigationView,
moduleDelegate: self,
screens: screens,
route: nil,
routes: nil,
environmentRouter: nil,
content: content
selection: $selectedModule,
modules: allModules,
content: { router in
content(router, lastModuleId)
},
currentTransition: moduleTransition
)
}


public func transitionModule<T>(id: String, _ option: TransitionOption, destination: @escaping (AnyRouter) -> T) where T : View {
// Note: lastModuleId is not the AnyTransitionWithDestination's id
UserDefaults.lastModuleId = id

let new = AnyTransitionWithDestination(
id: UUID().uuidString,
transition: option,
destination: { router in
AnyDestination(destination(router))
}
)

self.moduleTransition = option

Task { @MainActor in
try? await Task.sleep(nanoseconds: 1_000_000)

self.allModules.append(new)
self.selectedModule = new
}
}

public func dismissModule() {
if let index = allModules.firstIndex(where: { $0.id == selectedModule.id }), allModules.indices.contains(index - 1) {
self.moduleTransition = allModules[index].transition.reversed

Task { @MainActor in
try? await Task.sleep(nanoseconds: 1_000_000)

self.selectedModule = allModules[index - 1]

try? await Task.sleep(nanoseconds: 25_000)
allModules.remove(at: index)
}

}
}

public func dismissAllModules() {
self.moduleTransition = .trailing.reversed

Task { @MainActor in
try? await Task.sleep(nanoseconds: 1_000_000)

self.selectedModule = allModules.first ?? .root

try? await Task.sleep(nanoseconds: 25_000)
self.allModules = [allModules.first ?? .root]
}
}

}

struct RouterViewInternal<Content:View>: View, Router {
Expand All @@ -38,6 +114,7 @@ struct RouterViewInternal<Content:View>: View, Router {
@Environment(\.openURL) var openURL

let addNavigationView: Bool
let moduleDelegate: ModuleDelegate
let content: (AnyRouter) -> Content

// Routable methods
Expand Down Expand Up @@ -78,7 +155,7 @@ struct RouterViewInternal<Content:View>: View, Router {
@State private var selectedTransition: AnyTransitionWithDestination = .root
@State private var allTransitions: [AnyTransitionWithDestination] = [.root]

public init(addNavigationView: Bool = true, screens: (Binding<[AnyDestination]>)? = nil, route: AnyRoute? = nil, routes: Binding<[[AnyRoute]]>? = nil, environmentRouter: Router? = nil, @ViewBuilder content: @escaping (AnyRouter) -> Content) {
public init(addNavigationView: Bool = true, moduleDelegate: ModuleDelegate, screens: (Binding<[AnyDestination]>)? = nil, route: AnyRoute? = nil, routes: Binding<[[AnyRoute]]>? = nil, environmentRouter: Router? = nil, @ViewBuilder content: @escaping (AnyRouter) -> Content) {
self.addNavigationView = addNavigationView
self._screenStack = screens ?? .constant([])

Expand All @@ -97,7 +174,7 @@ struct RouterViewInternal<Content:View>: View, Router {

self._environmentRouter = State(wrappedValue: environmentRouter)
self.content = content

self.moduleDelegate = moduleDelegate
}

public var body: some View {
Expand All @@ -107,17 +184,9 @@ struct RouterViewInternal<Content:View>: View, Router {
router: router,
selection: $selectedTransition,
transitions: allTransitions,
content: {
content(router)
.onAppear {
print("B")
}
},
content: content,
currentTransition: transition
)
.onAppear {
print("C")
}
.showingScreen(
option: segueOption,
screens: $screens,
Expand All @@ -134,23 +203,17 @@ struct RouterViewInternal<Content:View>: View, Router {
})
.showingAlert(option: alertOption, item: $alert)
.environment(\.router, router)
.onAppear {
print("D")
}
}
.showingModal(items: modals, onDismissModal: { info in
dismissModal(id: info.id)
})
.onAppear {
print("D")
}
}

}

struct RouterView_Previews: PreviewProvider {
static var previews: some View {
RouterView { router in
RouterView { (router, lastModuleId) in
Text("Hi")
.onTapGesture {
router.showScreen(.push) { router in
Expand Down Expand Up @@ -274,7 +337,7 @@ extension RouterViewInternal {
// Sheet and FullScreenCover enter new Environments and require a new Navigation to be added, and don't need an environmentRouter because they will host the environment.
self.sheetDetents = [.large]
self.sheetSelectionEnabled = false
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: true, screens: nil, route: route, routes: routeBinding, environmentRouter: nil, content: destination), onDismiss: nil))
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: true, moduleDelegate: self, screens: nil, route: route, routes: routeBinding, environmentRouter: nil, content: destination), onDismiss: nil))
} else {
// Using existing Navigation
// Push continues in the existing Environment and uses the existing Navigation
Expand All @@ -283,16 +346,16 @@ extension RouterViewInternal {
if #available(iOS 16, *) {
if screenStack.isEmpty {
// We are in the root Router and should start building on $screens
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, screens: $screens, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, moduleDelegate: self, screens: $screens, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
} else {
// We are not in the root Router and should continue off of $screenStack
self.screenStack.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, screens: $screenStack, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
self.screenStack.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, moduleDelegate: self, screens: $screenStack, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
}

// iOS 14/15 uses NavigationView and can only push 1 view at a time
} else {
// Push a new screen and don't pass view stack to child view (screens == nil)
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, screens: nil, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: false, moduleDelegate: self, screens: nil, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: destination), onDismiss: route.onDismiss))
}
}
}
Expand All @@ -314,7 +377,7 @@ extension RouterViewInternal {
destinations.forEach { route in
localRoutes.append(route)

let view = AnyDestination(RouterViewInternal<AnyView>(addNavigationView: false, screens: bindingStack, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: { router in
let view = AnyDestination(RouterViewInternal<AnyView>(addNavigationView: false, moduleDelegate: self, screens: bindingStack, route: route, routes: routeBinding, environmentRouter: environmentRouter, content: { router in
AnyView(route.destination(router))
}), onDismiss: route.onDismiss)
localStack.append(view)
Expand Down Expand Up @@ -347,7 +410,7 @@ extension RouterViewInternal {
self.sheetSelectionEnabled = false
}

self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: true, screens: nil, route: route, routes: routeBinding, environmentRouter: nil, content: destination), onDismiss: nil))
self.screens.append(AnyDestination(RouterViewInternal<V>(addNavigationView: true, moduleDelegate: self, screens: nil, route: route, routes: routeBinding, environmentRouter: nil, content: destination), onDismiss: nil))

// Resizable binding is within current Router, so onFirstAppear of newRoute will never execute
// Manually mark as isPresented
Expand Down Expand Up @@ -721,3 +784,21 @@ extension RouterViewInternal {
}
}
}


// Modules

extension RouterViewInternal {

public func transitionModule<T>(id: String, _ option: TransitionOption, destination: @escaping (AnyRouter) -> T) where T : View {
moduleDelegate.transitionModule(id: id, option, destination: destination)
}

public func dismissModule() {
moduleDelegate.dismissModule()
}

public func dismissAllModules() {
moduleDelegate.dismissAllModules()
}
}

0 comments on commit f95e323

Please sign in to comment.