Using the iOS Renderer

iOS Project Requirements

The minimum requirements are:

  • iOS 15
  • Swift 5.7

Installing the library

The iOS Renderer is available through Cocoapods and Swift Package Manager.

Swift Package Manager

In Xcode, click File -> Add Packages..., enter FlowX repo’s URL https://github.com/flowx-ai-external/flowx-ios-renderer. Set the dependency rule to Up To Next Major and add package.

If you are developing a framework and use FlowX as a dependency, add to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/flowx-ai-external/flowx-ios-renderer", .upToNextMajor(from: "3.0.0"))
]

Cocoapods

Prerequisites

  • Cocoapods gem installed

Cocoapods private trunk setup

Add the private trunk repo to your local Cocoapods installation with the command:

pod repo add flowx-specs git@github.com:flowx-ai-external/flowx-ios-specs.git

Adding the dependency

Add the source of the private repository in the Podfile

source 'git@github.com:flowx-ai-external/flowx-ios-specs.git'

Add a post install hook in the Podfile setting BUILD_LIBRARY_FOR_DISTRIBUTION to YES.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
end

Add the pod and then run pod install

pod 'FlowX'

Example

source 'https://github.com/flowx-ai-external/flowx-ios-specs.git'
source 'https://github.com/CocoaPods/Specs.git'

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
end

target 'AppTemplate' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  
  # Pods for AppTemplate
  pod 'FlowXRenderer'
  
  target 'AppTemplateTests' do
    inherit! :search_paths
    # Pods for testing
  end
  
  target 'AppTemplateUITests' do
    # Pods for testing
  end
  
end

Library dependencies

  • Alamofire
  • SDWebImageSwiftUI
  • SDWebImageSVGCoder

Configuring the library

The SDK has 2 configurations, available through shared instances: FXConfig which holds general purpose properties, and FXSessionConfig which holds user session properties.

It is recommended to call the FXConfig configuration method at app launch.
Call the FXSessionConfig configure method after the user logs in and a valid user session is available.

FXConfig

This config is used for general purpose properties.

Properties

NameDescriptionTypeRequirement
baseURLThe base URL used for REST networkingStringMandatory
enginePathThe process engine url path componentStringMandatory
imageBaseURLThe base URL used for static assetsStringMandatory
languageThe language used for retrieving enumerations and substitution tagsStringMandatory. Defaults to “en”
logLevelEnum value indicating the log level logging. Default is noneBoolOptional

Sample

FXConfig.sharedInstance.configure { (config) in
    config.baseURL = myBaseURL
    config.enginePath = "engine"
    config.imageBaseURL = myImageBaseURL
    config.language = "en" 
    config.logLevel = .verbose
}

Changing the current language

The current language can be changed after the initial configuration, by calling the changeLanguage method:

FXConfig.sharedInstance.changeLanguage(language: "en")

FXSessionConfig

This config is used for providing networking or auth session-specific properties.

The library expects either the JWT access token or an Alamofire Session instance managed by the container app. In case a session object is provided, the request adapting should be handled by the container app.

Properties

NameDescriptionType
sessionManagerAlamofire session instance used for REST networkingSession
tokenJWT authentication access tokenString

Sample for access token

    ...
    func configureFlowXSession() {
        FXSessionConfig.sharedInstance.configure { config in
            config.token = myAccessToken
        }
    }   


Sample for session

import Alamofire
    ...
    func configureFlowXSession() {
        FXSessionConfig.sharedInstance.configure { config in
            config.sessionManager = Session(interceptor: MyRequestInterceptor())
        }
    }   


class MyRequestInterceptor: RequestInterceptor {
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Swift.Result<URLRequest, Error>) -> Void) {
        var urlRequest = urlRequest
        urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        completion(.success(urlRequest))
    }    
}

Theming

Make sure the FXSessionConfig configure method was called with a valid session before setting up the theme.

Before starting or resuming a process, the theme setup API should be called. The start or continue process APIs should be called only after the theme setup was completed.

Theme setup

The setup theme is called using the shared instance of FXTheme

public func setupTheme(withUuid uuid: String,
                       localFileUrl: URL? = nil,
                       completion: (() -> Void)?)
  • uuid - the UUID of the theme configured in the FlowX Designer.

  • localFileUrl - optional parameter for providing a fallback theme file, in case the fetch theme request fails.

  • completion - a completion closure called when the theme setup finishes.

In addition to the completion parameter, FXTheme’s shared instance also provides a Combine publisher named themeFetched which sends true if the theme setup was finished.

Sample

FXTheme.sharedInstance.setupTheme(withUuid: myThemeUuid,
                                  localFileUrl: Bundle.main.url(forResource: "theme", withExtension: "json"),
                                  completion: {
    print("theme setup finished")
})
...
    var subscription: AnyCancellable?

    func myMethodForThemeSetupFinished() {
        subscription = FXTheme.sharedInstance.themeFetched.sink { result in
            if result {
                DispatchQueue.main.async {
                    // you can now start/continue a process
                }
            }
        }
    }
}
    
...

Using the library

Public API

The library’s public APIs described in this section are called using the shared instance of FlowX, FlowX.sharedInstance.

Check renderer compatibility

Before using the iOS SDK, it is recommended to check the compatibility between the renderer and the deployed FlowX services.

This can be done by calling the checkRendererVersion which has a completion handler containing a Bool value.

FlowX.sharedInstance.checkRendererVersion { compatible in
    print(compatible)
}

How to start and end FlowX session

After all the configurations are set, you can start a FlowX session by calling the startSession() method.

This is optional, as the session starts lazily when the first process is started.

FlowX.sharedInstance.startSession()

When you want to end a FlowX session, you can call the endSession() method. This also does a complete clean-up of the started processes.

You might want to use this method in a variety of scenarios, for instance when the user logs out.

FlowX.sharedInstance.endSession()

How to start a process

There are 3 methods available for starting a FlowX process. The container app is responsible with presenting the navigation controller or tab controller holding the process navigation.

  1. Start a process which renders inside an instance of UINavigationController or UITabBarController, depending on the BPMN diagram of the process.

The controller to be presented will be provided inside the completion closure parameter of the method.

Use this method when you want the process to be rendered inside a controller themed using the FlowX Theme defined in the FlowX Designer.
public func startProcess(name: String,
                         params: [String: Any]?,
                         isModal: Bool = false,
                         showLoader: Bool = false,
                         completion: ((UIViewController?) -> Void)?)
  • name - the name of the process

  • params - the start parameters, if any

  • isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

  • showLoader - a boolean indicating whether the loader should be displayed when starting the process.

  • completion - a completion closure which passes either an instance of UINavigationController or UITabBarController to be presented.

  1. Start a process which renders inside a provided instance of a UINavigationController.
Use this method when you want the process to be rendered inside a custom instance of UINavigationController.
Optionally you can pass an instance of FXNavigationViewController, which has the appearance set in the FlowX Theme, using the FXNavigationViewControllers class func FXNavigationViewController.navigationController().

If you use this method, make sure that the process does not use a tab controller as root view.

public func startProcess(navigationController: UINavigationController,
                         name: String,
                         params: [String: Any]?,
                         isModal: Bool = false,
                         showLoader: Bool = false)

navigationController - the instance of UINavigationController which will hold the process navigation stack

name - the name of the process

params - the start parameters, if any

isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

showLoader - a boolean indicating whether the loader should be displayed when starting the process.

  1. Start a process which renders inside a provided instance of a UITabBarController.
Use this method when you want the process to be rendered inside a custom instance of UITabBarController.
If you use this method, make sure that the process has a tab controller as root view.
public func startProcess(tabBarController: UITabBarController,
                         name: String,
                         params: [String: Any]?,
                         isModal: Bool = false,
                         showLoader: Bool = false)

tabBarController - the instance of UITabBarController which will hold the process navigation

name - the name of the process

params - the start parameters, if any

isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

showLoader - a boolean indicating whether the loader should be displayed when starting the process.

Sample

FlowX.sharedInstance.startProcess(name: processName,
                                  params: startParams,
                                  isModal: true,
                                  showLoader: true) { processRootViewController in
    if let processRootViewController = processRootViewController {
        processRootViewController.modalPresentationStyle = .overFullScreen
        self.present(processRootViewController, animated: false)
    }
}

or

FlowX.sharedInstance.startProcess(navigationController: processNavigationController,
                                  name: processName,
                                  params: startParams,
                                  isModal: true
                                  showLoader: true)

self.present(processNavigationController, animated: true, completion: nil)

or

FlowX.sharedInstance.startProcess(tabBarController: processTabController,
                                  name: processName,
                                  params: startParams,
                                  isModal: true
                                  showLoader: true)

self.present(processTabController, animated: true, completion: nil)

How to resume an existing process

There are 3 methods available for resuming a FlowX process. The container app is responsible with presenting the navigation controller or tab controller holding the process navigation.

  1. Continue a process which renders inside an instance of UINavigationController or UITabBarController, depending on the BPMN diagram of the process.

The controller to be presented will be provided inside the completion closure parameter of the method.

Use this method when you want the process to be rendered inside a controller themed using the FlowX Theme defined in the FlowX Designer.
public func continueExistingProcess(uuid: String,
                                    isModal: Bool = false,
                                    showLoader: Bool = false,
                                    completion: ((UIViewController?) -> Void)?)
  • name - the name of the process

  • isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

  • showLoader - a boolean indicating whether the loader should be displayed when starting the process.

  • completion - a completion closure which passes either an instance of UINavigationController or UITabBarController to be presented.

  1. Continue a process which renders inside a provided instance of a UINavigationController.
Use this method when you want the process to be rendered inside a custom instance of UINavigationController.
Optionally you can pass an instance of FXNavigationViewController, which has the appearance set in the FlowX Theme, using the FXNavigationViewControllers class func FXNavigationViewController.navigationController().

If you use this method, make sure that the process does not use a tab controller as root view.

public func continueExistingProcess(uuid: String,
                                    name: String,
                                    navigationController: UINavigationController,
                                    isModal: Bool = false) {

uuid - the UUID string of the process

name - the name of the process

navigationController - the instance of UINavigationController which will hold the process navigation stack

isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

  1. Continue a process which renders inside a provided instance of a UITabBarController.
Use this method when you want the process to be rendered inside a custom instance of UITabBarController.
If you use this method, make sure that the process has a tab controller as root view.
public func continueExistingProcess(uuid: String,
                                    name: String,
                                    tabBarController: UITabBarController,
                                    isModal: Bool = false) {

uuid - the UUID string of the process

name - the name of the process

tabBarController - the instance of UITabBarController which will hold the process navigation

isModal - a boolean indicating whether the process navigation is modally displayed. When the process navigation is displayed modally, a close bar button item is displayed on each screen displayed throughout the process navigation.

Sample

FlowX.sharedInstance.continueExistingProcess(uuid: uuid,
                                             name: processName,
                                             isModal: true) { processRootViewController in
    if let processRootViewController = processRootViewController {
        processRootViewController.modalPresentationStyle = .overFullScreen
        self.present(processRootViewController, animated: true)
    }
}

or

FlowX.sharedInstance.continueExistingProcess(uuid: uuid,
                                             name: processName,
                                             navigationController: processNavigationController,
                                             isModal: true)
processNavigationController.modalPresentationStyle = .overFullScreen

self.present(processNavigationController, animated: true, completion: nil)

or

FlowX.sharedInstance.continueExistingProcess(uuid: uuid,
                                             name: processName,
                                             tabBarController: processTabBarController,
                                             isModal: false)
processTabBarController.modalPresentationStyle = .overFullScreen

self.present(processTabBarController, animated: true, completion: nil)

How to end a process

You can manually end a process by calling the stopProcess(name: String) method.

This is useful when you want to explicitly ask the FlowX shared instance to clean up the instance of the process sent as parameter.

For example, it could be used for modally displayed processes that are dismissed by the user, in which case the dismissRequested(forProcess process: String, navigationController: UINavigationController) method of the FXDataSource will be called.

Sample

FlowX.sharedInstance.stopProcess(name: processName)

FXDataSource

The library offers a way of communication with the container app through the FXDataSource protocol.

The data source is a public property of FlowX shared instance.

public weak var dataSource: FXDataSource?

public protocol FXDataSource: AnyObject {
    func controllerFor(componentIdentifier: String) -> FXController?
    
    func viewFor(componentIdentifier: String) -> FXView?
    
    func viewFor(componentIdentifier: String, customComponentViewModel: FXCustomComponentViewModel) -> AnyView?

    func navigationController() -> UINavigationController?

    func errorReceivedForAction(name: String?)
    
    func validate(validatorName: String, value: String) -> Bool
    
    func dismissRequested(forProcess process: String, navigationController: UINavigationController)
    
    func viewForStepperHeader(stepViewModel: StepViewModel) -> AnyView?

}
  • func controllerFor(componentIdentifier: String) -> FXController?

This method is used for providing a custom component using UIKit UIViewController, identified by the componentIdentifier argument.

  • func viewFor(componentIdentifier: String) -> FXView?

This method is used for providing a custom component using UIKit UIView, identified by the componentIdentifier argument.

  • func viewFor(componentIdentifier: String, customComponentViewModel: FXCustomComponentViewModel) -> AnyView?

This method is used for providing a custom component using SwiftUI View, identified by the componentIdentifier argument. A view model is provided as an ObservableObject to be added as @ObservedObject inside the SwiftUI view for component data observation.

  • func navigationController() -> UINavigationController?

This method is used for providing a navigation controller. It can be either a custom UINavigationController class, or just a regular UINavigationController instance themed by the container app.

  • func errorReceivedForAction(name: String?)

This method is called when an error occurs after an action is executed.

  • func validate(validatorName: String, value: String) -> Bool

This method is used for custom validators. It provides the name of the validator and the value to be validated. The method returns a boolean indicating whether the value is valid or not.

  • func dismissRequested(forProcess process: String, navigationController: UINavigationController)

This method is called, on a modally displayed process navigation, when the user attempts to dismiss the modal navigation. Typically it is used when you want to present a confirmation pop-up.

The container app is responsible with dismissing the UI and calling the stop process APIs.

  • func viewForStepperHeader(stepViewModel: StepViewModel) -> AnyView?

This method is used for providing a custom SwiftUI view for the stepper navigation header.

Sample

class MyFXDataSource: FXDataSource {
    func controllerFor(componentIdentifier: String) -> FXController? {
        switch componentIdentifier {
        case "customComponent1":
            let customComponent: CustomViewController = viewController()
            return customComponent
        default:
            return nil
        }
    }
    
    func viewFor(componentIdentifier: String) -> FXView? {
        switch componentIdentifier {
        case "customComponent2":
            return CustomView()
        default:
            return nil
        }
    }
    
    func viewFor(componentIdentifier: String, customComponentViewModel: FXCustomComponentViewModel) -> AnyView? {
        switch componentIdentifier {
        case "customComponent2":
            return AnyView(SUICustomView(viewModel: customComponentViewModel))
        default:
            return nil
        }
    }
    
    func navigationController() -> UINavigationController? {
        nil
    }
    
    func errorReceivedForAction(name: String?) {
        
    }
    
    func validate(validatorName: String, value: Any) -> Bool {
        switch validatorName {
        case "myCustomValidator":
            let myCustomValidator = MyCustomValidator(input: value as? String)
            return myCustomValidator.isValid()
        default:
            return true
        }
    }
    
    func dismissRequested(forProcess process: String, navigationController: UINavigationController) {
        navigationController.dismiss(animated: true, completion: nil)
        FlowX.sharedInstance.stopProcess(name: process)
    }

    func viewForStepperHeader(stepViewModel: StepViewModel) -> AnyView? {
        return AnyView(CustomStepperHeaderView(stepViewModel: stepViewModel))
    }

}

Custom components

FXController

FXController is an open class subclassing UIViewController, which helps the container app provide full custom screens the renderer. It needs to be subclassed for each custom screen.

Use this only when the custom component configured in the UI Designer is the root component of the User Task node.
open class FXController: UIViewController {
    
    internal(set) public var data: Any?
    internal(set) public var actions: [ProcessActionModel]?

    open func titleForScreen() -> String? {
        return nil
    }
    
    open func populateUI() {
        
    }
    
    open func updateUI() {
        
    }
    
}
  • internal(set) public var data: Any?

data is the property, containing the data model for the custom component. The type is Any, as it could be a primitive value, a dictionary or an array, depending on the component configuration.

  • internal(set) public var actions: [ProcessActionModel]?

actions is the array of actions provided to the custom component.

  • func titleForScreen() -> String?

This method is used for setting the screen title. It is called by the renderer when the view controller is displayed.

  • func populateUI()

This method is called by the renderer, after the controller has been presented, when the data is available.

This will happen asynchronously. It is the container app’s responsibility to make sure that the initial state of the view controller does not have default/residual values displayed.

  • func updateUI()

This method is called by the renderer when an already displayed view controller needs to update the data shown.

FXView

FXView is a protocol that helps the container app provide custom UIKit subviews to the renderer. It needs to be implemented by UIView instances. Similar to FXController it has data and actions properties and a populate method.

public protocol FXView: UIView {
    var data: Any? { get set }
    var actions: [ProcessActionModel]? { get set }

    func populateUI()
}
  • var data: [String: Any]?

data is the property, containing the data model for the custom view. The type is Any, as it could be a primitive value, a dictionary or an array, depending on the component configuration.

  • var actions: [ProcessActionModel]?

actions is the array of actions provided to the custom view.

  • func populateUI()

This method is called by the renderer after the screen containing the view has been displayed.

It is the container app’s responsibility to make sure that the initial state of the view does not have default/residual values displayed.

It is mandatory for views implementing the FXView protocol to provide the intrinsic content size.
override var intrinsicContentSize: CGSize {
    return CGSize(width: UIScreen.main.bounds.width, height: 100)
}

SwiftUI Custom components

Custom SwiftUI components can be provided as type-erased views.

FXCustomComponentViewModel is a class implementing the ObservableObject protocol. It is used for managing the state of custom SwiftUI views. It has two published properties, for data and actions.

    @Published public var data: Any?
    @Published public var actions: [ProcessActionModel] = []

Example

struct SampleView: View {
    
    @ObservedObject var viewModel: FXCustomComponentViewModel
            
    var body: some View {
        Text("Lorem")
    }
}

Custom header view for Stepper navigation

The container application can provide a custom view that will be used as the stepper navigation header, using the FXDataSource protocol method viewForStepperHeader. The method has a parameter, which provides the data needed for populating the view’s UI.

public struct StepViewModel {
    // title for the current step; optional
    public var stepTitle: String?
    // title for the current substep, if there is a stepper in stepper configured; optional
    public var substepTitle: String?
    // 1-based index of the current step
    public var step: Int
    // total number of steps
    public var totalSteps: Int
    // 1-based index of the current substep, if there is a stepper in stepper configured; optional
    public var substep: Int?
    // total number of substeps in the current step, if there is a stepper in stepper configured; optional
    public var totalSubsteps: Int?
}

Sample

struct CustomStepperHeaderView: View {
    
    let viewModel: StepViewModel
            
    var body: some View {
        VStack(spacing: 16) {
            ProgressView(value: Float(stepViewModel.step) / Float(stepViewModel.totalSteps))
                .foregroundStyle(Color.blue)
            if let stepTitle = stepViewModel.stepTitle {
                Text(stepTitle)
            }
            if let substepTitle = stepViewModel.substepTitle {
                Text(substepTitle)
            }
        }
        .background(Color.white)
        .shadow(radius: 10)
    }
}

How to run an action from a custom component

The custom components which the container app provides will contain FlowX actions to be executed. In order to run an action you need to call the following method:

public func runAction(action: ProcessActionModel,
                      params: [String: Any]? = nil)

action - the ProcessActionModel action object

params - the parameters for the action

How to run an upload action from a custom component

public func runUploadAction(action: ProcessActionModel,
                            image: UIImage)

action - the ProcessActionModel action object

image - the image to upload

public func runUploadAction(action: ProcessActionModel,
                            fileURL: URL)

action - the ProcessActionModel action object

fileURL - the local URL of the image

Getting a substitution tag value by key

public func getTag(withKey key: String) -> String?

All substitution tags will be retrieved by the SDK before starting the first process and will be stored in memory.

Whenever the container app needs a substitution tag value for populating the UI of the custom components, it can request the substitution tag using the method above, providing the key.

Getting a media item url by key

public func getMediaItemURL(withKey key: String) -> String?

All media items will be retrieved by the SDK before starting the first process and will be stored in memory.

Whenever the container app needs a media item url for populating the UI of the custom components, it can request the url using the method above, providing the key.

Handling authorization token changes

When the access token of the auth session changes, you can update it in the renderer using the func updateAuthorization(token: String) method.

FlowX.sharedInstance.updateAuthorization(token: accessToken)