Swiftpack.co -  trafi/StoryFlow as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
Isolated view controllers inferred navigation flows :sparkles:
.package(url: "https://github.com/trafi/StoryFlow.git", from: "1.5.5")

StoryFlow Logo


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 ๐Ÿ˜ฑ
typealias OutputType = String
func doTask() {
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.

typealias OutputType = String
func doTask() {
func doTask() {
    let prevVc = self.presenting as! PrevVc
    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.

typealias OutputType = Int
func doTask() {
func doTask() {
    let nav = self.presenting as! NavC
    let prevVc = nav.vcs[2] as! PrevVc
    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() {
        // 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`

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


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


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.


Add the following line to your Cartfile:

github "Trafi/StoryFlow"


Stars: 24
Last commit: 29 weeks ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jackโ€™d and SCRUFF. We are two of the worldโ€™s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

Nested OneOfN UpdateHandling types
29 weeks ago

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