Skip to content

Commit f26687d

Browse files
committed
Merge branch 'main' into experiment/data-cache-as-macro
2 parents 9fe80dd + 6105f1e commit f26687d

19 files changed

+128
-57
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ This target contains types and protocols supporting the architecture. See
1414

1515
The repository uses [DocC](https://www.swift.org/documentation/docc/) for developer-friendly access to documentation.
1616

17+
More high-level documentation can be found in the Futured Engineering Handbook [here](https://futuredapp.github.io/Engineering-Handbook/teams/ios/ios_architecture/).
18+
1719
### FuturedHelpers
1820

1921
This target contains non-mandatory extension to the Architecture and additional types and Views which

Sources/FuturedArchitecture/Architecture/Alert/AlertModel.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SwiftUI
44
///
55
/// ## Overview
66
///
7-
/// It wrappes the native `alert(_:isPresented:presenting:actions:message:)`, but you show an alert in different way by using the `defaultAlert(model:)` view modifier,
7+
/// It wraps the native `alert(_:isPresented:presenting:actions:message:)`, but you show an alert in different way by using the `defaultAlert(model:)` view modifier,
88
/// which then appears whenever the bound `model` value is not `nil` value.
99
/// Alert model contains two actions: `primaryAction` and `secondaryAction`, which are then represented as SwiftUI Button
1010
/// If both values are nil, system presents alert with standard "OK" button and given `title` and `message`
@@ -62,12 +62,23 @@ public struct AlertModel: Identifiable {
6262
}
6363
}
6464

65+
public struct TextField {
66+
let title: String
67+
let text: Binding<String>
68+
69+
public init(title: String, text: Binding<String>) {
70+
self.title = title
71+
self.text = text
72+
}
73+
}
74+
6575
public var id: String? {
6676
title + (message ?? "")
6777
}
6878

6979
let title: String
7080
let message: String?
81+
let textField: TextField?
7182
let primaryAction: ButtonAction?
7283
let secondaryAction: ButtonAction?
7384

@@ -78,9 +89,16 @@ public struct AlertModel: Identifiable {
7889
/// - primaryAction: The specification of the alert primary action.
7990
/// - secondaryAction: The specification of the alert secondary action.
8091

81-
public init(title: String, message: String?, primaryAction: ButtonAction? = nil, secondaryAction: ButtonAction? = nil) {
92+
public init(
93+
title: String,
94+
message: String?,
95+
textField: TextField? = nil,
96+
primaryAction: ButtonAction? = nil,
97+
secondaryAction: ButtonAction? = nil
98+
) {
8299
self.title = title
83100
self.message = message
101+
self.textField = textField
84102
self.primaryAction = primaryAction
85103
self.secondaryAction = secondaryAction
86104
}

Sources/FuturedArchitecture/Architecture/Alert/AlertModifier.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,22 @@ private struct AlertModifier: ViewModifier {
2020
),
2121
presenting: model,
2222
actions: { model in
23+
if let textField = model.textField {
24+
TextField(textField.title, text: textField.text)
25+
}
2326
if let primaryAction = model.primaryAction {
2427
Button(
2528
primaryAction.title,
2629
role: primaryAction.buttonRole,
2730
action: primaryAction.action
2831
)
29-
30-
if let secondaryAction = model.secondaryAction {
31-
Button(
32-
secondaryAction.title,
33-
role: secondaryAction.buttonRole,
34-
action: secondaryAction.action
35-
)
36-
}
32+
}
33+
if let secondaryAction = model.secondaryAction {
34+
Button(
35+
secondaryAction.title,
36+
role: secondaryAction.buttonRole,
37+
action: secondaryAction.action
38+
)
3739
}
3840
},
3941
message: { model in

Sources/FuturedArchitecture/Architecture/Coordinator.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import SwiftUI
22

3-
/// This architecture is modelled around the concept of *Flow Coordinators*. You might think about
3+
/// This architecture is modeled around the concept of *Flow Coordinators*. You might think about
44
/// *flow coordinator* as a "view model for container view." For example, whereas data model of
55
/// a Table View is stored in a *component model*, data model of ``SwiftUI.TabView`` and ``SwiftUI.NavigationStack``
66
/// is stored in an instance conforming to `Coordinator`.
77
///
88
/// This base `protocol` contains set of common requirements for all coordinators. Other protocols
9-
/// tailored to specific Containers are provided aswell.
9+
/// tailored to specific Containers are provided as well.
1010
public protocol Coordinator: ObservableObject {
1111
/// Type used to represent the state of the container, i.e. which child-components should be presented.
1212
associatedtype Destination: Hashable & Identifiable
@@ -17,7 +17,7 @@ public protocol Coordinator: ObservableObject {
1717

1818
/// `rootView` returns the coordinator's main view.
1919
/// - Note: It is common pattern to provide a default "destination" view as the body of the *container* instead of
20-
/// ``SwiftUI.EmptyView``. If you do so, remeber to always capture the `instance` of the *coordinator* weakly!
20+
/// ``SwiftUI.EmptyView``. If you do so, remember to always capture the `instance` of the *coordinator* weakly!
2121
/// - Warning: Maintain its purity by defining only the view, without added logic or modifiers.
2222
/// If logic or modifiers are needed, encapsulate them in a separate view that can accommodate necessary dependencies.
2323
/// Skipping this recommendation may prevent UI updates when changing `@Published` properties, as `rootView` is static.
@@ -72,8 +72,8 @@ public extension Coordinator {
7272

7373
/// `TabCoordinator` provides additional requirements for the use with ``SwiftUI.TabView``.
7474
/// This *coordinator* is ment to have ``TabViewFlow`` as the Root view.
75-
/// - Experiment: This API is in preview and subjet to change.
76-
/// - Todo: ``SwiftUI.TabView`` requires internal state, which is forbiddden as per
75+
/// - Experiment: This API is in preview and subject to change.
76+
/// - Todo: ``SwiftUI.TabView`` requires internal state, which is forbidden as per
7777
/// documentation of ``Coordinator.rootView(with:)``. Also, the API introduces `Tab` type
7878
/// which is essentially duplication of `Destination`. Consider, how the API limits the use of tabs.
7979
public protocol TabCoordinator: Coordinator {
@@ -95,7 +95,7 @@ public protocol VariantCoordinator: TabCoordinator {}
9595
///
9696
/// - ToDo: Create a template for this coordinator.
9797
public protocol NavigationStackCoordinator: Coordinator {
98-
/// Property modelling the Views currently placed on stack.
98+
/// Property modeling the Views currently placed on stack.
9999
@MainActor
100100
var path: [Destination] { get set }
101101
}
@@ -114,7 +114,7 @@ public extension NavigationStackCoordinator {
114114
}
115115

116116
/// Convenience function used to remove all views from the stack, until the provided destination.
117-
/// - Parameter destination: Destination to be reached. If nil is passed, or such destionation
117+
/// - Parameter destination: Destination to be reached. If nil is passed, or such destination
118118
/// is not currently on the stack, all views are removed.
119119
/// - Experiment: This API is in preview and subject to change.
120120
@MainActor

Sources/FuturedArchitecture/Architecture/DataCache.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import Foundation
1010
/// only via provided `update` methods. As a general rule, value types should
1111
/// be used as a `Model`.
1212
///
13-
/// - Experiment: This API is in preview and subjet to change.
14-
/// - ToDo: How the `DataCache` may interact with persistance such as
13+
/// - Experiment: This API is in preview and subject to change.
14+
/// - ToDo: How the `DataCache` may interact with persistence such as
1515
/// `CoreData` or `SwiftData` is an open question and subject of further
1616
/// research.
1717
public actor ActorDataCache<Model: Equatable> {
@@ -34,10 +34,38 @@ public actor ActorDataCache<Model: Equatable> {
3434
/// Atomically update one variable.
3535
///
3636
/// - ToDo: Investigate whether we can use variadic generics to improve the API.
37-
/// No change is emmited when the value is the same.
37+
/// No change is emitted when the value is the same.
3838
@inlinable
3939
public func update<T: Equatable>(_ keyPath: WritableKeyPath<Model, T>, with value: T) {
4040
guard value != self.value[keyPath: keyPath] else { return }
4141
self.value[keyPath: keyPath] = value
4242
}
43+
44+
/// Populate one variable of Collection type.
45+
/// - Description: The method will append new elements to the existing collection. The elements which are already in the collection as well as in the new collection will be updated. No change is emmited when the new collection is empty.
46+
@inlinable
47+
public func populate<T: Collection>(_ keyPath: WritableKeyPath<Model, T>, with collection: T) where T.Element: Equatable {
48+
guard !collection.isEmpty else { return }
49+
var values = self.value[keyPath: keyPath].filter { !collection.contains($0) }
50+
values.append(contentsOf: collection)
51+
guard let values = values as? T else {
52+
assertionFailure("Cannot convert back to generic")
53+
return
54+
}
55+
self.value[keyPath: keyPath] = values
56+
}
57+
58+
/// Populate one optional variable of Collection type.
59+
/// - Description: The method will append new elements to the existing collection. The elements which are already in the collection as well as in the new collection will be updated. No change is emmited when the new collection is empty.
60+
@inlinable
61+
public func populate<T: Collection>(_ keyPath: WritableKeyPath<Model, T?>, with collection: T) where T.Element: Equatable {
62+
guard !collection.isEmpty else { return }
63+
var values = self.value[keyPath: keyPath]?.filter { !collection.contains($0) } ?? []
64+
values.append(contentsOf: collection)
65+
guard let values = values as? T else {
66+
assertionFailure("Cannot convert back to generic")
67+
return
68+
}
69+
self.value[keyPath: keyPath] = values
70+
}
4371
}

Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
//
22
// ModalCoverModel.swift
3-
//
3+
//
44
//
55
// Created by Simon Sestak on 01/08/2024.
66
//
77

8+
import SwiftUI
89
import Foundation
910

10-
/// Style of the modally presented view.
11+
12+
public enum SheetDetent: Hashable {
13+
case medium
14+
case large
15+
case fraction(CGFloat)
16+
17+
func detent() -> PresentationDetent {
18+
switch self {
19+
case .medium:
20+
return .medium
21+
case .large:
22+
return .large
23+
case let .fraction(fraction):
24+
return .fraction(fraction)
25+
}
26+
}
27+
}
28+
29+
/// Style of the modally presented view.
1130
///
1231
/// It is intended to be used with ``ModalCoverModel``. Style has been placed to
1332
/// the global scope, since the Model is generic.

Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,34 @@ public struct NavigationStackFlow<Coordinator: NavigationStackCoordinator, Conte
66
@StateObject private var coordinator: Coordinator
77
@ViewBuilder private let content: () -> Content
88

9+
/// Use in case when whole navigation stack should have detents.
10+
@State private var navigationDetents: Set<PresentationDetent>?
11+
912
/// - Parameters:
13+
/// - detents: The set of detents which should be applied to the whole navigation stack.
1014
/// - coordinator: The instance of the coordinator used as the model and retained as the ``SwiftUI.StateObject``
1115
/// - content: The root view of this navigation stack. The ``navigationDestination(for:destination:)`` modifier
1216
/// is applied to this content.
13-
public init(coordinator: @autoclosure @escaping () -> Coordinator, content: @MainActor @escaping () -> Content) {
17+
public init(
18+
detents: Set<SheetDetent>? = nil,
19+
coordinator: @autoclosure @escaping () -> Coordinator,
20+
content: @MainActor @escaping () -> Content
21+
) {
22+
self.navigationDetents = detents == nil ? nil : Set(detents!.map { $0.detent() })
1423
self._coordinator = StateObject(wrappedValue: coordinator())
1524
self.content = content
1625
}
1726

18-
#if os(macOS)
19-
public var body: some View {
20-
NavigationStack(path: $coordinator.path) {
21-
content().navigationDestination(for: Coordinator.Destination.self, destination: coordinator.scene(for:))
22-
}
23-
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
24-
}
25-
#else
2627
public var body: some View {
2728
NavigationStack(path: $coordinator.path) {
2829
content().navigationDestination(for: Coordinator.Destination.self, destination: coordinator.scene(for:))
2930
}
31+
.presentationDetents(navigationDetents ?? [])
3032
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
33+
#if !os(macOS)
3134
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
35+
#endif
3236
}
33-
#endif
3437

3538
private var sheetBinding: Binding<Coordinator.Destination?> {
3639
.init {

Sources/FuturedArchitecture/Architecture/TabViewFlow.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22

33
/// The `TabViewFlow` encapsulates the ``SwiftUI.TabView`` and binds it to the
44
/// variables and callbacks of the ``TabCoordinator`` which is retains as a ``SwiftUI.StateObject``.
5-
/// - Experiment: This API is in preview and subjet to change.
5+
/// - Experiment: This API is in preview and subject to change.
66
public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
77
@StateObject private var coordinator: Coordinator
88
@ViewBuilder private let content: () -> Content

Sources/FuturedArchitecture/Documentation.docc/Documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The FuturedKit Architecture is a flow-coordinator, component, component model ba
66

77
This architecture uses some concepts which may be familiar, but we decided to modify the usual naming (mainly due to naming conflits with existing APIs).
88

9-
![overview](archoverview)
9+
![overview](Images/archoverview.svg)
1010

1111
### Scene
1212

Sources/FuturedHelpers/Helpers/SceneDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ extension View {
3636
/// - Parameter sceneDelegate: The SceneDelegate to set.
3737
/// - Description:
3838
/// In the main app root view call this modifier and pass the SceneDelegate. You need to specify the AppSceneDelegate which conforms to the UIWindowSceneDelegate.
39-
/// This is necessary because the SceneDelegate is accessible in SwiftUI only via EnviromentObject.
39+
/// This is necessary because the SceneDelegate is accessible in SwiftUI only via EnvironmentObject.
4040
public func set<T: AppSceneDelegate>(appSceneDelegateClass: T.Type, sceneDelegate: SceneDelegate) -> some View {
4141
modifier(SceneDelegateWrapperViewModifier<T>(delegate: sceneDelegate))
4242
}

0 commit comments

Comments
 (0)