Skip to content

Commit

Permalink
+ Implement Navigation in SwiftUI with iOS 17+ and Navigation Stack. (#9
Browse files Browse the repository at this point in the history
)
  • Loading branch information
iletai committed Dec 21, 2023
1 parent 5f1b07c commit 724a66c
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
3E7A81DA2B2B0F4D0003D5A3 /* Utils */ = {
isa = PBXGroup;
children = (
3EEC9A792B301B540082BEE9 /* ViewModifer */,
3E7A81DB2B2B0FA90003D5A3 /* View+Extension.swift */,
);
path = Utils;
Expand All @@ -108,6 +109,13 @@
path = View;
sourceTree = "<group>";
};
3EEC9A792B301B540082BEE9 /* ViewModifer */ = {
isa = PBXGroup;
children = (
);
path = ViewModifer;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,39 @@
//

import Foundation
import BaseNavigationStack
import SwiftUI

public enum ViewNavigationTarget: Identifiable, Hashable {
public enum ViewNavigationTarget: BaseViewProtocol {
public func withNavigationDestination() -> some View {
switch self {
case .policy(_):
PolicyView()
case .list:
ListView()
case .splash:
SplashView()
default:
EmptyView()
}
}

public static func == (lhs: ViewNavigationTarget, rhs: ViewNavigationTarget) -> Bool {
lhs.id == rhs.id
}

public var id: String {
switch self {
case .policy(_):
return "policy"
case .updateProfile(_):
return "updateProfile"
case .splash:
return "splash"
case .main:
return "main"
case .list:
return "list"
case .policy(_):
return "policy"
case .updateProfile(_):
return "updateProfile"
case .splash:
return "splash"
case .main:
return "main"
case .list:
return "list"
}
}

Expand All @@ -35,20 +50,62 @@ public enum ViewNavigationTarget: Identifiable, Hashable {

public func hash(into hasher: inout Hasher) {
switch self {
case .splash:
hasher.combine("splash")
case .main:
hasher.combine("main")
case .list:
hasher.combine("list")
case .policy(let url):
hasher.combine("policy")
hasher.combine(url)
case .updateProfile(let closure):
hasher.combine("updateProfile")
// Note: You may want to provide a more specific hash for closures
// depending on your use case.
hasher.combine(ObjectIdentifier(closure as AnyObject))
case .splash:
hasher.combine("splash")
case .main:
hasher.combine("main")
case .list:
hasher.combine("list")
case .policy(let url):
hasher.combine("policy")
hasher.combine(url)
case .updateProfile(let closure):
hasher.combine("updateProfile")
hasher.combine(ObjectIdentifier(closure as AnyObject))
}
}

public func withSheetDestination() -> some View {
switch self {
case .policy(_):
PolicyView()
.presentationDetents(self.detent)
case .list:
ListView()
.presentationDetents(self.detent)
case .splash:
SplashView()
.presentationDetents(self.detent)
default:
EmptyView()
}
}

public func withFullScreenDestination() -> some View {
switch self {
case .policy(_):
PolicyView()
case .list:
ListView()
case .splash:
SplashView()
default:
EmptyView()
}
}

public var detent: Set<PresentationDetent> {
switch self {
case .splash:
return [.large]
case .main:
return [.large]
case .list:
return [.large]
case .policy(_):
return [.large]
case .updateProfile(_):
return [.large]
}
}
}
12 changes: 2 additions & 10 deletions NavigationStackExample/NavigationStackExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,11 @@ import BaseNavigationStack
import Foundation

typealias BaseNavigation = BaseNavigationStack<ViewNavigationTarget>

struct ContentView: View {
@State
var navigationRouter = BaseNavigation(isPresented: .constant(.splash))

var body: some View {
NavigationStack(path: navigationRouter.navigationPath) {
BaseView()
.withNavigationRouter()
.withSheetRouter(sheetDestination: navigationRouter.presentingSheet)
.withFullScreenRouter(navigationRouter.presentingFullScreen)
BaseNavigationView<SplashView, ViewNavigationTarget> {
SplashView()
}
.environment(navigationRouter)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ extension View {
PolicyView()
case .list:
ListView()
case .splash:
SplashView()
default:
EmptyView()
}
}
}

@MainActor
func withFullScreenRouter(_ presentFullView: Binding<ViewNavigationTarget?>) -> some View {
fullScreenCover(item: presentFullView) { fullView in
switch fullView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct ListView: View {
HStack {
Button {
dimiss()
navigationRouter.dismiss()
} label: {
Text("Dis")
}
Expand Down Expand Up @@ -60,6 +61,7 @@ struct ListView: View {
Spacer()
}
.navigationTitle("ListView")
.navigationBarTitleDisplayMode(.inline)
}
}

Expand All @@ -77,6 +79,7 @@ struct SplashView: View {
HStack {
Button {
dimiss()
navigationRouter.dismiss()
} label: {
Text("Dis")
}
Expand Down Expand Up @@ -105,6 +108,7 @@ struct SplashView: View {
Spacer()
}
.navigationTitle("Splash")
.navigationBarTitleDisplayMode(.inline)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ struct PolicyView: View {
HStack {
Button {
dimiss()
navigationRouter.dismiss()
} label: {
Text("Dis")
}
Button {
navigationRouter.presentSheet(.splash)
} label: {
Text("PresentSplas")
}
}
HStack {
Button {
Expand Down
31 changes: 11 additions & 20 deletions Sources/BaseNavigationStack/BaseNavigationStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import Observation

@Observable
/// Base Navigation Stack With Generic Type.
public class BaseNavigationStack<ScreenView> where ScreenView: Hashable {
public class BaseNavigationStack<ScreenView> where ScreenView: BaseViewProtocol {
private(set) var state: RootingState
public var urlHandler: ((URL) -> OpenURLAction.Result)?

public init(isPresented: Binding<ScreenView?>) {
state = RootingState(isPresented: isPresented)
state = RootingState(
isPresented: isPresented
)
}
}

Expand All @@ -25,7 +28,6 @@ public extension BaseNavigationStack {
/// - Parameter viewSpec: Push View
@MainActor
func pushToView(_ viewSpec: ScreenView) {
// if state.isPresenting { dismiss() }
state.navigationPath.append(viewSpec)
}

Expand All @@ -40,7 +42,6 @@ public extension BaseNavigationStack {
func navigateToRoot() {
if state.isPresenting {
state.presentingFullScreen = nil
state.presentingModal = nil
state.presentingSheet = nil
}
state.navigationPath.removeAllSafe()
Expand All @@ -57,40 +58,34 @@ public extension BaseNavigationStack {
/// - Parameter viewSpec: present Sheet View
@MainActor
func presentSheet(_ viewSpec: ScreenView) {
// if state.isPresenting { dismiss() }
state.presentingSheet = viewSpec
}

/// Full Screen View In Bas Navigation Stack
/// - Parameter viewSpec: PushViewTarget
@MainActor
func presentFullScreen(_ viewSpec: ScreenView) {
// if state.isPresenting { dismiss() }
state.presentingFullScreen = viewSpec
}

/// Present Modal View
/// - Parameter viewSpec: PushViewTarget
@MainActor
func presentModal(_ viewSpec: ScreenView) {
state.presentingModal = viewSpec
}


/// Dismiss If View Is Presenting or Embed In Navigation Stack
@MainActor
func dismiss() {
if state.presentingSheet != nil {
state.presentingSheet = nil
} else if state.presentingFullScreen != nil {
state.presentingFullScreen = nil
} else if state.presentingModal != nil {
state.presentingModal = nil
} else if navigationPath.count > 1 {
state.navigationPath.removeLast()
} else {
state.isPresented.wrappedValue = nil
}
}

public func handleOpenURL(url: URL) -> OpenURLAction.Result {
// TODO: Update URL Handler
return urlHandler?(url) ?? .systemAction
}
}

public extension BaseNavigationStack {
Expand All @@ -106,10 +101,6 @@ public extension BaseNavigationStack {
binding(keyPath: \.presentingFullScreen)
}

var presentingModal: Binding<ScreenView?> {
binding(keyPath: \.presentingModal)
}

var isPresented: Binding<ScreenView?> {
state.isPresented
}
Expand Down
42 changes: 42 additions & 0 deletions Sources/BaseNavigationStack/BaseNavigationView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// BaseNavigationView.swift
//
//
// Created by iletai on 20/12/2023.
//

import Foundation
import SwiftUI
import Combine

public struct BaseNavigationView<RootView: View, ScreenView>: View where ScreenView: BaseViewProtocol {
public typealias BaseNavigation = BaseNavigationStack<ScreenView>
/// Base Navigation Stack Root View
public let rootView: RootView

@State
private var navigationRouter = BaseNavigation(isPresented: .constant(nil))

public init(@ViewBuilder rootView: @escaping () -> RootView) {
self.rootView = rootView()
}

public var body: some View {
NavigationStack(path: navigationRouter.navigationPath) {
rootView
.navigationDestination(for: ScreenView.self) { screen in
screen.withNavigationDestination()
}
.sheet(item: navigationRouter.presentingSheet) { item in
item.withSheetDestination()
}
.fullScreenCover(item: navigationRouter.presentingFullScreen, content: { fullScreen in
fullScreen.withFullScreenDestination()
})
.environment(\.openURL, OpenURLAction {
navigationRouter.handleOpenURL(url: $0)
})
}
.environment(navigationRouter)
}
}
26 changes: 26 additions & 0 deletions Sources/BaseNavigationStack/Interface/BaseViewProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// PresentViewProtocol.swift
//
//
// Created by iletai on 18/12/2023.
//

import Foundation
import SwiftUI

public protocol BaseViewProtocol: Identifiable, Hashable {
associatedtype PresentSheetScreenView: View
associatedtype DestinationView: View
associatedtype PresentFullScreenView: View

@MainActor
@ViewBuilder
func withSheetDestination() -> PresentSheetScreenView

@ViewBuilder
func withFullScreenDestination() -> PresentFullScreenView

@MainActor
@ViewBuilder
func withNavigationDestination() -> DestinationView
}
Loading

0 comments on commit 724a66c

Please sign in to comment.