diff --git a/Documentation Resources/archoverview.drawio b/Documentation Resources/archoverview.drawio new file mode 100644 index 0000000..d85e597 --- /dev/null +++ b/Documentation Resources/archoverview.drawio @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index c2cf6f3..d58a048 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,26 @@ # FuturedKit -SwiftUI state management tools, resources and views used by Futured. - -## Features - -### Architecture - -- ``Coordinator`` - - The Coordinator protocol defines a destination type (instances are hashable and identifiable), root view and destination views that conform to the `View` protocol, and properties for a sheet, fullscreen cover and alert model. Furthermore, it provides methods for presenting/dismissing sheets and fullscreen covers, displaying alerts, handling the dismissal of sheets and fullscreen covers. - - ``TabCoordinator`` - - The `TabCoordinator` protocol extends the `Coordinator` protocol and provides additional functionality for managing tab views in SwiftUI apps. It introduces a `Tab` associated type and a `selectedTab` property which is used for managing the currently selected tab. - - ``NavigationStackCoordinator`` - - The `NavigationStackCoordinator` protocol also extends the `Coordinator` protocol and adds additional functionality for managing navigation stacks in SwiftUI apps. It manages a `path` which represents the array of navigational elements in the navigation stack, and provides methods for moving through this navigation stack. Actions such as navigate (to push a new view onto the stack), pop (to remove the current view from the stack), and pop to a specific destination in the stack are defined in this protocol. -- ``Component`` - - Components are the views that users interact with directly in the application. They handle the user interface and its functionality. From buttons to table views, every element a user sees and interacts with is a separate View in our application. Our aim here is to keep our Components as simple and clean as possible to provide a clutter-free and intuitive user interface. - - Components are designed to hold multiple view elements which can be reusable in other components. When a user interacts with a component action view (button, text field delegates, etc.), they call the ComponentModel functions for the desired behavior. -- ``ComponentModel`` - - ComponentModels act as an intermediary between Components and Models. In our iOS application, they handle the business logic and are in charge of making API calls, parsing data, managing and performing computations. Typically, the ComponentModel will format the data it receives from the Model so that it's ready to be presented by the View. -- ``DataCacheModel`` - - DataCacheModel is a component in our application's architecture whose function is to store and retrieve data in a fast and efficient manner. It saves user information or data from server responses, which can then be readily accessed when needed. The advantage of DataCacheModel is that it reduces the need for repetitive network calls, providing a smoother user experience. - - Use DataCacheModel for every DataModel which needs to be stored for multiple app flows or for app flows which can be reopened. For use cases like creating a new DataModel which is specific for one flow, create a new DataCache wrapper that wraps the DataModel in this specific flow coordinator. - -### Views - -- ``AnyShape`` -- ``WrappedUIImagePicker`` -- ``CameraImagePicker`` -- ``GalleryImagePicker`` +SwiftUI app architecture and views used by Futured. + +## Documentation + +This repository contains two main targets: `FuturedArchitecture` and `FuturedHelpers`. The repository +also contains set of Templates, see `Installation`. + +### FuturedArchitecture + +This target contains types and protocols supporting the architecture. See +[Architecture documentation](Sources/FuturedArchitecture/Documentation.docc/Documentation.md). + +The repository uses [DocC](https://www.swift.org/documentation/docc/) for developer-friendly access to documentation. + +### FuturedHelpers + +This target contains non-mandatory extension to the Architecture and additional types and Views which +are commonly used. + +- ``CameraImagePicker`` (See source documentation) +- ``GalleryImagePicker`` (See source documentation) ## Installation @@ -36,6 +30,13 @@ When using Swift package manager install using or add following line to your dep .package(url: "https://github.com/futuredapp/FuturedKit.git", from: "1.0.0") ``` +The repository provides number of Templates for user convenience. You can install them using make: + +```bash +cd Templates +make +``` + ## Contributions All contributions are welcome. diff --git a/Sources/FuturedArchitecture/Architecture/ComponentModel.swift b/Sources/FuturedArchitecture/Architecture/ComponentModel.swift index 0a4265c..97e0a55 100644 --- a/Sources/FuturedArchitecture/Architecture/ComponentModel.swift +++ b/Sources/FuturedArchitecture/Architecture/ComponentModel.swift @@ -1,7 +1,31 @@ import Foundation +/// `ComponentModel` is an analogy to `ViewModel`. Each *component model* should have +/// it's own protocol which uses the `ComponentModel` protocol as it's requirement. This allows us +/// to create a Mock implementations of component models and allow for simpler and more scalable +/// SwiftUI Previews. +/// +/// As is eluded to by `ComponentModel`, each component model has two main competencies: +/// - Hold and organize data required by the associated view. +/// - Receive events from the associated view and propagate them upwards to a *coordinator.* +/// +/// The associated type `Event` is just an `enum` in most cases, but we do not explicitly discourage +/// you from using *any* type you see fit. However, keep in mind, that we should not burden *coordinators* +/// with too much logic. Keep the API for the *coordinators* simple. +/// +/// - Note: Each *component model* should have have *mock* class and *implementation* class. +/// Each *Component* (i.e. View) should have own *component model*. Each instance of component +/// model has to be referenced by no more than 1 *coordinator.* public protocol ComponentModel: ObservableObject { + + /// Type used to pass events to the *coordinator*. `enum` is used in most cases, but not required. associatedtype Event + /// Closure which should be provided by the *coordinator* and only invoked from within the + /// *component model* instance. + /// + /// The return type if this closure is `Void` intentionally. If bidirectional communication is + /// desired, either pass closure to the *coordinator* using the event, or use other + /// recommended pattern of data flow. var onEvent: (Event) -> Void { get } } diff --git a/Sources/FuturedArchitecture/Architecture/Coordinator.swift b/Sources/FuturedArchitecture/Architecture/Coordinator.swift index e92435b..e0eaa3c 100644 --- a/Sources/FuturedArchitecture/Architecture/Coordinator.swift +++ b/Sources/FuturedArchitecture/Architecture/Coordinator.swift @@ -1,25 +1,52 @@ import SwiftUI +/// This architecture is modelled around the concept of *Flow Coordinators*. You might think about +/// *flow coordinator* as a "view model for container view." For example, whereas data model of +/// a Table View is stored in a *component model*, data model of ``SwiftUI.TabView`` and ``SwiftUI.NavigationStack`` +/// is stored in an instance conforming to `Coordinator`. +/// +/// This base `protocol` contains set of common requirements for all coordinators. Other protocols +/// tailored to specific Containers are provided aswell. public protocol Coordinator: ObservableObject { + /// Type used to represent the state of the container, i.e. which child-components should be presented. associatedtype Destination: Hashable & Identifiable + /// The root view of the coordinator is commonly the container itself. associatedtype RootView: View + /// Views which might be presented by the container based on an instance of `Destination`. associatedtype DestinationViews: View - - /// `rootView` returns the coordinator's main view. Maintain its purity by defining only the view, without added logic or modifiers. + + /// `rootView` returns the coordinator's main view. + /// - Note: It is common pattern to provide a default "destination" view as the body of the *container* instead of + /// ``SwiftUI.EmptyView``. If you do so, remeber to always capture the `instance` of the *coordinator* weakly! + /// - Warning: Maintain its purity by defining only the view, without added logic or modifiers. /// If logic or modifiers are needed, encapsulate them in a separate view that can accommodate necessary dependencies. /// Skipping this recommendation may prevent UI updates when changing `@Published` properties, as `rootView` is static. + /// - Parameter instance: An instance of `Coordinator` which will be retained by the *container*. + /// - Returns: The container view. @ViewBuilder static func rootView(with instance: Self) -> RootView + /// Modal cover is part of the model of the container. It represents the state of the View covering the container. @MainActor var modalCover: ModalCoverModel? { get set } + + /// This function provides an instance of a `View` (commonly a *Component*) for each possible state of the + /// container (the destination). @ViewBuilder func scene(for destination: Destination) -> DestinationViews + + /// This is a delegate function called when a modal view presented by the *container* is dismissed. + /// - Note: Default empty implementation is provided. func onModalDismiss() } public extension Coordinator { + /// Convenience function for presenting a modal over the *container*. + /// - Parameters: + /// - destination: The description of the desired view passed to the ``scene(for:)`` function + /// of the *coordinator*. + /// - type: Kind of modal presentation. func present(modal destination: Destination, type: ModalCoverModelStyle) { switch type { case .sheet: @@ -35,6 +62,7 @@ public extension Coordinator { } } + /// Convenience method for dismissing a modal. func dismissModal() { Task { @MainActor in self.modalCover = nil @@ -44,6 +72,12 @@ public extension Coordinator { func onModalDismiss() {} } +/// `TabCoordinator` provides additional requirements for the use with ``SwiftUI.TabView``. +/// This *coordinator* is ment to have ``TabViewFlow`` as the Root view. +/// - Experiment: This API is in preview and subjet to change. +/// - Todo: ``SwiftUI.TabView`` requires internal state, which is forbiddden as per +/// documentation of ``Coordinator.rootView(with:)``. Also, the API introduces `Tab` type +/// which is essentially duplication of `Destination`. Consider, how the API limits the use of tabs. public protocol TabCoordinator: Coordinator { associatedtype Tab: Hashable @@ -51,24 +85,37 @@ public protocol TabCoordinator: Coordinator { var selectedTab: Tab { get set } } +/// `NavigationStackCoordinator` provides additional requirements for use with ``SwiftUI.NavigationStack``. +/// This *coordinator* is ment have ``NavigationStackFlow`` as the Root view. +/// +/// - ToDo: Create a template for this coordinator. public protocol NavigationStackCoordinator: Coordinator { + /// Property modelling the Views currently placed on stack. @MainActor var path: [Destination] { get set } } public extension NavigationStackCoordinator { + /// Convenience function used to add new view to the navigation stack. func navigate(to destination: Destination) { Task { @MainActor in self.path.append(destination) } } + /// Convenience function used to remove topmost view from the navigation stack. func pop() { Task { @MainActor in self.path.removeLast() } } + /// Convenience function used to remove all views from the stack, until the provided destination. + /// - Parameter destination: Destination to be reached. If nil is passed, or such destionation + /// is not currently on the stack, all views are removed. + /// - Experiment: This API is in preview and subject to change. + /// - Bug: @mikolasstuchlik thinks, that dismissing *all* views when destination is not found is + /// confusing and might be source of bugs. func pop(to destination: Destination?) { Task { @MainActor in let index = destination.flatMap(self.path.lastIndex(of:)) ?? self.path.startIndex diff --git a/Sources/FuturedArchitecture/Architecture/DataCache.swift b/Sources/FuturedArchitecture/Architecture/DataCache.swift index 4758f6f..5a8a080 100644 --- a/Sources/FuturedArchitecture/Architecture/DataCache.swift +++ b/Sources/FuturedArchitecture/Architecture/DataCache.swift @@ -1,6 +1,21 @@ import Foundation +/// `DataCache` is intended to store state which may be used by +/// more than one *component* and/or fetched from remote. +/// +/// An Application should contain one shared application-wide cache, but each +/// coordinator may also create a private data cache. +/// +/// The data from data cache should be taken as a subscription and modified +/// only via provided `update` methods. As a general rule, value types should +/// be used as a `Model`. +/// +/// - Experiment: This API is in preview and subjet to change. +/// - ToDo: How the `DataCache` may interact with persistance such as +/// `CoreData` or `SwiftData` is an open question and subject of further +/// research. public actor DataCache { + /// The data held by this data cache. @inlinable @Published public private(set) var value: Model @@ -8,12 +23,18 @@ public actor DataCache { self._value = Published(initialValue: value) } + /// Atomically update the whole data cache. Use this method if you need + /// to perform number of changes at once. @inlinable public func update(with value: Model) { guard value != self.value else { return } self.value = value } + /// Atomically update one variable. + /// + /// - ToDo: Investigate whether we can use variadic generics to improve the API. + /// No change is emmited when the value is the same. @inlinable public func update(_ keyPath: WritableKeyPath, with value: T) { guard value != self.value[keyPath: keyPath] else { return } diff --git a/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift b/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift index 534791a..31705ab 100644 --- a/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift +++ b/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift @@ -7,6 +7,10 @@ import Foundation +/// Style of the modally presented view. +/// +/// It is intended to be used with ``ModalCoverModel``. Style has been placed to +/// the global scope, since the Model is generic. public enum ModalCoverModelStyle { case sheet #if !os(macOS) @@ -14,6 +18,7 @@ public enum ModalCoverModelStyle { #endif } +/// This struct is a model associating presentation style with a destination on a specific ``Coordinator``. public struct ModalCoverModel: Identifiable { public let destination: Destination public let style: ModalCoverModelStyle diff --git a/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift b/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift index 6ebc267..59fad6f 100644 --- a/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift +++ b/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift @@ -1,9 +1,15 @@ import SwiftUI +/// The `NavigationStackFlow` encapsulates the ``SwiftUI.NavigationStack`` and binds it to the +/// variables and callbacks of the ``NavigationStackCoordinator`` which is retains as a ``SwiftUI.StateObject``. public struct NavigationStackFlow: View { @StateObject private var coordinator: Coordinator @ViewBuilder private let content: () -> Content + /// - Parameters: + /// - coordinator: The instance of the coordinator used as the model and retained as the ``SwiftUI.StateObject`` + /// - content: The root view of this navigation stack. The ``navigationDestination(for:destination:)`` modifier + /// is applied to this content. public init(coordinator: @autoclosure @escaping () -> Coordinator, content: @escaping () -> Content) { self._coordinator = StateObject(wrappedValue: coordinator()) self.content = content diff --git a/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift b/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift index efcc477..56c9ce6 100644 --- a/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift +++ b/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift @@ -1,9 +1,16 @@ import SwiftUI +/// The `TabViewFlow` encapsulates the ``SwiftUI.TabView`` and binds it to the +/// variables and callbacks of the ``TabCoordinator`` which is retains as a ``SwiftUI.StateObject``. +/// - Experiment: This API is in preview and subjet to change. public struct TabViewFlow: View { @StateObject private var coordinator: Coordinator @ViewBuilder private let content: () -> Content + /// - Parameters: + /// - coordinator: The instance of the coordinator used as the model and retained as the ``SwiftUI.StateObject`` + /// - content: The definition of tabs held by this TabView should be placed into this ViewBuilder. You are required to use instances of `Tab` + /// type as tags of the views. For an example refer to the template. public init(coordinator: @autoclosure @escaping () -> Coordinator, @ViewBuilder content: @escaping () -> Content) { self._coordinator = StateObject(wrappedValue: coordinator()) self.content = content diff --git a/Sources/FuturedArchitecture/Documentation.docc/Documentation.md b/Sources/FuturedArchitecture/Documentation.docc/Documentation.md index 0354dcd..cc32478 100644 --- a/Sources/FuturedArchitecture/Documentation.docc/Documentation.md +++ b/Sources/FuturedArchitecture/Documentation.docc/Documentation.md @@ -1,20 +1,60 @@ -# ``FuturedKit`` +# FuturedKit -SwiftUI architecture, resources and views used by Futured. +The FuturedKit Architecture is a flow-coordinator, component, component model based architecture. The architecture uses Combine and Swift Concurrency for state management. Originally, we wanted to use Swift Concurrency with Observable, but we needed to backport the architecture to iOS 16. -## Topics +## Main concepts -### Architecture +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). -- ``Coordinator`` -- ``NavigationStackFlow`` -- ``TabViewFlow`` -- ``ComponentModel`` -- ``DataCache`` +![overview](archoverview) -### Views +### Scene -- ``AnyShape`` -- ``WrappedUIImagePicker`` -- ``CameraImagePicker`` -- ``GalleryImagePicker`` +*Scene* is the basic building block of the architecture and comprises of two main building blocks: `Component` and `ComponentModel`. + +- `Component` is a struct conforming to ``SwiftUI.View``. It has generic parameter `Model` which is used to represent the `ComponentModel` of this scene. The `model` is retained as an `ObservableObject`. +- `ComponentModel` consists of three parts: `ComponentModelProtocol`, `MockComponentModel` and the `ComponentModel` itself. We use this decomposition so we can create mocked component model for better SwiftUI Previews experience. + - `ComponentModelProtocol` defines the properties required by the `Component`. It also defines functions, which the `Component` may call in response to an event, such as a button tap. All `ComponentModelProtocol` are required to extend the ``ComponentModel`` protocol. + - `MockComponentModel` should be only used in debug builds (use `#if DEBUG`) and as compact as possible to allow for responsive SwiftUI Previews. + - `ComponentModel` is the implementation of `ComponentModelProtocol` and is responsible for storing the data, sending events to the ``Coordinator`` and subscribing to changes which may be relevant for this scene. + +### Flow Coordinator + +*Flow Coordinator* is a class which manages creation, lifetime and data flows in child-scenes of a container (such as Navigation or Tab). A *flow coordinator* may be also looked upon as a "view model of a container view." All coordinators are required to conform to ``Coordinator`` protocol, but we provide implementations for the most commonly used containers: + + - ``NavigationStackCoordinator`` and associated view ``NavigationStackFlow`` is used for Navigation. + - ``TabCoordinator`` and associated view ``TabViewFlow`` is used for Tabs. + +Flow coordinators are also responsible for presentation of modal views. + +### Container + +Container is not defined as a type or a protocol, but is a part of the architecture. It is used for dependency management. In essence, *container* is only required to be an instance of a class, hold references to all globally accessible instances (for example Services). + +### Data Cache + +``DataCache`` is an actor holding an equatable structure. It is responsible for managing the structure as a source of truth, serializing write operations and exposing the contents as a `Published` variable, so consuments can subscribe to changes. Data cache may be used to store data shared across the app or cache results of an API calls. + +Each application should have one global data cache stored in the `Container`. Individual Coordinators may have their own private Data Caches to coordinate data flows across child scenes. + +**Data stored in Data Cache should be directly the source of truth for views, or mapped using a subscription.** + +### Flow Provider + +Flow providers are optional part of the architecture. It may be used to encapsulate parts of a flow, which may be used in number of coordinators. For more information, visit ``CoordinatorSceneFlowProvider`` and related template. + +## Data Flows + +The idea behind data flows in the Architecture is fairly simple: use closures when talking to a parent, arguments when passing immutable data from parent to a child and multidelegates (such as Published properties) when data passed from parent to a child a subject to change. Below are described some suggestions on how to impement data flows for certain scenarios. + +### Component <-> Component Model + +### Component Model <-> Coordinator + +### Component Model <-> Component Model + +### Component Model -> Component Model using navigation + +### Persisted data models + +Data Flow for persisted data is an open issue. diff --git a/Sources/FuturedArchitecture/Documentation.docc/Images/archoverview.svg b/Sources/FuturedArchitecture/Documentation.docc/Images/archoverview.svg new file mode 100644 index 0000000..c0b1bfa --- /dev/null +++ b/Sources/FuturedArchitecture/Documentation.docc/Images/archoverview.svg @@ -0,0 +1,4 @@ + + + +
@main
MyNewApp:
App
AppCoordinator:
ObsevableObject
Container
UsersListFlowCoordinator:
ObsrevableObject
UserList Scene
User Detail Scene
Push
Edit User Scene
Push
onEvent closure
onEvent closure
NavigationStackFlow
Tab View
TabViewCoordinator:
ObsrevableObject
TabViewFlow
Restaurants
Tab
User List Tab
onEvent(.selectedTab(0)
rootView(with: TabViewCoordinator())
rootView(with: UserListFlowCoordinator())
RestaurantListFlowCoordinator:
ObsrevableObject
Restaurant List Scene
Restaurant Detail Scene
Push
onEvent closure
NavigationStackFlow
onEvent closure
rootView(with: RestaurantListFlowCoordinator())
\ No newline at end of file