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.0.1 #25

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
19 changes: 14 additions & 5 deletions Sources/FuturedArchitecture/Architecture/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ 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
Expand All @@ -36,6 +35,16 @@ public extension Coordinator {
}

func onSheetDismiss() {}

var sheet: Destination? {
get { modalCover?.style == .sheet ? modalCover?.destination : nil }
set { modalCover = newValue.map { ModalCoverModel(destination: $0, style: .sheet) } }
}

var fullscreenCover: Destination? {
get { modalCover?.style == .fullscreenCover ? modalCover?.destination : nil }
set { modalCover = newValue.map { ModalCoverModel(destination: $0, style: .fullscreenCover) } }
}
}

#if !os(macOS)
Expand Down
22 changes: 22 additions & 0 deletions Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// 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

enum Style {
case sheet
case fullscreenCover
}

public var id: Destination.ID {
destination.id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public struct NavigationStackFlow<Coordinator: NavigationStackCoordinator, Conte
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)
}
#else
public var body: some View {
Expand All @@ -24,7 +23,6 @@ public struct NavigationStackFlow<Coordinator: NavigationStackCoordinator, Conte
}
.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)
}
#endif
}
2 changes: 0 additions & 2 deletions Sources/FuturedArchitecture/Architecture/TabViewFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
content()
}
.sheet(item: $coordinator.sheet, onDismiss: coordinator.onSheetDismiss, content: coordinator.scene(for:))
.defaultAlert(model: $coordinator.alertModel)
}
#else
public var body: some View {
Expand All @@ -24,7 +23,6 @@ public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
}
.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)
}
#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``
210 changes: 210 additions & 0 deletions Sources/FuturedHelpers/Helpers/CoordinatorSceneFlowProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//
// CoordinatorSceneFlowProvider.swift
//
// Created by Simon Sestak on 31/07/2024.
//

import SwiftUI

/** A protocol to represent navigable destinations in a reusable scene flow
```
protocol TemplateFlowDestination: CoordinatorSceneFlowDestination {
static var destination: Self { get }
static var end: Self { get }
}
```
**/
protocol CoordinatorSceneFlowDestination: Hashable, Identifiable, Equatable {}

/**
A protocol defining an interface for reusable scene flow providers.
```
final class TemplateSceneFlowProvider: CoordinatorSceneFlowProvider {
let onNavigateToDestination: (Destination) -> Void
let onPop: () -> Void
let onPresentSheet: ((Destination) -> Void)? = nil
let onDismissSheet: (() -> Void)? = nil
let onPresentFullscreenCover: ((Destination) -> Void)? = nil
let onDismissFullscreenCover: (() -> Void)? = nil
let onPopToDestination: ((Destination?) -> Void)? = nil
let onShowError: ((AppError) -> Void)? = nil

private let container: BasicCapitalContainer

init(container: BasicCapitalContainer, onNavigateToDestination: @escaping (Destination) -> Void, onPop: @escaping () -> Void) {
self.container = container
self.onNavigateToDestination = onNavigateToDestination
self.onPop = onPop
}

static func rootView(with instance: TemplateSceneFlowProvider) -> some View {
LearnComponent(
model: LearnComponentModel(
dataCache: instance.container.dataCache
) { [weak instance] event in
switch event {
case .showUnderstandBasicCapital:
instance?.navigate(to: .destination)
}
}
)
}

func scene(for destination: Destination) -> some View {
switch destination {
case .destination:
Color.red
case .end:
EmptyView()
}
}

enum Destination: String, TemplateFlowDestination {
case destination
case end

var id: String {
rawValue
}
}
}

final class TemplateCoordinator: NavigationStackCoordinator {
@Published var path: [Destination] = []
@Published var sheet: Destination?
@Published var fullscreenCover: Destination?
@Published var alertModel: AlertModel?

private let container: BasicCapitalContainer

private lazy var templateSceneProvider: TemplateSceneFlowProvider = {
TemplateSceneFlowProvider(container: container, onNavigateToDestination: { [weak self] destination in
if destination == .end {
self?.navigate(to: .flowSpecificDestinationAfterEmbededFlow)
} else {
self?.navigate(to: destination)
}
}, onPop: { [weak self] in
self?.pop()
})
}()

init(container: BasicCapitalContainer) {
self.container = container
}

static func rootView(with instance: TemplateCoordinator) -> some View {
NavigationStackFlow(coordinator: instance) {
SomeSpeficComponent(
model: SomeSpeficComponentModel(
dataCache: instance.container.dataCache
) { [weak instance] event in
switch event {
case .showFlowSpecificDestination:
instance?.navigate(to: .flowSpecificDestination)
case .showEmbededFlow:
instance?.navigate(to: .embededFlow(destination: nil))
}
}
)
}
}

func scene(for destination: Destination) -> some View {
switch destination {
case let .embededFlow(destination):
embededFlowScenes(destination: destination)
default:
FlowSpecificComponent(model: FlowSpecificComponentModel())
}
}

@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)
}
}

enum Destination: Hashable, Identifiable {
case flowSpecificDestination
case embededFlow(destination: (any TemplateFlowDestination)?)
case flowSpecificDestinationAfterEmbededFlow

var id: String {
switch self {
case .flowSpecificDestination:
"flowSpecificDestination"
case let .embededFlow(destination):
"embededFlow-\(destination?.id ?? "")"
case .flowSpecificDestinationAfterEmbededFlow:
"flowSpecificDestinationAfterEmbededFlow"
}
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}
```
**/

protocol CoordinatorSceneFlowProvider {
associatedtype RootView: View
associatedtype DestinationViews: View
associatedtype Destination: Hashable & Identifiable

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

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

var onNavigateToDestination: (Destination) -> Void { get }
var onPop: () -> Void { get }

var onPresentSheet: ((Destination) -> Void)? { get }
var onDismissSheet: (() -> Void)? { get }
var onPresentFullscreenCover: ((Destination) -> Void)? { get }
var onDismissFullscreenCover: (() -> Void)? { get }
var onPopToDestination: ((Destination?) -> Void)? { get }
var onShowError: ((Error) -> Void)? { get }
}

extension CoordinatorSceneFlowProvider {
func navigate(to destination: Destination) {
onNavigateToDestination(destination)
}

func pop() {
onPop()
}

func present(sheet: Destination) {
onPresentSheet?(sheet)
}

func onSheetDismiss() {
onDismissSheet?()
}

func present(fullscreenCover: Destination) {
onPresentFullscreenCover?(fullscreenCover)
}

func onFullscreenCoverDismiss() {
onDismissFullscreenCover?()
}

func pop(to destination: Destination?) {
onPopToDestination?(destination)
}

func show(error: Error) {
onShowError?(error)
}
}

Loading
Loading