Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1.1.0 #26

Merged
merged 18 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "0.2.0"
}
},
{
"identity" : "futured-macros",
"kind" : "remoteSourceControl",
"location" : "https://github.com/futuredapp/futured-macros",
"state" : {
"branch" : "main",
"revision" : "e2d832df517e1dd00be6c45a1560b349ca9d337f"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
}
],
"version" : 2
Expand Down
11 changes: 8 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// swift-tools-version:5.7.1
// swift-tools-version:5.9

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "FuturedKit",
Expand All @@ -22,11 +23,15 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/mkj-is/BindingKit", from: "1.0.0"),
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit", from: "0.1.0")
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit", from: "0.1.0"),
.package(url: "https://github.com/futuredapp/futured-macros", branch: "main")
],
targets: [
.target(
name: "FuturedArchitecture"
name: "FuturedArchitecture",
dependencies: [
.product(name: "FuturedMacros", package: "futured-macros")
]
),
.target(
name: "FuturedHelpers",
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ SwiftUI state management tools, resources and views used by Futured.
- ``CameraImagePicker``
- ``GalleryImagePicker``

### Alert presentation

- ``AlertModel``

## Installation

When using Swift package manager install using or add following line to your dependencies:
Expand Down
53 changes: 20 additions & 33 deletions Sources/FuturedArchitecture/Architecture/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,43 @@ public protocol Coordinator: ObservableObject {
associatedtype RootView: View
associatedtype DestinationViews: View

/// `rootView` returns the coordinator's main view. 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.
@ViewBuilder
static func rootView(with instance: Self) -> RootView

var sheet: Destination? { get set }
#if !os(macOS)
var fullscreenCover: Destination? { get set }
#endif
var alertModel: AlertModel? { get set }
var modalCover: ModalCoverModel<Destination>? { get set }

@ViewBuilder
func scene(for destination: Destination) -> DestinationViews
func onSheetDismiss()
#if !os(macOS)
func onFullscreenCoverDismiss()
#endif
func onModalDismiss()
}

public extension Coordinator {
func present(sheet: Destination) {
Task { @MainActor in
self.sheet = sheet
}
}

func dismissSheet() {
Task { @MainActor in
self.sheet = nil
}
}

func onSheetDismiss() {}
}

#if !os(macOS)
public extension Coordinator {
func present(fullscreenCover: Destination) {
Task { @MainActor in
self.fullscreenCover = fullscreenCover
func present(modal destination: Destination, type: ModalCoverModel<Destination>.Style) {
switch type {
case .sheet:
Task { @MainActor in
self.modalCover = .init(destination: destination, style: .sheet)
}
#if !os(macOS)
case .fullscreenCover:
Task { @MainActor in
self.modalCover = .init(destination: destination, style: .fullscreenCover)
}
#endif
}
}

func dismissFullscreenCover() {
func dismissModal() {
Task { @MainActor in
self.fullscreenCover = nil
self.modalCover = nil
}
}

func onFullscreenCoverDismiss() {}
func onModalDismiss() {}
}
#endif

public protocol TabCoordinator: Coordinator {
associatedtype Tab: Hashable
Expand Down
24 changes: 24 additions & 0 deletions Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ModalCoverModel.swift
//
//
// Created by Simon Sestak on 01/08/2024.
//

import Foundation

public struct ModalCoverModel<Destination: Hashable & Identifiable>: Identifiable {
let destination: Destination
let style: Style

public enum Style {
case sheet
#if !os(macOS)
case fullscreenCover
#endif
}

public var id: Destination.ID {
destination.id
}
}
26 changes: 21 additions & 5 deletions Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@ public struct NavigationStackFlow<Coordinator: NavigationStackCoordinator, Conte
NavigationStack(path: $coordinator.path) {
content().navigationDestination(for: Coordinator.Destination.self, destination: coordinator.scene(for:))
}
.sheet(item: $coordinator.sheet, onDismiss: coordinator.onSheetDismiss, content: coordinator.scene(for:))
.defaultAlert(model: $coordinator.alertModel)
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#else
public var body: some View {
NavigationStack(path: $coordinator.path) {
content().navigationDestination(for: Coordinator.Destination.self, destination: coordinator.scene(for:))
}
.sheet(item: $coordinator.sheet, onDismiss: coordinator.onSheetDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: $coordinator.fullscreenCover, onDismiss: coordinator.onFullscreenCoverDismiss, content: coordinator.scene(for:))
.defaultAlert(model: $coordinator.alertModel)
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#endif

private var sheetBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .sheet ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .sheet) }
}
}

#if !os(macOS)
private var fullscreenCoverBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) }
}
}
#endif
}
26 changes: 21 additions & 5 deletions Sources/FuturedArchitecture/Architecture/TabViewFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@ public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
TabView(selection: $coordinator.selectedTab) {
content()
}
.sheet(item: $coordinator.sheet, onDismiss: coordinator.onSheetDismiss, content: coordinator.scene(for:))
.defaultAlert(model: $coordinator.alertModel)
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#else
public var body: some View {
TabView(selection: $coordinator.selectedTab) {
content()
}
.sheet(item: $coordinator.sheet, onDismiss: coordinator.onSheetDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: $coordinator.fullscreenCover, onDismiss: coordinator.onFullscreenCoverDismiss, content: coordinator.scene(for:))
.defaultAlert(model: $coordinator.alertModel)
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#endif

private var sheetBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .sheet ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .sheet) }
}
}

#if !os(macOS)
private var fullscreenCoverBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) }
}
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@ SwiftUI architecture, resources and views used by Futured.
- ``WrappedUIImagePicker``
- ``CameraImagePicker``
- ``GalleryImagePicker``

