Skip to content

Commit

Permalink
Merge pull request #28 from futuredapp/feature/version-1-documentation
Browse files Browse the repository at this point in the history
Feature: Source api documentation
  • Loading branch information
mikolasstuchlik authored Sep 2, 2024
2 parents 89a3d37 + 2f4587f commit 5f7a43e
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 43 deletions.
164 changes: 164 additions & 0 deletions Documentation Resources/archoverview.drawio

Large diffs are not rendered by default.

55 changes: 28 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions Sources/FuturedArchitecture/Architecture/ComponentModel.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
51 changes: 49 additions & 2 deletions Sources/FuturedArchitecture/Architecture/Coordinator.swift
Original file line number Diff line number Diff line change
@@ -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<Destination>? { 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:
Expand All @@ -35,6 +62,7 @@ public extension Coordinator {
}
}

/// Convenience method for dismissing a modal.
func dismissModal() {
Task { @MainActor in
self.modalCover = nil
Expand All @@ -44,31 +72,50 @@ 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

@MainActor
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
Expand Down
21 changes: 21 additions & 0 deletions Sources/FuturedArchitecture/Architecture/DataCache.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
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<Model: Equatable> {
/// The data held by this data cache.
@inlinable
@Published public private(set) var value: Model

public init(value: Model) {
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<T: Equatable>(_ keyPath: WritableKeyPath<Model, T>, with value: T) {
guard value != self.value[keyPath: keyPath] else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@

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)
case fullscreenCover
#endif
}

/// This struct is a model associating presentation style with a destination on a specific ``Coordinator``.
public struct ModalCoverModel<Destination: Hashable & Identifiable>: Identifiable {
public let destination: Destination
public let style: ModalCoverModelStyle
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Coordinator: NavigationStackCoordinator, Content: View>: 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
Expand Down
7 changes: 7 additions & 0 deletions Sources/FuturedArchitecture/Architecture/TabViewFlow.swift
Original file line number Diff line number Diff line change
@@ -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<Coordinator: TabCoordinator, Content: View>: 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
Expand Down
Loading

0 comments on commit 5f7a43e

Please sign in to comment.