Skip to content

Commit

Permalink
Merge pull request #26 from futuredapp/feature/v1.1.0
Browse files Browse the repository at this point in the history
Version 1.1.0
  • Loading branch information
ssestak authored Aug 16, 2024
2 parents 775748d + ab33329 commit 28288d2
Show file tree
Hide file tree
Showing 20 changed files with 366 additions and 103 deletions.
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" : {
"revision" : "ef659dff70d16d27cbe091d365e561c4087e61f5",
"version" : "0.1.0"
}
},
{
"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", from: "0.1.0")
],
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``
67 changes: 67 additions & 0 deletions Sources/FuturedHelpers/Helpers/CoordinatorSceneFlowProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// CoordinatorSceneFlowProvider.swift
//
// Created by Simon Sestak on 31/07/2024.
//

import SwiftUI
import FuturedArchitecture

/**
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: TemplateSceneFlowProvider.Destination)`

# Example #
The scene provider is defined in the coordinator as follows:
```
private lazy var templateSceneFlowProvider: TemplateSceneFlowProvider = {
TemplateSceneFlowProvider(
container: container,
navigateTo: { [weak self] destination in
if destination == .end {
self?.navigate(to: .flowSpecificDestinationAfterEmbededFlow)
} else {
self?.navigate(to: .embeded(destination: destination))
}
}, pop: { [weak self] in
self?.pop()
}
)
}()
```
Then scenes can be provided in flow coordinator like:
```
func scene(for destination: Destination) -> some View {
switch destination {
case let .embededFlow(destination):
templateSceneFlowProvider.scene(for: destination)
case .flowSpecificDestinationAfterEmbededFlow:
SomeComponent(model: ...
}
}
```
*/
public protocol CoordinatorSceneFlowProvider {
associatedtype Destination: Hashable & Identifiable
associatedtype DestinationViews: View

@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
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// ___FILEHEADER___

import EnumIdentable
import FuturedArchitecture
import SwiftUI

final class ___VARIABLE_flowCoordinatorIdentifier___FlowCoordinator: NavigationStackCoordinator {
private var container: ___PACKAGENAME:identifier___Container
private var container: Container

@Published var path: [Destination] = []
@Published var sheet: Destination?
@Published var alertModel: AlertModel?
@Published var modalCover: ModalCoverModel<Destination>?

init(container: ___PACKAGENAME:identifier___Container) {
init(container: Container) {
self.container = container
}

Expand All @@ -30,11 +30,8 @@ final class ___VARIABLE_flowCoordinatorIdentifier___FlowCoordinator: NavigationS
}

extension ___VARIABLE_flowCoordinatorIdentifier___FlowCoordinator {
enum Destination: String, Hashable, Identifiable {
@EnumIdentable
enum Destination: Hashable, Identifiable {
case destination

var id: String {
rawValue
}
}
}
Loading

0 comments on commit 28288d2

Please sign in to comment.