### Alert presentation

- ``AlertModel``
91 changes: 91 additions & 0 deletions Sources/FuturedHelpers/Helpers/CoordinatorSceneFlowProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// CoordinatorSceneFlowProvider.swift
//
// Created by Simon Sestak on 31/07/2024.
//

import SwiftUI
import FuturedArchitecture

/**
A typealias representing navigable destinations in a reusable scene flow.

# Notes: #
1. If the scenes defined in the provider can continue with other scenes, define an end destination.
- This end destination should trigger a scene display outside of the scene provider.
2. Other destinations will be used to display scenes defined within the scene provider.

# Example #
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Example #
# Example #
Declare a `protocol` conforming to the composition. The protocol should only be conformed to by enums. We use the requirements to force appearance of certain cases: [SE-0280](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0280-enum-cases-as-protocol-witnesses.md).

```
protocol TemplateFlowDestination: CoordinatorSceneFlowDestination {
static var destination: Self { get }
static var end: Self { get }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should use naming which does not appear to have a special meaning, if it is only an example.

Suggested change
static var destination: Self { get }
static var end: Self { get }
static var someDestination: Self { get }
static var otherDestionation: Self { get }

}
```
*/
public typealias CoordinatorSceneFlowDestination = Hashable & Identifiable & Equatable


/**
A protocol providing an interface for reusable scene flow providers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a reusable scene flow provider? Is my suggestion correct?

Suggested change
A protocol providing an interface for reusable scene flow providers.
A protocol providing an interface for reusable scene flow providers. *Reusable scene flow provider* is part of a scene flow, which can be used as a part of more *Flow Coordinators*. The shared section of the flow is taken out of the *Flow Coordinator* and placed into a class conforming to `CoordinatorSceneFlowProvider`.

Protocol defines necessary (`navigateTo`, `pop`) and optional navigation functions.
Optional functions cater to specific navigational use cases like presenting/dismissing modal screen, and popping to destinations.

- Warning: The `@EnumIndetable` macro won't function with this scene provider as you need to define a destination with an associated value, which isn't primitive.

# Notes: #
1. Declare the scene flow provider as a lazy var property in the coordinator.
2. Coordinator destinations should have an enum that encapsulates flow provider destinations.
- `case embededFlow(destination: (any TemplateFlowDestination)?)`
3. To display the first scene of a scene provider, navigate to the embedded flow with nil.
- `instance?.navigate(to: .embededFlow(destination: nil))`

# Example #
The scene provider is defined in the coordinator as follows:
```
private lazy var templateSceneProvider: TemplateSceneFlowProvider = {
TemplateSceneFlowProvider(
container: container,
navigateTo: { [weak self] destination in
if destination == .end {
self?.navigate(to: .flowSpecificDestinationAfterEmbededFlow)
} else {
self?.navigate(to: destination)
}
}, pop: { [weak self] in
self?.pop()
}
)
}()
```
To create a scene from the SceneFlowProvider:
```
@ViewBuilder
private func embededFlowScenes(destination: (any TemplateFlowDestination)?) -> some View {
if let destination = destination as? TemplateSceneFlowProvider.Destination {
templateSceneProvider.scene(for: destination)
} else {
TemplateSceneFlowProvider.rootView(with: templateSceneProvider)
}
}
```
*/
public protocol CoordinatorSceneFlowProvider {
associatedtype Destination: Hashable & Identifiable
associatedtype RootView: View
associatedtype DestinationViews: View

@ViewBuilder
static func rootView(with instance: Self) -> RootView

@ViewBuilder
func scene(for destination: Destination) -> DestinationViews

var navigateTo: (Destination) -> Void { get }
var pop: () -> Void { get }

var present: ((Destination, ModalCoverModel<Destination>.Style) -> Void)? { get }
var dismissModal: (() -> Void)? { get }
var onModalDismiss: (() -> Void)? { get }
var popTo: ((Destination?) -> Void)? { get }
}
44 changes: 44 additions & 0 deletions Sources/FuturedHelpers/Helpers/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// SceneDelegate.swift
//
//
// Created by Simon Sestak on 31/07/2024.
//

import SwiftUI

#if !os(macOS)
protocol AppSceneDelegate: AnyObject, UIWindowSceneDelegate, ObservableObject {
var delegate: SceneDelegate? { get set }
}

protocol SceneDelegate: AnyObject {
func sceneDidEnterBackground(_ scene: UIScene)
func sceneWillEnterForeground(_ scene: UIScene)
}

private struct SceneDelegateWrapperViewModifier<ApSceneDelegate: AppSceneDelegate>: ViewModifier {
@EnvironmentObject private var sceneDelegate: ApSceneDelegate

let delegate: SceneDelegate?

func body(content: Content) -> some View {
content
.onAppear {
sceneDelegate.delegate = delegate
}
}
}

extension View {
/// Sets the SceneDelegate for the application.
/// - Parameter appSceneDelegateClass: The call which conforms to the UIWindowSceneDelegate.
/// - Parameter sceneDelegate: The SceneDelegate to set.
/// - Description:
/// In the main app root view call this modifier and pass the SceneDelegate. You need to specify the AppSceneDelegate which conforms to the UIWindowSceneDelegate.
/// This is necessary because the SceneDelegate is accessible in SwiftUI only via EnviromentObject.
func set<T: AppSceneDelegate>(appSceneDelegateClass: T.Type, sceneDelegate: SceneDelegate) -> some View {
modifier(SceneDelegateWrapperViewModifier<T>(delegate: sceneDelegate))
}
}
#endif
Loading
Loading