Skip to content

Commit 232ae86

Browse files
Merge pull request #15 from RobertDresler/1.4
Release 1.4
2 parents 27196e8 + a8cef08 commit 232ae86

16 files changed

+150
-139
lines changed

Examples/Modules/App/Sources/App/AppNavigationNode.swift

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public final class AppNavigationNode {
1515
private let flagsRepository: FlagsRepository
1616
private let deepLinkForwarderService: DeepLinkForwarderService
1717
private let onboardingService: OnboardingService
18-
private var cancellables = Set<AnyCancellable>()
1918

2019
public init(
2120
flagsRepository: FlagsRepository,
@@ -25,46 +24,30 @@ public final class AppNavigationNode {
2524
self.flagsRepository = flagsRepository
2625
self.deepLinkForwarderService = deepLinkForwarderService
2726
self.onboardingService = onboardingService
28-
bind()
2927
}
3028

31-
private var notLoggedNode: any NavigationNode {
32-
.stacked(StartNavigationNode(inputData: StartInputData(), onboardingService: onboardingService))
29+
public func body(for content: SwitchedNavigationNodeView) -> some View {
30+
content
31+
.onReceive(flagsRepository.$isUserLogged) { [weak self] in self?.switchNode(isUserLogged: $0) }
32+
.onReceive(flagsRepository.$isAppLocked) { [weak self] in self?.setLockedAppWindow(isAppLocked: $0) }
33+
.onReceive(flagsRepository.$isWaitingWindowOpen) { [weak self] in self?.handleIsWaitingWindowOpen($0) }
34+
.onReceive(deepLinkForwarderService.deepLinkPublisher) { [weak self] in self?.handleDeepLink($0) }
3335
}
34-
35-
private var loggedNode: any NavigationNode {
36-
MainTabsNavigationNode(
37-
inputData: MainTabsInputData(
38-
initialTab: .commands
36+
37+
private func switchNode(isUserLogged: Bool) {
38+
execute(
39+
.switchNode(
40+
isUserLogged
41+
? MainTabsNavigationNode(
42+
inputData: MainTabsInputData(
43+
initialTab: .commands
44+
)
45+
)
46+
: .stacked(StartNavigationNode(inputData: StartInputData(), onboardingService: onboardingService))
3947
)
4048
)
4149
}
4250

43-
// MARK: Actions
44-
45-
private func bind() {
46-
flagsRepository.$isUserLogged
47-
.sink { [weak self] in self?.setNode(isUserLogged: $0) }
48-
.store(in: &cancellables)
49-
50-
flagsRepository.$isAppLocked
51-
.sink { [weak self] in self?.setLockedAppWindow(isAppLocked: $0) }
52-
.store(in: &cancellables)
53-
54-
flagsRepository.$isWaitingWindowOpen
55-
.sink { [weak self] in self?.handleIsWaitingWindowOpen($0) }
56-
.store(in: &cancellables)
57-
58-
deepLinkForwarderService.deepLinkPublisher
59-
.sink { [weak self] in self?.handleDeepLink($0) }
60-
.store(in: &cancellables)
61-
}
62-
63-
private func setNode(isUserLogged: Bool) {
64-
let switchedNode = isUserLogged ? loggedNode : notLoggedNode
65-
execute(.switchNode(switchedNode))
66-
}
67-
6851
private func handleDeepLink(_ deepLink: NavigationDeepLink) {
6952
execute(.handleDeepLink(deepLink))
7053
}

Examples/Modules/MainTabs/Sources/MainTabs/MainTabsNavigationNode.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ public final class MainTabsNavigationNode {
3434
)
3535
}
3636

37+
public func body(for content: TabsRootNavigationNodeView) -> some View {
38+
content
39+
}
40+
3741
}

Examples/Modules/Subscription/Sources/Subscription/SubscriptionNavigationNode.swift

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,25 @@ public final class SubscriptionNavigationNode {
1111

1212
private let inputData: SubscriptionInputData
1313
private let flagsRepository: FlagsRepository
14-
private var cancellables = Set<AnyCancellable>()
1514

1615
public init(inputData: SubscriptionInputData, flagsRepository: FlagsRepository) {
1716
self.inputData = inputData
1817
self.flagsRepository = flagsRepository
19-
bind()
2018
}
2119

22-
private func bind() {
23-
flagsRepository.$isUserPremium
24-
.sink { [weak self] in self?.setNode(isUserPremium: $0) }
25-
.store(in: &cancellables)
20+
public func body(for content: SwitchedNavigationNodeView) -> some View {
21+
content
22+
.onReceive(flagsRepository.$isUserPremium) { [weak self] in self?.switchNode(isUserPremium: $0) }
2623
}
2724

28-
private func setNode(isUserPremium: Bool) {
29-
let switchedNode = isUserPremium ? premiumNode : freemiumNode
30-
execute(.switchNode(switchedNode))
31-
}
32-
33-
private var premiumNode: any NavigationNode {
34-
SubscriptionPremiumNavigationNode(inputData: SubscriptionPremiumInputData())
35-
}
36-
37-
private var freemiumNode: any NavigationNode {
38-
SubscriptionFreemiumNavigationNode(inputData: SubscriptionFreemiumInputData())
25+
private func switchNode(isUserPremium: Bool) {
26+
execute(
27+
.switchNode(
28+
isUserPremium
29+
? SubscriptionPremiumNavigationNode(inputData: SubscriptionPremiumInputData())
30+
: SubscriptionFreemiumNavigationNode(inputData: SubscriptionFreemiumInputData())
31+
)
32+
)
3933
}
4034

4135
}

README.md

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ I highly recommend starting by exploring the Examples app. The app features many
6565

6666
To get started, first add the package to your project:
6767

68-
- In Xcode, add the package by using this URL: `https://github.com/RobertDresler/SwiftUINavigation` and choose the dependency rule **up to next major version** from `1.3.0`
69-
- Alternatively, add it to your `Package.swift` file: `.package(url: "https://github.com/RobertDresler/SwiftUINavigation", from: "1.3.0")`
68+
- In Xcode, add the package by using this URL: `https://github.com/RobertDresler/SwiftUINavigation` and choose the dependency rule **up to next major version** from `1.4.0`
69+
- Alternatively, add it to your `Package.swift` file: `.package(url: "https://github.com/RobertDresler/SwiftUINavigation", from: "1.4.0")`
7070

7171
Once the package is added, you can copy this code and begin exploring the framework by yourself:
7272

@@ -84,7 +84,7 @@ struct YourApp: App {
8484
}
8585

8686
@NavigationNode
87-
final class HomeNavigationNode {
87+
class HomeNavigationNode {
8888

8989
var body: some View {
9090
HomeView()
@@ -112,7 +112,7 @@ struct HomeView: View {
112112
}
113113

114114
@NavigationNode
115-
final class DetailNavigationNode {
115+
class DetailNavigationNode {
116116

117117
var body: some View {
118118
DetailView()
@@ -146,7 +146,7 @@ A `NavigationNode` represents a single node in the navigation graph, similar to
146146

147147
```swift
148148
@NavigationNode
149-
final class HomeNavigationNode {
149+
class HomeNavigationNode {
150150

151151
var body: some View {
152152
HomeView()
@@ -173,60 +173,91 @@ You can access the `NavigationNode` from your `View` using the following:
173173

174174
```swift
175175
.stacked(HomeNavigationNode())
176-
```
176+
```
177+
178+
If you want to create your own implementation, you can update the node's body using `body(for:)`.
177179

178180
- **`@TabsRootNavigationNode`**
179181
Represents what you would typically associate with a `TabView` or `UITabBarController`. You can create your own implementation of `TabsRootNavigationNode` like this (for more, see [Examples App](#Explore-Examples-App)):
180182

181183
```swift
182-
struct MainTabsInputData {
184+
@TabsRootNavigationNode
185+
class MainTabsNavigationNode {
183186

184187
enum Tab {
185-
case commands
186-
case flows
188+
case home
189+
case settings
187190
}
188191

189-
var initialTab: AnyHashable
192+
init(initialTab: Tab) {
193+
state = TabsRootNavigationNodeState(
194+
selectedTabNodeID: initialTab,
195+
tabsNodes: [
196+
DefaultTabNode(
197+
id: Tab.home,
198+
image: Image(systemName: "house"),
199+
title: "Home",
200+
navigationNode: .stacked(HomeNavigationNode())
201+
),
202+
DefaultTabNode(
203+
id: Tab.settings,
204+
image: Image(systemName: "gear"),
205+
title: "Settings",
206+
navigationNode: .stacked(SettingsNavigationNode())
207+
)
208+
]
209+
)
210+
}
211+
212+
func body(for content: TabsRootNavigationNodeView) -> some View {
213+
content // Modify default content if needed
214+
}
190215

191216
}
217+
```
218+
219+
- **`@SwitchedNavigationNode`**
220+
Use this macro to create a node that can dynamically switch between different child nodes.
192221

193-
@TabsRootNavigationNode
194-
final class MainTabsNavigationNode {
222+
This node is useful for scenarios like:
223+
- A root node that displays either the tabs root node or the login screen based on whether the user is logged in.
224+
- A subscription screen that shows different content depending on whether the user is subscribed.
225+
- And more...
195226

196-
init(inputData: MainTabsInputData) {
197-
let commandsTab = DefaultTabNode(
198-
id: MainTabsInputData.Tab.commands,
199-
image: Image(systemName: "square.grid.2x2"),
200-
title: "Commands",
201-
navigationNode: .stacked(
202-
ActionableListNavigationNode(inputData: .default),
203-
tabBarToolbarBehavior: .hiddenWhenNotRoot(animated: true)
204-
)
205-
)
206-
let flowsTab = DefaultTabNode(
207-
id: MainTabsInputData.Tab.flows,
208-
image: Image(systemName: "point.topright.filled.arrow.triangle.backward.to.point.bottomleft.scurvepath"),
209-
title: "Flows",
210-
navigationNode: .stacked(
211-
ActionableListNavigationNode(inputData: ActionableListInputData(id: .flows)),
212-
tabBarToolbarBehavior: .hiddenWhenNotRoot(animated: true)
227+
See the example below, or for a practical implementation, check out the [Examples App](#Explore-Examples-App).
228+
229+
```swift
230+
class UserService {
231+
@Published var isUserLogged = false
232+
}
233+
234+
@SwitchedNavigationNode
235+
class AppNode {
236+
237+
var userService: UserService
238+
239+
init(userService: UserService) {
240+
self.userService = userService
241+
}
242+
243+
func body(for content: SwitchedNavigationNodeView) -> some View {
244+
content
245+
.onReceive(userService.$isUserLogged) { [weak self] in self?.switchNode(isUserLogged: $0) }
246+
}
247+
248+
private func switchNode(isUserLogged: Bool) {
249+
execute(
250+
.switchNode(
251+
isUserLogged
252+
? MainTabsNavigationNode(initialTab: .home)
253+
: LoginNavigationNode()
213254
)
214255
)
215-
state = TabsRootNavigationNodeState(
216-
selectedTabNodeID: inputData.initialTab,
217-
tabsNodes: [
218-
commandsTab,
219-
flowsTab
220-
]
221-
)
222256
}
223257

224258
}
225259
```
226260

227-
- **`@SwitchedNavigationNode`**
228-
A node that can dynamically switch between different child nodes. For example, it can display a tab bar node when the user is logged in or a welcome screen node when the user is not logged in.
229-
230261
#### Predefined Nodes:
231262

232263
- **`.stacked`/`StackRootNavigationNode`**
@@ -262,7 +293,7 @@ A command is executed on a `NavigationNode` using the `execute(_:)` method:
262293

263294
```swift
264295
@NavigationNode
265-
final class HomeNavigationNode {
296+
class HomeNavigationNode {
266297

267298
...
268299

@@ -323,7 +354,7 @@ Since presenting views using native mechanisms requires separate view modifiers,
323354

324355
```swift
325356
@NavigationNode
326-
final class HomeNavigationNode {
357+
class HomeNavigationNode {
327358

328359
...
329360

Sources/Macros/StackRootNavigationNode.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct StackRootNavigationNode: ExtensionMacro, MemberMacro, MemberAttrib
2626
DeclSyntax(
2727
"""
2828
@MainActor \(accessModifier) var body: some View {
29-
StackRootNavigationNodeView()
29+
body(for: StackRootNavigationNodeView())
3030
}
3131
3232
\(accessModifier) let state: StackRootNavigationNodeState
@@ -42,11 +42,9 @@ public struct StackRootNavigationNode: ExtensionMacro, MemberMacro, MemberAttrib
4242
conformingTo protocols: [TypeSyntax],
4343
in context: some MacroExpansionContext
4444
) throws -> [ExtensionDeclSyntax] {
45-
let accessModifier = declaration.modifiers.first {
46-
[.keyword(.open), .keyword(.public), .keyword(.package)].contains($0.name.tokenKind)
47-
}
48-
return [
49-
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}")
45+
[
46+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}"),
47+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): ModifiableStackRootNavigationNode {}")
5048
]
5149
}
5250
}

Sources/Macros/SwitchedNavigationNode.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct SwitchedNavigationNode: ExtensionMacro, MemberMacro, MemberAttribu
2626
DeclSyntax(
2727
"""
2828
@MainActor \(accessModifier)var body: some View {
29-
SwitchedNavigationNodeView()
29+
body(for: SwitchedNavigationNodeView())
3030
}
3131
\(accessModifier) let state = SwitchedNavigationNodeState()
3232
\(accessModifier) var isWrapperNode: Bool { false }
@@ -42,11 +42,9 @@ public struct SwitchedNavigationNode: ExtensionMacro, MemberMacro, MemberAttribu
4242
conformingTo protocols: [TypeSyntax],
4343
in context: some MacroExpansionContext
4444
) throws -> [ExtensionDeclSyntax] {
45-
let accessModifier = declaration.modifiers.first {
46-
[.keyword(.open), .keyword(.public), .keyword(.package)].contains($0.name.tokenKind)
47-
}
48-
return [
49-
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}")
45+
[
46+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}"),
47+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): ModifiableSwitchedNavigationNode {}")
5048
]
5149
}
5250
}

Sources/Macros/TabsRootNavigationNode.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public struct TabsRootNavigationNode: ExtensionMacro, MemberMacro, MemberAttribu
3636
\(accessModifier) let state: TabsRootNavigationNodeState
3737
3838
@MainActor \(accessModifier) var body: some View {
39-
TabsRootNavigationNodeView()
39+
body(for: TabsRootNavigationNodeView())
4040
}
4141
"""
4242
)
@@ -50,11 +50,9 @@ public struct TabsRootNavigationNode: ExtensionMacro, MemberMacro, MemberAttribu
5050
conformingTo protocols: [TypeSyntax],
5151
in context: some MacroExpansionContext
5252
) throws -> [ExtensionDeclSyntax] {
53-
let accessModifier = declaration.modifiers.first {
54-
[.keyword(.open), .keyword(.public), .keyword(.package)].contains($0.name.tokenKind)
55-
}
56-
return [
57-
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}")
53+
[
54+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): NavigationNode {}"),
55+
try ExtensionDeclSyntax("@MainActor extension \(type.trimmed): ModifiableTabsRootNavigationNode {}")
5856
]
5957
}
6058
}

Sources/SwiftUINavigation/Macros/Macros.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
@attached(memberAttribute)
44
public macro NavigationNode() = #externalMacro(module: "Macros", type: "NavigationNode")
55

6-
@attached(extension, conformances: NavigationNode, names: arbitrary)
6+
@attached(extension, conformances: NavigationNode, ModifiableTabsRootNavigationNode, names: arbitrary)
77
@attached(member, names: arbitrary)
88
@attached(memberAttribute)
99
public macro TabsRootNavigationNode() = #externalMacro(module: "Macros", type: "TabsRootNavigationNode")
1010

11-
@attached(extension, conformances: NavigationNode, names: arbitrary)
11+
@attached(extension, conformances: NavigationNode, ModifiableSwitchedNavigationNode, names: arbitrary)
1212
@attached(member, names: arbitrary)
1313
@attached(memberAttribute)
1414
public macro SwitchedNavigationNode() = #externalMacro(module: "Macros", type: "SwitchedNavigationNode")
1515

16-
@attached(extension, conformances: NavigationNode, names: arbitrary)
16+
@attached(extension, conformances: NavigationNode, ModifiableStackRootNavigationNode, names: arbitrary)
1717
@attached(member, names: arbitrary)
1818
@attached(memberAttribute)
1919
public macro StackRootNavigationNode() = #externalMacro(module: "Macros", type: "StackRootNavigationNode")

0 commit comments

Comments
 (0)