diff --git a/.github/DocsPostProcessor/.gitignore b/.github/DocsPostProcessor/.gitignore new file mode 100644 index 000000000..bb460e7be --- /dev/null +++ b/.github/DocsPostProcessor/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.github/DocsPostProcessor/Package.swift b/.github/DocsPostProcessor/Package.swift new file mode 100644 index 000000000..8075a5fb9 --- /dev/null +++ b/.github/DocsPostProcessor/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.4 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "DocsPostProcessor", + products: [ + .executable( + name: "DocsPostProcessor", + targets: ["DocsPostProcessor"]), + ], + dependencies: [ + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "1.7.4"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "0.5.0"), + ], + targets: [ + .executableTarget( + name: "DocsPostProcessor", + dependencies: [ + "SwiftSoup", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ]), + ] +) diff --git a/.github/DocsPostProcessor/Sources/DocsPostProcessor/DocsPostProcessor.swift b/.github/DocsPostProcessor/Sources/DocsPostProcessor/DocsPostProcessor.swift new file mode 100644 index 000000000..87ed69d9f --- /dev/null +++ b/.github/DocsPostProcessor/Sources/DocsPostProcessor/DocsPostProcessor.swift @@ -0,0 +1,65 @@ +import Foundation +import ArgumentParser +import SwiftSoup + +struct DocsPostProcessor: ParsableCommand { + enum Err: Error { + case noOverviewFound + } + + @Flag(help: "Replaces overview in the left navigation with rendered README") + var replaceOverviewWithReadme = false + + @Flag(help: "Replaces readme video link with an embedded vimeo player") + var replaceReadmeVideoWithVimeoEmbed = false + + @Argument(help: "The file, or directory with the HTML to change") + var path: String + + mutating func run() throws { + let url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(path) + var files: [URL] = (url.pathExtension == "html") ? [url] : [] + if url.pathExtension != "html", let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]) { + for case let fileURL as URL in enumerator { + let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey]) + if fileAttributes.isRegularFile == true, fileURL.pathExtension == "html" { + files.append(fileURL) + } + } + } + for file in files { + let html = try String(contentsOf: file, encoding: .utf8) + let doc: Document = try SwiftSoup.parse(html) + if replaceOverviewWithReadme { + try replaceOverviewWithReadme(document: doc) + print("Replaced overview with README for: \(file.absoluteString)") + } + if replaceReadmeVideoWithVimeoEmbed, file.lastPathComponent == "index.html" { + try replaceReadmeVideoWithVimeoEmbed(document: doc) + print("Replaced readme video with vimeo embed") + } + try doc.html().write(to: file, atomically: true, encoding: .utf8) + } + } + + func replaceOverviewWithReadme(document: Document) throws { + if let nav = try? document.select("nav").first() { + if let firstNavLink = try nav.getElementsByClass("nav-group-name-link").first(), + let oldUrl = URL(string: try firstNavLink.attr("href")), + oldUrl.lastPathComponent == "Overview.html" { + try firstNavLink.attr("href", oldUrl.deletingLastPathComponent().appendingPathComponent("index.html").absoluteString) + } else { + throw Err.noOverviewFound + } + } + } + + func replaceReadmeVideoWithVimeoEmbed(document: Document) throws { + if let article = try? document.select("article").first() { + let link = try article.getElementsByAttributeValue("href", "https://user-images.githubusercontent.com/33705774/132767762-7447753c-feba-4ef4-b54c-38bfe9d1ee82.mp4") + try link.wrap(""" +
+ """) + } + } +} diff --git a/.github/DocsPostProcessor/Sources/DocsPostProcessor/main.swift b/.github/DocsPostProcessor/Sources/DocsPostProcessor/main.swift new file mode 100644 index 000000000..6ea97496b --- /dev/null +++ b/.github/DocsPostProcessor/Sources/DocsPostProcessor/main.swift @@ -0,0 +1 @@ +DocsPostProcessor.main() \ No newline at end of file diff --git a/.github/abstract/Controlling Presentation.md b/.github/abstract/Controlling Presentation.md new file mode 100644 index 000000000..4b412b494 --- /dev/null +++ b/.github/abstract/Controlling Presentation.md @@ -0,0 +1,12 @@ +SwiftCurrent allows you to control how your workflow presents its `FlowRepresentable`s. This control over presentation is different for UIKit and SwiftUI. You can also control `FlowPersistence` which is a description for what should happen to an item in a `Workflow` if it's been skipped, or once the workflow has proceeded. + +### UIKit +In UIKit you control presentation using `LaunchStyle.PresentationType`. The default is a contextual presentation mode. If it detects you are in a navigation view, it'll present by pushing onto the navigation stack. If it cannot detect a navigation view it presents modally. Alternatively you can explicitly state you'd like it to present modally or in a navigation stack when you define your `Workflow`. + +### In SwiftUI +In SwiftUI you control presentation using `LaunchStyle.SwiftUI.PresentationType`. The default is simple view replacement. This is especially powerful because your workflows in SwiftUI do not need to be an entire screen, they can be just part of a view. Using the default presentation type you can also get fine grained control over animations. You can also explicitly state you'd like it to present modally (using a sheet, or fullScreenCover) or in a navigation stack when you define your `WorkflowLauncher`. + +### Persistence +You can control what happens to items in your workflow using `FlowPersistence`. Using `FlowPersistence.persistWhenSkipped` means that when `FlowRepresentable.shouldLoad` returns false the item is still stored on the workflow. If for example, you're in a navigation stack this means the item *is* skipped, but you can back up to it. + +Using `FlowPersistence.removedAfterProceeding` means once the `Workflow` has proceeded, the item is removed, references to it are cleaned up, and it is removed from any back stacks. \ No newline at end of file diff --git a/.github/abstract/Creating Workflows in SwiftUI.md b/.github/abstract/Creating Workflows in SwiftUI.md index e58b1f27d..54bbfee87 100644 --- a/.github/abstract/Creating Workflows in SwiftUI.md +++ b/.github/abstract/Creating Workflows in SwiftUI.md @@ -19,7 +19,7 @@ struct FirstView: View, FlowRepresentable { > Note: The `_workflowPointer` is needed as an anchor point for your `Workflow`. You do not have to worry about setting it, you merely need space for it on your structs. SwiftUI actually does the exact same thing with a `_location` variable, it's just that Apple has secret compiler magic to hide that. Unfortunately, that compiler magic is not shared. -> Note: `Workflow.proceedInWorkflow()` is what you call to have your view move forward to the next item in the `Workflow` it is part of. +> Note: `FlowRepresentable.proceedInWorkflow()` is what you call to have your view move forward to the next item in the `Workflow` it is part of. ### Step 2: Define your `WorkflowLauncher`. This indicates whether the workflow is shown and describes what items are in it. diff --git a/.github/abstract/Creating Workflows in UIKit.md b/.github/abstract/Creating Workflows in UIKit.md new file mode 100644 index 000000000..dee541580 --- /dev/null +++ b/.github/abstract/Creating Workflows in UIKit.md @@ -0,0 +1,43 @@ +### Step 1: +To create workflows in UIKit you should start with a `UIViewController` that should be part of a `Workflow` and modify it to be `FlowRepresentable` + +#### Example: +```swift +import UIKit +import SwiftCurrent +import SwiftCurrent_UIKit + +// This screen shows an employee only screen +class FirstViewController: UIWorkflowItem, FlowRepresentable { // SwiftCurrent + private let email: String + private let finishButton = UIButton() + + required init(with email: String) { // SwiftCurrent + self.email = email + super.init(nibName: nil, bundle: nil) + // Configure your view programmatically or look at StoryboardLoadable to use storyboards. + } + + required init?(coder: NSCoder) { nil } + + @objc private func finishPressed() { + proceedInWorkflow(email) // SwiftCurrent + } +} +``` + +> Note: `FlowRepresentable.proceedInWorkflow()` is what you call to have your view move forward to the next item in the `Workflow` it is part of. + +### Step 2: +Define your `Workflow` and launch it. This is what allows you to configure or re-order your workflow. + +#### Example: +```swift +// From the ViewController you'd like to launch the workflow +@objc private func didTapLaunchWorkflow() { + let workflow = Workflow(FirstViewController.self) // SwiftCurrent + .thenPresent(SecondViewController.self) // SwiftCurrent + + launchInto(workflow, args: "Some starting arguments") +} +``` \ No newline at end of file diff --git a/.github/abstract/Creating Workflows.md b/.github/abstract/Creating Workflows.md new file mode 100644 index 000000000..8398af43a --- /dev/null +++ b/.github/abstract/Creating Workflows.md @@ -0,0 +1,8 @@ +### What is a workflow? +A `Workflow` is a description for a sequence of `FlowRepresentable`. Those operations are normally displaying screens or views. There are different ways of [creating workflows in SwiftUI](Creating%20Workflows%20in%20SwiftUI.html) and [creating workflows in UIKit](Creating%20Workflows%20in%20UIKit.html). + +### What is a `FlowRepresentable`? +A `FlowRepresentable` is a protocol that describes a type that can appear in a `Workflow`. It optionally declares `FlowRepresentable.WorkflowInput` and `FlowRepresentable.WorkflowOutput` indicating whether it requires data or whether it passes data forward. It also provides an initializer so that it can be created by the `Workflow`. Once an item conforms to `FlowRepresentable` it is capable of being injected into any `Workflow`. + +### Valid workflow sequences +Workflows enforce (either at compile-time or run-time) that the sequence of `FlowRepresentables` are well-formed. For example if a particular `FlowRepresentable` has a `FlowRepresentable.WorkflowInput` of `String` but the previous item passes an `Int` forward, that workflow is malformed. It's possible for a `FlowRepresentable` to declare a `FlowRepresentable.WorkflowInput` of `Never` if it does not take in data, `Any` if it can intake multiple types, or `AnyWorkflow.PassedArgs` if it can handle being passed no data, or data of any kind. Lastly, if your `FlowRepresentable.WorkflowInput` needs to take in multiple values you can use a tuple. \ No newline at end of file diff --git a/.github/abstract/How to use SwiftCurrent with UIKit.md b/.github/abstract/How to use SwiftCurrent with UIKit.md new file mode 100644 index 000000000..48bbf160c --- /dev/null +++ b/.github/abstract/How to use SwiftCurrent with UIKit.md @@ -0,0 +1 @@ +SwiftCurrent works well with UIKit. To work correctly it does require `FlowRepresentable`s to be `UIViewController`s and not `UIView`s. It comes with [support for Storyboards](using-storyboards.html) with `StoryboardLoadable` and [programmatic views](using-programmatic-views.html). \ No newline at end of file diff --git a/.github/abstract/Type Erasure.md b/.github/abstract/Type Erasure.md new file mode 100644 index 000000000..11930cd56 --- /dev/null +++ b/.github/abstract/Type Erasure.md @@ -0,0 +1,4 @@ +Sometimes it may be desireable to explicitly remove type information. Often times this is when you want to pass around types with complicated generic signatures. SwiftCurrent, much like many of Apple's standard libraries, ships with several type erasers for your convenience. + +### `AnyWorkflow.PassedArgs` +The type `AnyWorkflow.PassedArgs` is worth calling out separately. It's very similar to Swift's standard `Optional` type, but with a crucial difference. Consumers of SwiftCurrent need to be able to clearly differentiate between `nil` being passed in a `workflow` that proceeded and no arguments at all being passed. So if a `FlowRepresentable.WorkflowInput` is `Any?` that means it can accept any kind of data, even if that data is optional, but data *must* be passed to it. If a `FlowRepresentable.WorkflowInput` is `AnyWorkflow.PassedArgs` it means that it can take any kind of data, including nil data, OR it can take no data at all. \ No newline at end of file diff --git a/.github/abstract/Underlying Types.md b/.github/abstract/Underlying Types.md new file mode 100644 index 000000000..e0b42b094 --- /dev/null +++ b/.github/abstract/Underlying Types.md @@ -0,0 +1 @@ +These types are required to make SwiftCurrent function. Most of these are public symbols because of the modular design of SwiftCurrent. Some of them are public in case you want to extend SwiftCurrent to do even more. Generally speaking, these types are not something you need to worry about. \ No newline at end of file diff --git a/.github/document.sh b/.github/document.sh index 5ca821283..bb7a26712 100755 --- a/.github/document.sh +++ b/.github/document.sh @@ -6,4 +6,6 @@ jazzy --config .github/.jazzy.yaml --podspec SwiftCurrent.podspec --sourcekitten rm swiftcurrent-docs.json rm swiftcurrentuikit-docs.json rm swiftcurrent-swiftui-docs.json -open docs/index.html \ No newline at end of file +cd .github/DocsPostProcessor +swift run DocsPostProcessor ../../Docs --replace-overview-with-readme --replace-readme-video-with-vimeo-embed +open ../../docs/index.html \ No newline at end of file diff --git a/README.md b/README.md index d6e89e2f9..e410ad4a6 100644 --- a/README.md +++ b/README.md @@ -13,81 +13,25 @@ SwiftCurrent is a library that lets you easily manage journeys through your Swift application. -When Developing in UIKit, each view controller has to know about the one following it in order to share data. Now imagine a flow where the first 3 screens are optional. What would it look like if you could decouple all of that? +It comes with built-in support for UIKit and SwiftUI app-routing. In SwiftCurrent workflows are a sequence of operations. Those operations are normally showing views in an application. The workflow describes the sequence of views and manages what view should come next. Your views are responsible for performing necessary tasks before proceeding forward in the workflow, like processing user input. -```swift -let workflow = Workflow(LocationsViewController.self) // Skip this if you have GPS - .thenProceed(with: PickupOrDeliveryViewController.self) // Skip this if you only have 1 choice - .thenProceed(with: MenuSelectionViewController.self) // Skip this for new stores - .thenProceed(with: FoodSelectionViewController.self) - .thenProceed(with: ReviewOrderViewController.self) // This lets you edit anything you've already picked - .thenProceed(with: SubmitPaymentViewController.self) - -// from wherever this flow is launched -launchInto(workflow) -``` -The above code is all that is needed from the screen starting this flow. Each screen determines if it needs to show based on data passed in and what that screen knows about the system (such as GPS availability), and all of it is type safe. If you ever want to re-order these, simply move their position in the chain. - -As you continue to develop your applications, each view controller will become more decoupled from the rest of the app. That means, if you want a completely different order of screens, just define a new [Workflow](https://wwt.github.io/SwiftCurrent/Classes/Workflow.html). - -## See it in action with our example app - -Clone our repo, open `SwiftCurrent.xcworkspace`, target the `UIKitExample` scheme, and run to see our example app in action. +https://user-images.githubusercontent.com/33705774/132767762-7447753c-feba-4ef4-b54c-38bfe9d1ee82.mp4 -The [example app has a README](https://github.com/wwt/SwiftCurrent/blob/main/ExampleApps/UIKitExample/README.md) that details interesting usages. +### Why should I use SwiftCurrent? +Architectural patterns and libraries that attempt to create a separation between views and workflows already exist. However, SwiftCurrent is different. We took a new design approach that focuses on -## Interested but you need SwiftUI support? +- **A Developer Friendly API**: The library was built with developers in mind. It started with a group of developers talking about the code experience they desired. Then the library team took on whatever complexities were necessary to bring them that experience. +- **Compile-time safety**: We tell you at compile time everything we can so you know things will work. +- **Minimal Boilerplate**: We have hidden this as much as possible. We hate it as much as you do and are constantly working on cutting the cruft. -[We're working on it now!](https://github.com/wwt/SwiftCurrent/milestone/2) - -If you would like to try the beta release, please install the `BETA_SwiftCurrent_SwiftUI` product in SPM or the `BETA_SwiftUI` sub spec in CocoaPods. For more detailed steps, [see our installation instructions](https://wwt.github.io/SwiftCurrent/installation.html). See [Getting Started with SwiftUI](https://wwt.github.io/SwiftCurrent/getting-started-with-swiftui.html) for a quick tutorial. To see the example app for SwiftUI, clone our repo, open `SwiftCurrent.xcworkspace`, target the `SwiftUIExample` scheme, and run. The [example app has a README](https://github.com/wwt/SwiftCurrent/blob/main/ExampleApps/SwiftUIExample/README.md) that details interesting usages. - -In order to use the library with SwiftUI, your minimum targeted versions must meet: iOS 14.0, macOS 11, tvOS 14.0, or watchOS 7.0. - -For us, beta means that the API may change without warning until the full release. However, we expect bugs to be at a minimum and documentation to be true and accurate. +#### From there, we created a library that: +- **Isolates your views**: You can design your views so that they are unaware of the view that will come next. +- **Easily reorders views**: Changing view order is as easy as ⌘+⌥+\[ (moving the line up or down) +- **Composes workflows together**: Create branching flows easily by joining workflows together. +- **Creates conditional flows**: Make your flows robust and handle ever-changing designs. Need a screen only to show up sometimes? Need a flow for person A and another for person B? We've got you covered. # Quick Start - -This quick start uses SPM, but for other approaches, [see our installation instructions](https://wwt.github.io/SwiftCurrent/installation.html). - -## UIKit - -```swift -.package(url: "https://github.com/wwt/SwiftCurrent.git", .upToNextMajor(from: "4.0.0")), -... -.product(name: "SwiftCurrent", package: "SwiftCurrent"), -.product(name: "SwiftCurrent_UIKit", package: "SwiftCurrent") -``` -Then make your first FlowRepresentable view controllers: -```swift -import SwiftCurrent -import SwiftCurrent_UIKit -class OptionalViewController: UIWorkflowItem, FlowRepresentable { - let input: String - required init(with args: String) { - input = args - super.init(nibName: nil, bundle: nil) - } - required init?(coder: NSCoder) { nil } - override func viewDidLoad() { view.backgroundColor = .blue } - func shouldLoad() -> Bool { input.isEmpty } -} -class ExampleViewController: UIWorkflowItem, FlowRepresentable { - override func viewDidLoad() { view.backgroundColor = .green } -} -``` -Then from your root view controller, call: -```swift -import SwiftCurrent -... -let workflow = Workflow(OptionalViewController.self) - .thenProceed(with: ExampleViewController.self) -launchInto(workflow, args: "Skip optional screen") -``` - -And just like that you're started! - -## [BETA] SwiftUI +Why show a quick start when we have an example app? Because it's so easy to get started, we can just drop in 2 code snippets and you're ready to go! This quick start uses Swift Package Manager and SwiftUI, but for other approaches, [see our installation instructions](https://wwt.github.io/SwiftCurrent/installation.html). ```swift .package(url: "https://github.com/wwt/SwiftCurrent.git", .upToNextMajor(from: "4.1.0")), @@ -98,6 +42,7 @@ And just like that you're started! Then make your first FlowRepresentable view: ```swift import SwiftCurrent +import SwiftUI struct OptionalView: View, FlowRepresentable { weak var _workflowPointer: AnyFlowRepresentable? let input: String @@ -105,36 +50,37 @@ struct OptionalView: View, FlowRepresentable { var body: some View { Text("Only shows up if no input") } func shouldLoad() -> Bool { input.isEmpty } } -struct ExampleView: View, FlowRepresentable { +struct ExampleView: View, PassthroughFlowRepresentable { weak var _workflowPointer: AnyFlowRepresentable? var body: some View { Text("This is ExampleView!") } } ``` -Then from your ContentView body, add: +Then from your `ContentView` or whatever view (or app) you'd like to contain the workflow, add the following view to the body: ```swift import SwiftCurrent_SwiftUI -... -WorkflowLauncher(isLaunched: .constant(true), startingArgs: "Skip optional screen") { - thenProceed(with: OptionalView.self) { - thenProceed(with: ExampleView.self) +// ... +var body: some View { + // ... other view code (if any) + WorkflowLauncher(isLaunched: .constant(true), startingArgs: "Skip optional screen") { + thenProceed(with: OptionalView.self) { + thenProceed(with: ExampleView.self) + } } } ``` -And just like that you're started! - -Here's an example of an app written with SwiftCurrent running wherever SwiftUI runs. Check it out: - -https://user-images.githubusercontent.com/79471462/131555558-9e01e753-cafd-4ae2-af1b-b71a1aabb71f.mp4 - +And just like that you've got a workflow! You can now add more items to it, or re-order the items that are there. To understand more of how this works and what is there [check out our developer docs](https://wwt.github.io/SwiftCurrent/How%20to%20use%20SwiftCurrent%20with%20SwiftUI.html) -# Deep Dive +# Check out our example apps +We have [example apps](https://github.com/wwt/SwiftCurrent/tree/main/ExampleApps) for both SwiftUI and UIKit that show SwiftCurrent in action. They're even tested so you can see what it's like to test SwiftCurrent code. To run it locally, start by cloning the repo, open `SwiftCurrent.xcworkspace` and then run the `SwiftUIExample` scheme or the `UIKitExample` scheme. +# [See this to learn more](https://wwt.github.io/SwiftCurrent/index.html) +For specific documentation check out: - [Why SwiftCurrent?](https://wwt.github.io/SwiftCurrent/why-this-library.html) - [Installation](https://wwt.github.io/SwiftCurrent/installation.html) +- [Getting Started with SwiftUI](https://wwt.github.io/SwiftCurrent/getting-started-with-swiftui.html) - [Getting Started with Storyboards](https://wwt.github.io/SwiftCurrent/using-storyboards.html) - [Getting Started with Programmatic UIKit Views](https://wwt.github.io/SwiftCurrent/using-programmatic-views.html) -- [[BETA] Getting Started with SwiftUI](https://wwt.github.io/SwiftCurrent/getting-started-with-swiftui.html) - [Developer Documentation](https://wwt.github.io/SwiftCurrent/index.html) - [Upgrade Path](https://github.com/wwt/SwiftCurrent/blob/main/.github/UPGRADE_PATH.md) - [Contributing to SwiftCurrent](https://github.com/wwt/SwiftCurrent/blob/main/.github/CONTRIBUTING.md)