Skip to content

Dynamic and type-safe framework for building linear and non-linear flows.

License

Notifications You must be signed in to change notification settings

n26/flowkit-ios

Repository files navigation

FlowKit

Swift Platforms CocoaPods Swift Package Manager Coverage

FlowKit is a dynamic flow framework capable of building a flow, based on conditions and ordered according to a logic of next steps. By using FlowKit it's possible to create flows for specific users or cases inside your app. Along with FlowKit there is FlowKitAdditions, a basic implementation that would allow you to use FlowKit directly without coding.

Features

Dynamic flows

An application that uses FlowKit is able to receive the Flow remotely in the form of JSON from a Back-end layer, which will contain all the information necessary to generate all possible specific flows and send it to the application. By having all the conditions and the possible flows generation in one single place, it will be easier to test them. Receiving the flow remotely is only an option: it is possible to save the Flow JSON locally in the device and load it as well, or create it programmatically in the code.

Type safe

The compiler will advise you in case that you mix the things. The compiler will be your best friend.

Easy to extend

The Step could be easily extended, allowing a custom step base object, meanwhile the required properties were provided.

Non-linear flows

FlowKit allows you to create non-linear flows by specifying multiple nextSteps or using DisplayConditions in a step.

Flow output typed

Define a flow output that FlowKit will use to generate a typed output with autocompletion support while you are developing.

Steps

Create your flow steps with dynamic content and control step display and output.

How to use it

We have added an Example project to demostrate how to use FlowKit in your project.

1. Define your flow

Define your flow using FlowDefinition

struct YourFlowDefinition: FlowDefinition {
    typealias OUTPUT = YourFlowOutput
    typealias STEP = YourStepImplementation
}

where OUTPUT is your flow output type and STEP is your step implementation (You could use the one provided in FlowKitAdditions)

2. Create your steps

A StepHandler is in charge of building the step, notify with the completion that the step is completed and flow should show next step If your step is a screen, you should place the logic to present it there.

First you have to define your step, so create a struct that conforms to StepHandlerDefinition. It will force you to define the content, the step output and the flow output.

struct YourStepHandlerDefinition: StepHandlerDefinition {
    typealias CONTENT = YourContent
    typealias STEP_OUTPUT = YourStepOutput
    typealias FLOW_OUTPUT = YourFlowOutputDefinition
    typealias STEP = YourStepImplementation
    static let registerOutputKeyPath: KeyPath<FLOW_OUTPUT, STEP_OUTPUT>?
}
  • The FLOW_OUTPUT and STEP types must be the same as the once defined in YourFlowDefinition.
  • The STEP_OUTPUT is your step output type. If your step doesn't have an output, use the StepOutputEmpty.
  • The CONTENT is the dynamic content of the step, where you should place all the specific data that your step needs to be performed. For example, the image URL or text content we want to display in our step.

If your step has defined an output that you would like to set in YourFlowOutputDefinition you have to specify it.

static let registerOutputKeyPath: KeyPath<FLOW_OUTPUT, STEP_OUTPUT>? = \YourFlowOutputDefinition.yourProperty

Then you can create your step handler with the following sugar syntax:

  • Your step has an output
StepHandler<YourStepHandlerDefinition>.create { _, _, _, _, _ in }
  • Your step doesn't have an output
StepHandler<YourStepHandlerDefinition>.createWithEmptyOutput { _, _, _, _, _ in }

It would allow you to complete the step with a completion without type, completion()

3. Implement your step factory

The step factory is in charge of creating the concrete step handler for each step. The OUTPUT and STEP have to be the same that type in the Flow definition.

struct YourFactory: StepFactory {
    typealias OUTPUT = YourFlowOutput
    typealias STEP = YourStepInfo

    func makeHandler(for stepInfo: StepInfo) -> AnyStepHandler<OUTPUT, STEP>? {
        //Your steps
        if stepInfo.type == "your step type" {
            return AnyStepHandler(createYourStepHandler())
        }
        
        return nil
    }
    
