Skip to content

Isolated view controllers inferred navigation flows โœจ

License

Notifications You must be signed in to change notification settings

trafi/StoryFlow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

88 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

StoryFlow Logo

StoryFlow

License Swift Package Manager compatible Carthage compatible codecov

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)
}
๐Ÿ˜Ž ๐Ÿ˜ฑ๐Ÿ˜ณ๐Ÿ˜ญ๐Ÿฅต๐Ÿคฌ๐Ÿคฏ

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.

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

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.

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)

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.

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.

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.

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)"
	}
    }
}

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.

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) { โœจ }
}

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.

extension OutputTransform {
    // All relevant registered transforms will be applied before destination vc lookup
    static func 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"