Skip to content

Commit

Permalink
Merge pull request #11 from lumiasaki/feature/keypath_shared_state_ex…
Browse files Browse the repository at this point in the history
…tension

Introduce key path to shared state extension
  • Loading branch information
lumiasaki authored Jul 27, 2021
2 parents 8529655 + b6e6143 commit f30662f
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 20 deletions.
92 changes: 92 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/SceneBox.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SceneBox"
BuildableName = "SceneBox"
BlueprintName = "SceneBox"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SceneBoxTests"
BuildableName = "SceneBoxTests"
BlueprintName = "SceneBoxTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SceneBoxTests"
BuildableName = "SceneBoxTests"
BlueprintName = "SceneBoxTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SceneBox"
BuildableName = "SceneBox"
BlueprintName = "SceneBox"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Based on these two pain points, I conceived the framework to enable us to develo

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.2.4"))`
`.package(url: "https://github.com/lumiasaki/SceneBox.git", .upToNextMajor(from: "0.3.0"))`

## How to use

Expand Down Expand Up @@ -107,11 +107,11 @@ class MyViewController: UIViewController, Scene {
var sceneIdentifier: UUID!

func saveValue() {
sbx.putSharedState(by: "Color", sharedState: UIColor.red)
sbx.putSharedState(by: \.color, sharedState: UIColor.red)
}

func fetchValue() {
let color: UIColor? = sbx.getSharedState(by: "Color")
let color: UIColor? = sbx.getSharedState(by: \.color)
}

func pushToNext() {
Expand All @@ -131,7 +131,7 @@ class MyViewController: UIViewController, Scene {

var sceneIdentifier: UUID!

@SceneBoxSharedStateInjected(key: "Color")
@SharedStateInjected(\.timestamp)
private var color: UIColor?

init() {
Expand All @@ -157,7 +157,9 @@ In general, after initializing a `SceneBox`, it can be held manually by the call

### 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:key:)` and so on.
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: https://github.com/lumiasaki/SceneBox/issues/10
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.

Expand Down
155 changes: 142 additions & 13 deletions Sources/SceneBox/Extension/Core/SharedStateExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation

@available(*, deprecated, message: "Use `SharedStateInjected<T>` instead.")
@propertyWrapper
public class SceneBoxSharedStateInjected<T> {

Expand Down Expand Up @@ -41,6 +42,80 @@ public class SceneBoxSharedStateInjected<T> {
}
}

/// Property wrapper for helping developers to simplify state sharing among different `Scene`s, the principle of it is similar to `Environment` in SwiftUI, all `Scene`s share same source of truth through the `SharedStateExtension`, use this property wrapper will make developer to manipulate variables as normal properties, the only difference is that this one is backed by `SharedStateExtension` in static-type ( key path ) way.
/// Notice: Because the property wrapper needs an instance of `Scene` to get capability from it, since that, you need to configure it before using it by calling `configure(scene:)` in a proper place.
@propertyWrapper
public struct SharedStateInjected<T> {

private var scene: Scene?

private let keyPath: WritableKeyPath<SharedStateValues, T?>

public mutating func configure(scene: Scene?) {
self.scene = scene
}

public var wrappedValue: T? {
get {
guard let scene = scene else {
fatalError("configure scene firstly")
}

return scene.sbx.getSharedState(by: keyPath)
}

set {
guard let scene = scene else {
fatalError("configure scene firstly")
}

scene.sbx.putSharedState(by: keyPath, sharedState: newValue)
}
}

public init(_ keyPath: WritableKeyPath<SharedStateValues, T?>) {
self.keyPath = keyPath
}
}

/// This is an entry point for your custom keys.
/// A case about how to create a custom key for your logic:
/// ```swift
/// struct TimestampKey: SharedStateKey {
///
/// // this will give compiler a hint about concrete type.
/// static var currentValue: TimeInterval?
/// }
/// ```
public protocol SharedStateKey {

associatedtype ValueType

static var currentValue: ValueType? { get set }
}

/// This is an entry point for extending your values.
/// A case about how to create a key path for your value as below:
/// ```swift
/// extension SharedStateValues {
///
/// // this will generate a key path for `timestamp` variable.
/// var timestamp: TimeInterval? {
/// get { Self[TimestampKey.self] }
/// set { Self[TimestampKey.self] = newValue }
/// }
/// }
/// ```
public struct SharedStateValues {

public static var current = SharedStateValues()

public static subscript<K: SharedStateKey>(key: K.Type) -> K.ValueType? {
get { key.currentValue }
set { key.currentValue = newValue }
}
}

/// Message data structure for shared state extension to exchange information with other extensions in the SceneBox.
public struct SharedStateMessage {

Expand Down Expand Up @@ -77,34 +152,38 @@ public final class SharedStateExtension: Extension {
// MARK: - Private

private let workerQueue = DispatchQueue(label: "com.scenebox.shared-state.queue", attributes: .concurrent)

@available(*, deprecated, message: "Use `stateValue` instead.")
private var state: [AnyHashable : Any] = Dictionary()

fileprivate func querySharedState(by key: AnyHashable) -> Any? {
var result: Any?

workerQueue.sync {
result = state[key]
}

return result
}
private var stateValue: SharedStateValues = SharedStateValues()

fileprivate func setSharedState(_ sharedState: Any?, on key: AnyHashable) {
fileprivate func setSharedState<T>(_ sharedState: T?, on keyPath: WritableKeyPath<SharedStateValues, T?>) {
workerQueue.async(flags: .barrier) {
self.state[key] = sharedState
self.stateValue[keyPath: keyPath] = sharedState

self.sceneBox?.dispatch(event: EventBus.EventName.sharedStateChanges, message: SharedStateMessage(key: key, state: sharedState))
self.logger(content: "shared state changes: key: \(key), state: \(String(describing: sharedState))")
self.sceneBox?.dispatch(event: EventBus.EventName.sharedStateChanges, message: SharedStateMessage(key: keyPath, state: sharedState))
self.logger(content: "shared state changes: key: \(keyPath), state: \(String(describing: sharedState))")
}
}

fileprivate func querySharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>) -> T? {
var result: T?

workerQueue.sync {
result = stateValue[keyPath: keyPath]
}

return result
}
}

extension SceneCapabilityWrapper {

/// Receiving a shared state from the shared store if it exists.
/// - Parameter key: The key of the data you want to fetch from the store.
/// - Returns: The shared state stored in the extension. Nil if the data not exists with the provided key.
@available(*, deprecated, message: "Use `getSharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>) -> T?` instead.")
public func getSharedState(by key: AnyHashable) -> Any? {
guard let ext = try? _getExtension(by: SharedStateExtension.self) else {
return nil
Expand All @@ -113,15 +192,65 @@ extension SceneCapabilityWrapper {
return ext.querySharedState(by: key)
}

/// Receiving a shared state from the shared store if it exists.
/// - Parameter keyPath: Key path to the value by extending `SharedStateValues`.
/// - Returns: The shared state stored in the extension. Nil if the data not exists with the provided key path.
public func getSharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>) -> T? {
guard let ext = try? _getExtension(by: SharedStateExtension.self) else {
return nil
}

return ext.querySharedState(by: keyPath)
}

/// Putting a shared state to the shared store.
/// - Parameters:
/// - key: The key of the data you want to put into the store.
/// - sharedState: The shared state you want to save. Nil if you want to remove the data from the store.
@available(*, deprecated, message: "Use `putSharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>, sharedState: T?)` instead.")
public func putSharedState(by key: AnyHashable, sharedState: Any?) {
guard let ext = try? _getExtension(by: SharedStateExtension.self) else {
return
}

ext.setSharedState(sharedState, on: key)
}

/// Putting a shared state to the shared store.
/// - Parameters:
/// - keyPath: Key path to the value by extending `SharedStateValues`.
/// - sharedState: The shared state you want to save. Nil if you want to remove the data from the store.
public func putSharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>, sharedState: T?) {
guard let ext = try? _getExtension(by: SharedStateExtension.self) else {
return
}

ext.setSharedState(sharedState, on: keyPath)
}
}

// MARK: - Deprecated

extension SharedStateExtension {

@available(*, deprecated, message: "Use `querySharedState<T>(by keyPath: WritableKeyPath<SharedStateValues, T?>) -> T?` instead.")
fileprivate func querySharedState(by key: AnyHashable) -> Any? {
var result: Any?

workerQueue.sync {
result = state[key]
}

return result
}

@available(*, deprecated, message: "Use `setSharedState<T>(_ sharedState: T?, on keyPath: WritableKeyPath<SharedStateValues, T?>)` instead.")
fileprivate func setSharedState(_ sharedState: Any?, on key: AnyHashable) {
workerQueue.async(flags: .barrier) {
self.state[key] = sharedState

self.sceneBox?.dispatch(event: EventBus.EventName.sharedStateChanges, message: SharedStateMessage(key: key, state: sharedState))
self.logger(content: "shared state changes: key: \(key), state: \(String(describing: sharedState))")
}
}
}
Loading

0 comments on commit f30662f

Please sign in to comment.