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 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" : {
"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

/**
mikolasstuchlik marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we should make the @EnumIdentable add the conformance. I don't think it's MUST-HAVE at this point, but please, file an Issue to this effect.

case destination

var id: String {
rawValue
}
}
}
Loading
Loading