Functional view controllers automatic flow coordination โจ
โก๏ธ Lightning talk crash course from App Builders
๐ญ Idea presentation from UIKonf
Task | With StoryFlow ๐ | Without StoryFlow ๐ฑ |
---|---|---|
Create, Inject, Show |
typealias OutputType = String
ย
func doTask() {
self.produce("Input")
} |
func doTask() {
let nextVc = NextVc()
nextVc.input = "Input"
self.show(nextVc, sender: nil)
} |
๐ completely isolated from other vcs. ๐ค gained type-safe produce func.๐ automatic injection of produced value. ๐ navigation customizable out of vc. ๐ฅณ easy to test with mocked produce . |
๐ณ knows the type of next vc. ๐ก knows the property of next vc to inject. ๐ข knows how to navigate to next vc. ๐คฏ easy to break, hard to test. |
|
Update, Unwind |
typealias OutputType = String
ย
func doTask() {
self.produce("Update")
} |
func doTask() {
let prevVc = self.presenting as! PrevVc
prevVc.handle("Update")
self.dismiss(animated: true)
} |
๐ completely isolated from other vcs. ๐ค gained type-safe produce func.๐ automatic update with produced value. ๐ navigation customizable out of vc. ๐ฅณ easy to test with mocked produce . |
๐คฌ knows the place in nav stack of prev vc. ๐ณ knows the type of prev vc. ๐ฅต knows the method of prev vc for update. ๐ญ knows how to unwind to prev vc. ๐คฏ easy to break, hard to test. |
|
Update, Difficult unwind |
typealias OutputType = Int
ย
func doTask() {
self.produce(42)
} |
func doTask() {
let nav = self.presenting as! NavC
let prevVc = nav.vcs[2] as! PrevVc
ย
prevVc.handle(42)
ย
self.dismiss(animated: true)
nav.popTo(preVc, animated: false)
} |
๐ | ๐ฑ๐ณ๐ญ๐ฅต๐คฌ๐คฏ |
StoryFlow isolates your view controllers from each other and connects them in a navigation flow using three simple generic protocols - InputRequiring
, OutputProducing
and UpdateHandling
. You can customize navigation transition styles using CustomTransition
and routing using OutputTransform
.
StoryFlow contains InputRequiring
protocol. What vc gets created, injected and shown after producing an output is determined by finding the exact type match to InputType
.
This protocol has an extension that gives vc access to the produced output as its input. It is injected right after the init.
protocol InputRequiring {
associatedtype InputType
}
extension InputRequiring {
var input: InputType { return โจ } // Returns 'output' produced by previous vc
}
๐ see samples
class MyViewController: UIViewController, InputRequiring {
typealias InputType = MyType
override func viewDidLoad() {
super.viewDidLoad()
// StoryFlow provides 'input' that was produced as an 'output' by previous vc
title = input.description
}
}
class JustViewController: UIViewController, InputRequiring {
// When vc doesn't require any input it should still declare it's 'InputType'.
// Otherwise it's impossible for this vc to be opened using StoryFlow.
struct InputType {}
}
Also there's a convenience initializer designed to make InputRequiring
vcs easy.
extension InputRequiring {
init(input: InputType) { โจ }
}
// Example
let myType = MyType()
let myVc = MyViewController(input: myType)
myVc.input // myType
StoryFlow contains OutputProducing
protocol. Conforming to it allows vcs to navigate to other vcs that are either in the nav stack and have the exact UpdateType
type or that have the exact InputType
and will be initialized.
protocol OuputProducing {
associatedtype OutputType
}
extension OuputProducing {
func produce(_ output: OutputType) { โจ } // Opens vc with matching `UpdateType` or `InputType`
}
typealias IO = InputRequiring & OutputProducing // For convenience
๐ see samples
class MyViewController: UIViewController, OutputProducing {
typealias OutputType = MyType
@IBAction func goToNextVc() {
// StoryFlow will go back to a vc in the nav stack with `UpdateType = MyType`
// Or it will create, inject and show a new vc with `InputType = MyType`
produce(MyType())
}
}
To produce more than one type of output see the section about OneOfN
enum.
Also there's a convenience initializer designed to make OutputProducing
vcs easy.
extension OutputProducing {
init(produce: @escaping (OutputType) -> ()) { โจ }
}
// Example
let myType = MyType
let myVc = MyViewController(produce: { output in
output == myType // true
})
myVc.produce(myType)
StoryFlow contains UpdateHandling
protocol. Conforming to it allows to navigate back to it and passing data. Unwind happens and handle(update:)
gets called when UpdateType
exactly matches the produced output type.
protocol UpdateHandling {
associatedtype UpdateType
func handle(update: UpdateType) // Gets called with 'output' of dismissed vc
}
typealias IOU = InputRequiring & OutputProducing & UpdateHandling // For convenience
๐ see samples
class UpdatableViewController: UIViewController, UpdateHandling {
func handle(update: MyType) {
// Do something โจ
// This gets called when a presented vc produces an output of `OutputType = MyType`
}
}
To handle more than one type of output see the section about OneOfN
enum.
To require, produce and handle more than one type StoryFlow introduces a OneOfN
enum. It's used to define OutputType
, InputType
and UpdateType
typealiases. Enums for up to OneOf8
are defined, but it's possible to nest them as much as needed.
enum OneOf2<T1, T2> {
case t1(T1), t2(T2)
}
๐ see samples
class ZooViewController: UIViewController, IOU {
// 'OneOfN' with 'InputRequiring'
typealias InputType = OneOf2<Jungle, City>
override func viewDidLoad() {
super.viewDidLoad()
// Just use the 't1'...'tN' enum cases
switch input {
case .t1(let jungle):
title = jungle.name
case .t2(let city):
title = city.countryName
}
}
// 'OneOfN' with 'OutputProducing'
typealias OutputType = OneOf8<Tiger, Lion, Panda, Koala, Fox, Dog, Cat, OneOf2<Pig, Cow>>
@IBAction func openRandomGate() {
// There are a few ways 'produce' can be called with 'OneOfN' type
switch Int.random(in: 1...9) {
case 1: produce(.t1(๐ฏ)) // Use 't1' enum case to wrap 'Tiger' type
case 2: produce(.value(๐ฆ)) // Use convenience 'value' to wrap 'Lion' to 't2' case
case 3: produce(๐ผ) // Use directly with 'Panda' type
case 4: produce(๐จ)
case 5: produce(๐ฆ)
case 6: produce(๐ถ)
case 7: produce(๐ฑ)
case 8: produce(.t8(.t1(๐ท))) // Use 't8' and 't1' enum cases to double wrap it
case 9: produce(.value(๐ฎ)) // Use 'value' to wrap it only once
}
}
// 'OneOfN' with 'UpdateHandling'
typealias UpdateType = OneOf3<Day, Night, Holiday>
func handle(update: UpdateType) {
// Just use the 't1'...'tN' enum cases
switch input {
case .t1(let day):
subtitle = "Opened during \(day.openHours)"
case .t2(let night):
subtitle = "Closed for \(night.sleepHours)"
openRandomGate() // ๐
case .t3(let holiday):
subtitle = "Discounts on \(holiday.dates)"
}
}
}
By default StoryFlow will show new vcs using show
method and will unwind using relevant combination of dismiss
and pop
methods. This is customizable using static functions on CustomTransition
.
extension CustomTransition {
struct Context {
let from, to: UIViewController
let output: Any, outputType: Any.Type
let isUnwind: Bool
}
typealias Attempt = (Context) -> Bool
// All registered transitions will be tried before fallbacking to default behavior
static func register(attempt: @escaping Attempt) { โจ }
}
By default OutputType
have to exactly match to InputType
and UpdateType
for destination to be found and for navigation transitions to happen. This can be customized using a static funtion on OutputTransform
. Note, that To
can be a OneOfN
type, allowing for easy AB testing or other navigation splits that are determined outside of vcs.
extension OutputTransform {
// All relevant registered transforms will be applied before destination vc lookup
static func register<From, To>(transform: @escaping (From) -> To) { โจ }
}
Open your project in Xcode and select File > Swift Packages > Add Package Dependency. There enter https://github.com/trafi/StoryFlow/
as the repository URL.
Add the following line to your Cartfile
:
github "Trafi/StoryFlow"