    func createYourStepHandler() -> StepHandler<YourStepHandlerDefinition> {
        .create { stepInfo, content, navigation, output, completion in
            // Your concrete step code
        }
    }
}

4. Create your flow

To start creating a flow, we need the FlowData. Retrieve the FlowData from your Backend or create it locally in your app.

let flowData = FlowData<YourStep>(id: "idFlow", initialStepId: "stepId1", stepsInfo: [YourStepInfo])

4.1 UIKit

Once we have our StepFactory and FlowData we can create our flow:

let flow = FlowKit<YourFlowDefinition>(flowData: flowData, featureStepFactory: YourFactory())

Wherever you want to start the flow, you have to call the method start and inject a UINavigationController

flow.start(
    on: navigationController,
    onFinish: { flowOutput in
        //You have the output of the whole flow.
        //Add here you stuff.
    }
)

4.2 SwiftUI

To present a Flow in SwiftUI we create our FlowKitSwiftUIViewModel using StepFactory and FlowData

let flowViewModel = FlowKitSwiftUIViewModel<YourFlowDefinition>(
    flowData: flowData, 
    featureStepFactory: YourFactory()
    onFinish: { flowOutput, navigation in
        //You have the output of the whole flow.
        //Add here you stuff.
    }
)

To present the flow modally on SwiftUI we use the FlowKitView

.sheet(isPresented: $presentFlow) {
    FlowKitSwiftUIView<YourFlowDefinition>(viewModel: flowViewModel)
}

5. How to use the flow output typed

To use the typed output, you have to provide a struct that conforms to FlowOutputDefinition.

struct ConcreteFlowOutput: FlowOutputDefinition {
    let name: String
    let birthday: Date
    let address: Address
}

Then you will be able to use it in your steps and in your flow output with sugar syntax

StepHandler<YourStepHandlerDefinition>.create { _, _, controller, output, closure in
    print(output.name)
    print(output.address?.city)
    //Add your stuff here
}

// Flow output
flow.start(
    on: navigationController,
    onFinish: { flowOutput in
        //You have the output of the whole flow.
        flowOutput.name
        //Add here you stuff.
    }
)

But before you could use it with value, a previous step should set the value

//Define your step
struct YourStepHandlerDefinition: StepHandlerDefinition {
    typealias CONTENT = YourContent
    typealias STEP_OUTPUT = YourStepOutput
    typealias FLOW_OUTPUT = YourFlowOutputDefinition
    typealias STEP = YourStep
    static let registerOutputKeyPath: KeyPath<FLOW_OUTPUT, STEP_OUTPUT>? = \YourFlowOutputDefinition.yourProperty
}

StepHandler<YourStepHandlerDefinition>.create { _, _, _, _, closure in
    closure(Value)
}

You could always access the previous step or flow output through a dictionary, but remember that you have to know the stepId to unwrap and cast the value.

// StepHandler
StepHandler<YourStepHandlerDefinition>.create { _, _, controller, output, closure in
    output.rawData["stepId"] as? String
    output.rawData["stepId2"] as? Address
    //Add your stuff here
}

// Flow output
flow.start(
    on: navigationController,
    onFinish: { flowOutput in
        //You have the output of the whole flow.
        flowOutput.rawData["stepId"] as? String
        //Add here you stuff.
    }
)

How to install

Add flowkit-ios to your Podfile:

pod 'flowkit-ios'

Add the flowkit-ios dependency in your project Package.swift file.

dependencies: [
    .package(url: "https://github.com/n26/flowkit-ios.git"),
],
targets: [
    .target(
        name: "YOUR_MODULE",
        dependencies: [
            "FlowKit"
        ])
],

License

Copyright (c) 2023 N26 AG, licensed under the MIT license.

Contributing

Contributions are welcome and appreciated. Check CONTRIBUTING for information on how to contribute.