Swiftpack.co - trafi/StoryFlow as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by trafi.
trafi/StoryFlow 1.6.2
Isolated view controllers inferred navigation flows :sparkles:
⭐️ 24
🕓 10 weeks ago
iOS
.package(url: "https://github.com/trafi/StoryFlow.git", from: "1.6.2")

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

Swift Package Manager

Open your project in Xcode and select File > Swift Packages > Add Package Dependency. There enter https://github.com/trafi/StoryFlow/ as the repository URL.

Carthage

Add the following line to your Cartfile:

github "Trafi/StoryFlow"

GitHub

link
Stars: 24
Last commit: 10 weeks ago
Advertisement: IndiePitcher.com - Cold Email Software for Startups

Release Notes

1.6.2
10 weeks ago

What's Changed

Full Changelog: https://github.com/trafi/StoryFlow/compare/1.6.1...1.6.2

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics