Skip to content

Repository files navigation

StoryFlow Logo

StoryFlow

LicenseSwift Package Manager compatibleCarthage compatiblecodecov

Functional view controllers automatic flow coordination ✨
⚡️ Lightning talk crash course from App Builders
💭 Idea presentation from UIKonf

TaskWith StoryFlow 😎Without StoryFlow 😱
Create,
Inject,
Show
typealiasOutputType=Stringfunc doTask(){self.produce("Input")}
func doTask(){letnextVc=NextVc()
nextVc.input ="Input"self.show(nextVc, sender:nil)}
😎 completely isolated from other vcs.
🤓 gained type-safeproduce 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
typealiasOutputType=Stringfunc doTask(){self.produce("Update")}
func doTask(){letprevVc=self.presenting as!PrevVc
prevVc.handle("Update")self.dismiss(animated:true)}
😎 completely isolated from other vcs.
🤓 gained type-safeproduce 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
typealiasOutputType=Intfunc doTask(){self.produce(42)}
func doTask(){letnav=self.presenting as!NavCletprevVc= nav.vcs[2]as!PrevVc
prevVc.handle(42)self.dismiss(animated:true)
nav.popTo(preVc, animated:false)}
😎😱😳😭🥵🤬🤯

Usage

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.

InputRequiring

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.

protocolInputRequiring{associatedtypeInputType}extensionInputRequiring{varinput:InputType{return} // Returns 'output' produced by previous vc
}
🔎 see samples
classMyViewController:UIViewController,InputRequiring{typealiasInputType=MyTypeoverridefunc viewDidLoad(){
super.viewDidLoad()
// StoryFlow provides 'input' that was produced as an 'output' by previous vc
title = input.description
}}
classJustViewController: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.
structInputType{}}

Also there's a convenience initializer designed to make InputRequiring vcs easy.

extensionInputRequiring{init(input:InputType){}}
// Example
letmyType=MyType()letmyVc=MyViewController(input: myType)
myVc.input // myType

OutputProducing

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.

protocolOuputProducing{associatedtypeOutputType}extensionOuputProducing{func produce(_ output:OutputType){} // Opens vc with matching `UpdateType` or `InputType`
}typealiasIO=InputRequiring&OutputProducing // For convenience
🔎 see samples
classMyViewController:UIViewController,OutputProducing{typealiasOutputType=MyType@IBActionfunc 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.

extensionOutputProducing{init(produce:@escaping(OutputType)->()){}}
// Example
letmyType= MyType
letmyVc=MyViewController(produce:{ output in
output == myType // true
})
myVc.produce(myType)

UpdateHandling

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.

protocolUpdateHandling{associatedtypeUpdateTypefunc handle(update:UpdateType) // Gets called with 'output' of dismissed vc
}typealiasIOU=InputRequiring&OutputProducing&UpdateHandling // For convenience
🔎 see samples
classUpdatableViewController: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.

Multiple types

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.

enumOneOf2<T1, T2>{case t1(T1), t2(T2)}
🔎 see samples
classZooViewController:UIViewController,IOU{
// 'OneOfN' with 'InputRequiring'
typealiasInputType=OneOf2<Jungle,City>overridefunc 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'
typealiasOutputType=OneOf8<Tiger,Lion,Panda,Koala,Fox,Dog,Cat,OneOf2<Pig,Cow>>@IBActionfunc openRandomGate(){
// There are a few ways 'produce' can be called with 'OneOfN' type
switchInt.random(in:1...9){case1:produce(.t1(🐯)) // Use 't1' enum case to wrap 'Tiger' type
case2:produce(.value(🦁)) // Use convenience 'value' to wrap 'Lion' to 't2' case
case3:produce(🐼) // Use directly with 'Panda' type
case4:produce(🐨)case5:produce(🦊)case6:produce(🐶)case7:produce(🐱)case8:produce(.t8(.t1(🐷))) // Use 't8' and 't1' enum cases to double wrap it
case9:produce(.value(🐮)) // Use 'value' to wrap it only once
}}
// 'OneOfN' with 'UpdateHandling'
typealiasUpdateType=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)"}}}

CustomTransition

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.

extensionCustomTransition{structContext{letfrom,to:UIViewControllerletoutput:Any,outputType:Any.TypeletisUnwind:Bool}typealiasAttempt=(Context)->Bool
// All registered transitions will be tried before fallbacking to default behavior
staticfunc register(attempt:@escapingAttempt){}}

OutputTransform

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.

extensionOutputTransform{
// All relevant registered transforms will be applied before destination vc lookup
staticfunc register<From, To>(transform:@escaping(From)->To){}}

Installation

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"

About

Isolated view controllers inferred navigation flows ✨

Topics

Resources

Stars

24 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages