Skip to content

lumiasaki/SceneBox

Repository files navigation

SceneBox

Motivation

There is a very common scenario when we build an app, that is, using a series of uninterrupted processes to complete a business operation, for example, many apps have a user login/registration process, will display a series of login and registration channels on the first page for the user to choose, the second page may ask the user to fill in some information, when jumping to the third page may require the user to fill in some other information further, and the last page will aggregate all the information that has been filled in before, then make a network request to get the final result. If we use the traditional approach, one of the challenges here is that the developer needs to pass the data between pages one by one, which results the data that is clearly not the concern of current page, but is seen in its public interface declaration as requiring an unrelated data to be passed in the previous page. At the same time, all these pages, if without using other decoupling approaches, will be tightly coupled together, making it difficult for developers to easily modify the order of the pages in the process and to add new pages to the whole process.

Based on these two pain points, I conceived the framework to enable us to develop application-specific business scenarios that can be more scalable and efficient.

How to integrate into your project

To integrate using Apple's SPM, add following as a dependency to your Target.

.package(url: "https://github.com/lumiasaki/SceneBox.git", .upToNextMajor(from: "0.4.1"))

How to use

Distribute scene states from product level

struct SceneState: RawRepresentable, Hashable, Equatable {

    var rawValue: Int

    static let home = SceneState(rawValue: NavigationExtension.entry)
    static let detail = SceneState(rawValue: 1)    
}

extension SceneState: CaseIterable {

    /// Help to register all states.
    static var allCases: [SceneState] { [.home, .detail] }
}

Generate configuration

let sceneStates = Set([
    SceneState.home.rawValue
])

let sceneBoxConfiguration = Configuration(sceneStates: sceneStates)

Or generate it by using ConfigurationFile way to put your setting steps into one place.

Initiate SceneBox

let sceneBox = SceneBox(configuration: sceneBoxConfiguration) { scene, sceneBox in
            self.navigationController.pushViewController(scene, animated: false)
        } exit: { _ in }
        

Setup scene state identifier table

box.lazyAdd(sceneState: SceneState.home.rawValue) {
                let viewModel = HomeViewModel()
                let viewController = HomeViewController(viewModel: viewModel)
                viewModel.scene = viewController

                return viewController
            }

Launch the box

try? Executor.shared.execute(box: sceneBox)

Make a view controller as Scene

import Foundation
import UIKit
import SceneBox

class MyViewController: UIViewController, Scene {

    var sceneIdentifier: UUID!
  
    // ...
}

How to access capabilities from SceneBox in Scene

class MyViewController: UIViewController, Scene {

    var sceneIdentifier: UUID!
  
    func saveValue() {
        sbx.putSharedState(by: \MyState.color, sharedState: UIColor.red)
    }
    
    func fetchValue() {
        let color: UIColor? = sbx.getSharedState(by: \MyState.color)
    }
    
    func pushToNext() {
        sbx.transit(to: SceneState.detail.rawValue)
    }
}

You can create any feature to enhance your box by implementing extensions.

Shared state injection wrapper

class MyViewController: UIViewController, Scene {

    var sceneIdentifier: UUID!
  
    @SharedStateInjected(\MyState.color)
    private var color: UIColor?
    
    init() {
        _color.configure(scene: self)
        
        super.init(nibName: nil, bundle: nil)
    }
    
    func getCurrentColor() -> UIColor? {
        return color
    }
}

Basic Concept

SceneBox

A SceneBox represents a complete process, imagine it is a box that contains a series of pages inside, and the pages that are contained inside the box can easily use a series of capabilities provided by the box. Initializing a SceneBox requires providing a Configuration, in which the caller is required to provide the initialization method of the pages and the corresponding unique identifiers of the pages for later decoupling between pages purpose. Besides, SceneBox requires caller to provide two blocks to control the behavior when entering and exiting the box. The behavior of entering the box means that when a box is called with the execute() method, how to display the first page, whether it is pushed or other more custom operations are left to the callers to manage, with strong scalability. The behavior of leaving the box means that when the whole process is completed, the state of the box comes to terminated, the caller should provide an implementation to control the behavior at this point to handle post cleanup stuffs.

In general, after initializing a SceneBox, it can be held manually by the caller and call the execute() method of SceneBox, but using the framework's built-in Executor to start the operation is a much more recommend way, the Executor will automatically manage the life cycle of the SceneBox.

Scene

The Scene represents a page in the SceneBox, which is currently limited in the UIViewController class, and the limitation may be removed in the future. The fact that Scene is a protocol means that using SceneBox does not need to change the inheritance of your existing code, making it relatively easy to transform an existing UIViewController into a class that can be used in SceneBox. Scene provides a number of capabilities that can be used in SceneBox, such as getSharedState(by:), putSharedState(state:keyPath:) and so on.

For more details about shared state extension with key path, check this: #11

Once a UIViewController is marked as conforming to the Scene protocol, you can access a number of capabilities under sbx namespace of your view controller. Even more, you can extend your own capabilities to the Scene under the namespace easily by extend SceneCapabilityWrapper, you can follow the guide to extend it.

By calling these Scene capabilities, you no longer need to pass a piece of data from the first page to the last one, all of what you need to do is giving the data that will be shared to SceneBox, and if the downstream pages need the corresponding values, retrieve them from the SceneBox by a negotiated key. Also, since it is no longer necessary to explicitly push to the next page, but to call the transit(to:) method provided by Scene instead, the strong coupling between these pages is also lifted.

Extensions

All the capabilities in Scene are driven by SceneBox fundamental outlets, and SceneBox does not actually provide these capabilities directly, SceneBox only provides a series of basic interfaces to the extensions, and the use of these fundamental functions to form more complex and advanced functions is Extension's task. This is designed very much like Microsoft's Visual Studio Code, or Chrome's Chrome Extension. In fact, all the basic and additional capabilities of SceneBox are implemented by Extensions ( such as navigation or shared state capabilities provided by built-in extensions ), and you can even build a complete set of Redux or Data Binding on top of SceneBox if you want.

There is always a need for interaction between Extensions, and to reduce coupling between Extensions and to emphasize the independence of each one, Extensions interact with each other through an internal message bus, where each Extension can declare what messages it listens to and what messages it can respond to, without paying attention to where the messages come from or whether there is a recipient for the messages it sends.