Skip to content

Commit

Permalink
Merge pull request #72 from wwt/swiftui
Browse files Browse the repository at this point in the history
BETA RELEASE - SwiftUI support
  • Loading branch information
brianlombardo authored Jul 15, 2021
2 parents 5c2bc5c + ff5763a commit 89068f9
Show file tree
Hide file tree
Showing 31 changed files with 2,251 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/.jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ readme: README.md
module: SwiftCurrent
author_url: https://github.com/wwt/SwiftCurrent
github_url: https://github.com/wwt/SwiftCurrent
sourcekitten_sourcefile: swiftcurrent-docs.json,swiftcurrentuikit-docs.json
sourcekitten_sourcefile: swiftcurrent-docs.json,swiftcurrentuikit-docs.json,swiftcurrent-swiftui-docs.json
author: WWT and Tyler Thompson
4 changes: 3 additions & 1 deletion .github/document.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
SDK_PATH=`xcrun --sdk iphonesimulator --show-sdk-path`
sourcekitten doc --spm --module-name SwiftCurrent -- -Xswiftc "-sdk" -Xswiftc "$SDK_PATH" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.0-simulator" > swiftcurrent-docs.json
sourcekitten doc --module-name SwiftCurrent_UIKit -- -workspace ./SwiftCurrent.xcworkspace -scheme SwiftCurrent_UIKit -destination "platform=iOS Simulator,name=iPhone 12" > swiftcurrentuikit-docs.json
jazzy --config .github/.jazzy.yaml --podspec SwiftCurrent.podspec --sourcekitten-sourcefile swiftcurrent-docs.json,swiftcurrentuikit-docs.json
sourcekitten doc --spm --module-name SwiftCurrent_SwiftUI -- -Xswiftc "-sdk" -Xswiftc "$SDK_PATH" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.0-simulator" > swiftcurrent-swiftui-docs.json
jazzy --config .github/.jazzy.yaml --podspec SwiftCurrent.podspec --sourcekitten-sourcefile swiftcurrent-docs.json,swiftcurrentuikit-docs.json,swiftcurrent-swiftui-docs.json
rm swiftcurrent-docs.json
rm swiftcurrentuikit-docs.json
rm swiftcurrent-swiftui-docs.json
open docs/index.html
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ let package = Package(
.library(
name: "SwiftCurrent_UIKit",
targets: ["SwiftCurrent_UIKit"]),
.library(
name: "BETA_SwiftCurrent_SwiftUI",
targets: ["SwiftCurrent_SwiftUI"])
],
dependencies: [
.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: Version("2.0.0-beta.1")),
.package(url: "https://github.com/mattgallagher/CwlCatchException.git", from: Version("2.0.0-beta.1")),
.package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/sindresorhus/ExceptionCatcher", from: "2.0.0"),
.package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.8.1")
],
targets: [
.target(
Expand All @@ -27,6 +31,9 @@ let package = Package(
.target(
name: "SwiftCurrent_UIKit",
dependencies: ["SwiftCurrent"]),
.target(
name: "SwiftCurrent_SwiftUI",
dependencies: ["SwiftCurrent"]),
.testTarget(
name: "SwiftCurrentTests",
dependencies: [
Expand All @@ -37,5 +44,15 @@ let package = Package(
.product(name: "Algorithms", package: "swift-algorithms")
],
exclude: ["Info.plist", "SwiftCurrent.xctestplan"]),
.testTarget(
name: "SwiftCurrent-SwiftUITests",
dependencies: [
"SwiftCurrent",
"SwiftCurrent_SwiftUI",
"CwlPreconditionTesting",
"CwlCatchException",
"ViewInspector"
],
path: "Tests/SwiftCurrent_SwiftUITests"),
]
)
27 changes: 22 additions & 5 deletions Sources/SwiftCurrent/Models/FlowRepresentableMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,31 @@ public class FlowRepresentableMetadata {
- Parameter launchStyle: the style to use when launching the `FlowRepresentable`.
- Parameter flowPersistence: a closure passing arguments to the caller and returning the preferred `FlowPersistence`.
*/
public init<FR: FlowRepresentable>(_ flowRepresentableType: FR.Type,
launchStyle: LaunchStyle = .default,
flowPersistence:@escaping (AnyWorkflow.PassedArgs) -> FlowPersistence) {
flowRepresentableFactory = { args in
public convenience init<FR: FlowRepresentable>(_ flowRepresentableType: FR.Type,
launchStyle: LaunchStyle = .default,
flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence) {
self.init(flowRepresentableType,
launchStyle: launchStyle,
flowPersistence: flowPersistence) { args in
AnyFlowRepresentable(FR.self, args: args)
}
self.flowPersistence = flowPersistence
}

/**
Creates an instance that holds onto metadata associated with the `FlowRepresentable`.

- Parameter flowRepresentableType: specific type of the associated `FlowRepresentable`.
- Parameter launchStyle: the style to use when launching the `FlowRepresentable`.
- Parameter flowPersistence: a closure passing arguments to the caller and returning the preferred `FlowPersistence`.
- Parameter flowRepresentableFactory: a closure used to generate an `AnyFlowRepresentable` from the `FlowRepresentable` type.
*/
public init<FR: FlowRepresentable>(_ flowRepresentableType: FR.Type,
launchStyle: LaunchStyle = .default,
flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence,
flowRepresentableFactory: @escaping (AnyWorkflow.PassedArgs) -> AnyFlowRepresentable) {
self.launchStyle = launchStyle
self.flowPersistence = flowPersistence
self.flowRepresentableFactory = flowRepresentableFactory
}

func setPersistence(_ args: AnyWorkflow.PassedArgs) -> FlowPersistence {
Expand Down
7 changes: 7 additions & 0 deletions Sources/SwiftCurrent/Protocols/FlowRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ extension FlowRepresentable where WorkflowOutput == Never {
}
}

extension FlowRepresentable where WorkflowOutput == AnyWorkflow.PassedArgs {
/// Moves forward while passing arguments forward in the `Workflow`; if at the end, calls the `onFinish` closure used when launching the workflow.
public func proceedInWorkflow(_ args: WorkflowOutput) {
_workflowPointer?.proceedInWorkflowStorage?(args)
}
}

extension FlowRepresentable {
/// Moves forward while passing arguments forward in the `Workflow`; if at the end, calls the `onFinish` closure used when launching the workflow.
public func proceedInWorkflow(_ args: WorkflowOutput) {
Expand Down
12 changes: 2 additions & 10 deletions Sources/SwiftCurrent/TestOnly/TestOnly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ extension Notification.Name {
extension FlowRepresentable {
/// :nodoc: Your tests may want to manually set the closure so they can make assertions it was called, this is simply a convenience available for that.
public var proceedInWorkflowStorage: ((AnyWorkflow.PassedArgs) -> Void)? {
get {
{
_workflowPointer?.proceedInWorkflowStorage?($0)
}
}
get { { _workflowPointer?.proceedInWorkflowStorage?($0) } }
set {
_workflowPointer?.proceedInWorkflowStorage = { args in
newValue?(args)
Expand All @@ -35,11 +31,7 @@ extension FlowRepresentable {

/// :nodoc: Designed for V1 and V2 people who used to assign to proceedInWorkflow for tests. This auto extracts args.
public var _proceedInWorkflow: ((Any?) -> Void)? {
get {
{
_workflowPointer?.proceedInWorkflowStorage?(.args($0))
}
}
get { { _workflowPointer?.proceedInWorkflowStorage?(.args($0)) } }
set {
_workflowPointer?.proceedInWorkflowStorage = { args in
newValue?(args.extractArgs(defaultValue: nil))
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCurrent/TypeErased/AnyFlowRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import Foundation

/// A type erased `FlowRepresentable`.
public class AnyFlowRepresentable {
open class AnyFlowRepresentable {
typealias WorkflowInput = Any
typealias WorkflowOutput = Any

Expand Down
57 changes: 57 additions & 0 deletions Sources/SwiftCurrent/TypeErased/AnyWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,34 @@ public class AnyWorkflow {

// swiftlint:disable:next missing_docs
public func _abandon() { storageBase._abandon() }

/// Appends `FlowRepresentableMetadata` to the `Workflow`.
public func append(_ metadata: FlowRepresentableMetadata) {
storageBase.append(metadata)
}

/**
Launches the `Workflow`.

### Discussion
passedArgs are passed to the first instance, it has the opportunity to load, not load and transform them, or just not load.
In the event an instance does not load and does not transform args, they are passed unmodified to the next instance in the `Workflow` until one loads.

- Parameter orchestrationResponder: the `OrchestrationResponder` to notify when the `Workflow` proceeds or backs up.
- Parameter passedArgs: the arguments to pass to the first instance(s).
- Parameter launchStyle: the launch style to use.
- Parameter onFinish: the closure to call when the last element in the workflow proceeds; called with the `AnyWorkflow.PassedArgs` the workflow finished with.
- Returns: the first loaded instance or nil, if none was loaded.
*/
@discardableResult public func launch(withOrchestrationResponder orchestrationResponder: OrchestrationResponder,
passedArgs: AnyWorkflow.PassedArgs,
launchStyle: LaunchStyle = .default,
onFinish: ((AnyWorkflow.PassedArgs) -> Void)? = nil) -> AnyWorkflow.Element? {
storageBase.launch(withOrchestrationResponder: orchestrationResponder,
passedArgs: passedArgs,
launchStyle: launchStyle,
onFinish: onFinish)
}
}

extension AnyWorkflow: Sequence {
Expand Down Expand Up @@ -88,6 +116,21 @@ fileprivate class AnyWorkflowStorageBase {
func last(where _: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
fatalError("last(where:) not overridden by AnyWorkflowStorage")
}

// https://github.com/wwt/SwiftCurrent/blob/main/.github/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
func append(_ metadata: FlowRepresentableMetadata) {
fatalError("append(:) not overridden by AnyWorkflowStorage")
}

// https://github.com/wwt/SwiftCurrent/blob/main/.github/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
@discardableResult func launch(withOrchestrationResponder orchestrationResponder: OrchestrationResponder,
passedArgs: AnyWorkflow.PassedArgs,
launchStyle: LaunchStyle = .default,
onFinish: ((AnyWorkflow.PassedArgs) -> Void)? = nil) -> AnyWorkflow.Element? {
fatalError("launch(orchestrationResponder:passedArgs:launchStyle:onFinish) not overridden by AnyWorkflowStorage")
}
}

fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowStorageBase {
Expand Down Expand Up @@ -119,4 +162,18 @@ fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowSto
override func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
try workflow.last(where: predicate)
}

override func append(_ metadata: FlowRepresentableMetadata) {
workflow.append(metadata)
}

override func launch(withOrchestrationResponder orchestrationResponder: OrchestrationResponder,
passedArgs: AnyWorkflow.PassedArgs,
launchStyle: LaunchStyle = .default,
onFinish: ((AnyWorkflow.PassedArgs) -> Void)? = nil) -> AnyWorkflow.Element? {
workflow.launch(withOrchestrationResponder: orchestrationResponder,
passedArgs: passedArgs,
launchStyle: launchStyle,
onFinish: onFinish)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// swiftlint:disable:this file_name
// AnyWorkflowExtensions.swift
// SwiftCurrent
//
// Created by Tyler Thompson on 7/12/21.
// Copyright © 2021 WWT and Tyler Thompson. All rights reserved.
//

import SwiftCurrent

extension AnyWorkflow {
/// Called when the workflow should be terminated, and the app should return to the point before the workflow was launched.
public func abandon() {
orchestrationResponder?.abandon(self, onFinish: nil)
}
}
18 changes: 18 additions & 0 deletions Sources/SwiftCurrent_SwiftUI/Extensions/WorkflowExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// swiftlint:disable:this file_name
// WorkflowExtensions.swift
// SwiftCurrent
//
// Created by Tyler Thompson on 7/13/21.
// Copyright © 2021 WWT and Tyler Thompson. All rights reserved.
//

import SwiftCurrent
import SwiftUI

@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
extension Workflow where F: FlowRepresentable & View {
/// Called when the workflow should be terminated, and the app should return to the point before the workflow was launched.
public func abandon() {
AnyWorkflow(self).abandon()
}
}
67 changes: 67 additions & 0 deletions Sources/SwiftCurrent_SwiftUI/Models/WorkflowViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// WorkflowViewModel.swift
// SwiftCurrent_SwiftUI
//
// Created by Megan Wiemer on 7/13/21.
// Copyright © 2021 WWT and Tyler Thompson. All rights reserved.
//

import SwiftCurrent
import SwiftUI

@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
final class WorkflowViewModel: ObservableObject {
@Published var body = AnyView(EmptyView())
var isLaunched: Binding<Bool>?
var onAbandon = [() -> Void]()
}

@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
extension WorkflowViewModel: OrchestrationResponder {
func launch(to destination: AnyWorkflow.Element) {
extractView(from: destination).model = self
}

func proceed(to destination: AnyWorkflow.Element, from source: AnyWorkflow.Element) {
extractView(from: destination).model = self
}

func backUp(from source: AnyWorkflow.Element, to destination: AnyWorkflow.Element) {
extractView(from: destination).model = self
}

func abandon(_ workflow: AnyWorkflow, onFinish: (() -> Void)?) {
isLaunched?.wrappedValue = false
onAbandon.forEach { $0() }
}

func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
if workflow.lastLoadedItem?.value.metadata.persistence == .removedAfterProceeding {
if let lastPresentableItem = workflow.lastPresentableItem {
extractView(from: lastPresentableItem).model = self
} else {
isLaunched?.wrappedValue = false
}
}
onFinish?(passedArgs)
}

private func extractView(from element: AnyWorkflow.Element) -> AnyFlowRepresentableView {
guard let instance = element.value.instance as? AnyFlowRepresentableView else {
fatalError("Could not cast \(String(describing: element.value.instance)) to expected type: AnyFlowRepresentableView")
}
return instance
}
}

extension AnyWorkflow {
fileprivate var lastLoadedItem: AnyWorkflow.Element? {
last { $0.value.instance != nil }
}

fileprivate var lastPresentableItem: AnyWorkflow.Element? {
last {
$0.value.instance != nil && $0.value.metadata.persistence != .removedAfterProceeding
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AnyFlowRepresentableView.swift
// SwiftCurrent_SwiftUI
//
// Created by Megan Wiemer on 7/13/21.
// Copyright © 2021 WWT and Tyler Thompson. All rights reserved.
//

import SwiftCurrent
import SwiftUI

@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
final class AnyFlowRepresentableView: AnyFlowRepresentable {
var model: WorkflowViewModel? {
didSet {
setViewOnModel()
}
}
private var setViewOnModel = { }

init<FR: FlowRepresentable & View>(type: FR.Type, args: AnyWorkflow.PassedArgs) {
super.init(type, args: args)
guard let instance = underlyingInstance as? FR else {
fatalError("Could not cast \(String(describing: underlyingInstance)) to expected type: \(FR.self)")
}
setViewOnModel = { [weak self] in
self?.model?.body = AnyView(instance)
}
}

func changeUnderlyingView<V: View>(to view: V) {
setViewOnModel = { [weak self] in
self?.model?.body = AnyView(view)
}
}
}
22 changes: 22 additions & 0 deletions Sources/SwiftCurrent_SwiftUI/ViewInspector/Inspection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Inspection.swift
// SwiftCurrent
//
// Created by Tyler Thompson on 7/12/21.
// Copyright © 2021 WWT and Tyler Thompson. All rights reserved.
//

import SwiftUI
import Combine

// Necessary for ViewInspector tests
@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
final class Inspection<V> where V: View {
let notice = PassthroughSubject<UInt, Never>()
var callbacks = [UInt: (V) -> Void]()
func visit(_ view: V, _ line: UInt) {
if let callback = callbacks.removeValue(forKey: line) {
callback(view)
}
}
}
Loading

0 comments on commit 89068f9

Please sign in to comment.