Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

Commit

Permalink
Implement [...]
Browse files Browse the repository at this point in the history
  • Loading branch information
david-swift committed Jul 10, 2024
1 parent 755518a commit 7ea5d96
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 48 deletions.
2 changes: 2 additions & 0 deletions Sources/Model/Data Flow/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,5 @@ extension Binding: CustomStringConvertible where Value: CustomStringConvertible
}

}

extension Binding: Sendable where Value: Sendable { }
5 changes: 4 additions & 1 deletion Sources/Model/Data Flow/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Observation

/// A property wrapper for properties in a view that should be stored throughout view updates.
@propertyWrapper
@StateManager
public struct State<Value>: StateProtocol {

/// Access the stored value. This updates the views when being changed.
Expand Down Expand Up @@ -46,7 +47,9 @@ public struct State<Value>: StateProtocol {
return value
}
nonmutating set {
StateManager.setState(id: id, value: newValue)
Task {
await StateManager.setState(id: id, value: newValue)
}
}
}

Expand Down
28 changes: 23 additions & 5 deletions Sources/Model/Data Flow/StateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@
import Foundation

/// This type manages view updates.
@globalActor
public actor StateManager {

public static let shared = StateManager()

/// Whether to block updates in general.
public static var blockUpdates = false
@StateManager public static var blockUpdates = false
/// Whether to save state.
public static var saveState = true
@StateManager public static var saveState = true
/// The application identifier.
static var appID: String?
@StateManager static var appID: String?
/// The functions handling view updates.
static var updateHandlers: [(Bool) async -> Void] = []
@StateManager static var updateHandlers: [(Bool) async -> Void] = []
/// The state.
static var state: [State] = []
@StateManager static var state: [State] = []

/// Information about a piece of state.
struct State {
Expand Down Expand Up @@ -50,6 +53,7 @@ public actor StateManager {
/// - Parameter force: Whether to force all views to update.
///
/// Nothing happens if ``UpdateManager/blockUpdates`` is true.
@StateManager
public static func updateViews(force: Bool = false) async {
if !blockUpdates {
for handler in updateHandlers {
Expand All @@ -60,6 +64,7 @@ public actor StateManager {

/// Add a handler that is called when the user interface should update.
/// - Parameter handler: The handler. The parameter defines whether the whole UI should be force updated.
@StateManager
public static func addUpdateHandler(handler: @escaping (Bool) async -> Void) {
updateHandlers.append(handler)
}
Expand All @@ -68,6 +73,7 @@ public actor StateManager {
/// - Parameters:
/// - id: The identifier.
/// - value: The new value.
@StateManager
static func setState(id: UUID, value: Any?) {
if saveState {
guard let index = state.firstIndex(where: { $0.contains(id: id) }) else {
Expand All @@ -81,12 +87,14 @@ public actor StateManager {
/// Get the state value for a certain ID.
/// - Parameter id: The identifier.
/// - Returns: The value.
@StateManager
static func getState(id: UUID) -> Any? {
state[safe: state.firstIndex { $0.contains(id: id) }]?.value
}

/// Mark the state of a certain id as updated.
/// - Parameter id: The identifier.
@StateManager
static func updateState(id: UUID) {
if saveState {
state[safe: state.firstIndex { $0.contains(id: id) }]?.update = true
Expand All @@ -95,6 +103,7 @@ public actor StateManager {

/// Mark the state of a certain id as not updated.
/// - Parameter id: The identifier.
@StateManager
static func updatedState(id: UUID) {
if saveState {
state[safe: state.firstIndex { $0.contains(id: id) }]?.update = false
Expand All @@ -104,6 +113,7 @@ public actor StateManager {
/// Get whether to update the state of a certain id.
/// - Parameter id: The identifier.
/// - Returns: Whether to update the state.
@StateManager
static func getUpdateState(id: UUID) -> Bool {
state[safe: state.firstIndex { $0.contains(id: id) }]?.update ?? false
}
Expand All @@ -112,10 +122,18 @@ public actor StateManager {
/// - Parameters:
/// - oldID: The old identifier.
/// - newID: The new identifier.
@StateManager
static func changeID(old oldID: UUID, new newID: UUID) {
if saveState {
state[safe: state.firstIndex { $0.contains(id: oldID) }]?.changeID(new: newID)
}
}

/// Set the app's identifier.
/// - Parameter id: The identifier.
@StateManager
static func setAppID(_ id: String) {
appID = id
}

}
26 changes: 17 additions & 9 deletions Sources/Model/Extensions/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ extension Array: AnyView where Element == AnyView {
public func widget<Storage>(
modifiers: [(AnyView) -> AnyView],
type: Storage.Type
) -> Widget where Storage: AppStorage {
if count == 1, let widget = self[safe: 0]?.widget(modifiers: modifiers, type: type) {
) async -> Widget where Storage: AppStorage {
if count == 1, let widget = await self[safe: 0]?.widget(modifiers: modifiers, type: type) {
return widget
} else {
var modified = self
Expand All @@ -46,7 +46,7 @@ extension Array: AnyView where Element == AnyView {
updateProperties: Bool,
type: Storage.Type
) async where Storage: AppStorage {
for (index, element) in filter({ $0.renderable(type: type, modifiers: modifiers) }).enumerated() {
for (index, element) in await filter({ await $0.renderable(type: type, modifiers: modifiers) }).enumerated() {
if let storage = storages[safe: index] {
await element
.widget(modifiers: modifiers, type: type)
Expand Down Expand Up @@ -124,6 +124,14 @@ extension Array {
try await compactMap { try await transform($0) }
}

public func filter(_ isIncluded: (Self.Element) async throws -> Bool) async rethrows -> Self {
var newSelf = self
for element in self where try await isIncluded(element) {
newSelf.append(element)
}
return newSelf
}

}

extension Array where Element: Identifiable {
Expand Down Expand Up @@ -156,10 +164,10 @@ extension Array where Element == Renderable {
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
) {
for (index, element) in filter({ $0 as? RenderableType != nil }).enumerated() {
) async {
for (index, element) in await filter({ $0 as? RenderableType != nil }).enumerated() {
if let storage = storages[safe: index] {
element
await element
.update(storage, updateProperties: updateProperties, type: type, fields: fields)
}
}
Expand All @@ -173,10 +181,10 @@ extension Array where Element == Renderable {
public func storages<RenderableType>(
type: RenderableType.Type,
fields: [String: Any]
) -> [RenderableStorage] {
compactMap { element in
) async -> [RenderableStorage] {
await compactMap { element in
if element as? RenderableType != nil {
return element.container(type: type, fields: fields)
return await element.container(type: type, fields: fields)
}
return nil
}
Expand Down
51 changes: 27 additions & 24 deletions Sources/Model/User Interface/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Observation
/// }
/// ```
///
@StateManager
public protocol App: Sendable {

/// The app storage typ.
Expand Down Expand Up @@ -59,35 +60,37 @@ extension App {
public static func setupApp() -> Self {
var appInstance = self.init()
appInstance.app = Storage(id: appInstance.id)
StateManager.addUpdateHandler { force in
Task { [appInstance] in
var updateProperties = force
for property in appInstance.getState() {
if let oldID = await appInstance.app.getStorage().getState(key: property.key)?.id {
StateManager.changeID(old: oldID, new: property.value.id)
await appInstance.app.editStorage { await $0.setStateID(key: property.key, id: property.value.id) }
Task { [appInstance] in
await StateManager.addUpdateHandler { force in
Task { [appInstance] in
var updateProperties = force
for property in appInstance.getState() {
if let oldID = await appInstance.app.getStorage().getState(key: property.key)?.id {
await StateManager.changeID(old: oldID, new: property.value.id)
await appInstance.app.editStorage { await $0.setStateID(key: property.key, id: property.value.id) }
}
if await StateManager.getUpdateState(id: property.value.id) {
updateProperties = true
await StateManager.updatedState(id: property.value.id)
}
}
if StateManager.getUpdateState(id: property.value.id) {
updateProperties = true
StateManager.updatedState(id: property.value.id)
var removeIndices: [Int] = []
for (index, element) in await appInstance.app.getStorage().sceneStorage.enumerated() {
if await element.destroy {
removeIndices.insert(index, at: 0)
} else if let scene = await appInstance.scene.first(
where: { await $0.id == element.id }
) as? Storage.SceneElementType as? SceneElement {
await scene.update(element, app: appInstance.app, updateProperties: updateProperties)
}
}
}
var removeIndices: [Int] = []
for (index, element) in await appInstance.app.getStorage().sceneStorage.enumerated() {
if await element.destroy {
removeIndices.insert(index, at: 0)
} else if let scene = await appInstance.scene.first(
where: { await $0.id == element.id }
) as? Storage.SceneElementType as? SceneElement {
await scene.update(element, app: appInstance.app, updateProperties: updateProperties)
for index in removeIndices {
await appInstance.app.editStorage { await $0.removeSceneStorage(index: index) }
}
}
for index in removeIndices {
await appInstance.app.editStorage { await $0.removeSceneStorage(index: index) }
}
}
await StateManager.setAppID(appInstance.id)
}
StateManager.appID = appInstance.id
let state = appInstance.getState()
Task { [appInstance] in
await appInstance.app.editStorage { await $0.setState(state) }
Expand Down Expand Up @@ -118,7 +121,7 @@ extension App {
Task {
await StateManager.updateState(id: app.getStorage().stateStorage.first?.value.id ?? .init())
await StateManager.updateViews()
observe()
await observe()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Model/User Interface/Renderable/Renderable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public protocol Renderable: Sendable {
/// - Parameters:
/// - type: The type of the renderable elements.
/// - fields: More information.
func container<RenderableType>(type: RenderableType.Type, fields: [String: Any]) -> RenderableStorage
func container<RenderableType>(type: RenderableType.Type, fields: [String: Any]) async -> RenderableStorage

/// Update the stored content.
/// - Parameters:
Expand All @@ -25,6 +25,6 @@ public protocol Renderable: Sendable {
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
)
) async

}
1 change: 1 addition & 0 deletions Sources/Model/User Interface/View/AnyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

/// The view type used for any form of a view.
@StateManager
public protocol AnyView: Sendable {

/// The view's content.
Expand Down
1 change: 1 addition & 0 deletions Sources/Model/User Interface/View/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public protocol View: AnyView {
extension View {

/// The view's content.
@StateManager
public var viewContent: Body {

Check failure on line 34 in Sources/Model/User Interface/View/View.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Attributes Violation: Attributes should be on their own lines in functions and types, but on the same line as variables and imports (attributes)
[StateWrapper(content: { view }, state: getState())]
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/View/StateWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ struct StateWrapper: ConvenienceWidget {
var updateProperties = updateProperties
for property in state {
if let oldID = await storage.getState(key: property.key)?.id {
StateManager.changeID(old: oldID, new: property.value.id)
await StateManager.changeID(old: oldID, new: property.value.id)
await storage.setStateID(key: property.key, id: property.value.id)
}
if StateManager.getUpdateState(id: property.value.id) {
if await StateManager.getUpdateState(id: property.value.id) {
updateProperties = true
StateManager.updatedState(id: property.value.id)
await StateManager.updatedState(id: property.value.id)
}
}
guard let storage = await storage.getContent(key: .mainContent).first else {
Expand Down Expand Up @@ -88,7 +88,7 @@ struct StateWrapper: ConvenienceWidget {
Task {
await StateManager.updateState(id: storage.state.first?.value.id ?? .init())
await StateManager.updateViews()
observe(storage: storage)
await observe(storage: storage)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Tests/SampleBackends/Backend1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public enum Backend1 {
public struct Button: BackendWidget {

var label: String
var action: @Sendable () -> Void
var action: @Sendable @StateManager () -> Void

public init(_ label: String, action: @Sendable @escaping () -> Void) {
public init(_ label: String, action: @Sendable @StateManager @escaping () -> Void) {
self.label = label
self.action = action
}
Expand Down Expand Up @@ -95,7 +95,7 @@ public enum Backend1 {
}

public func update<Storage>(_ storage: ViewStorage, modifiers: [(any AnyView) -> any AnyView], updateProperties: Bool, type: Storage.Type) async {
(content as [Renderable]).update(await storage.getRenderableContent(key: .mainContent), updateProperties: updateProperties, type: MenuElement.self, fields: [:])
await (content as [Renderable]).update(await storage.getRenderableContent(key: .mainContent), updateProperties: updateProperties, type: MenuElement.self, fields: [:])
}

}
Expand Down

0 comments on commit 7ea5d96

Please sign in to comment.