From 5a59a019809cb8be57d45b77ffa0d2f18c336e8a Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 27 Jun 2024 21:58:59 +0200 Subject: [PATCH] Improve performance of state system --- Package.swift | 6 +- Sources/Model/Data Flow/State.swift | 50 ++++----- Sources/Model/Data Flow/StateManager.swift | 106 ++++++++++++++++++ Sources/Model/Data Flow/StateProtocol.swift | 8 +- Sources/Model/Data Flow/UpdateManager.swift | 34 ------ Sources/Model/Extensions/Array.swift | 3 + .../Model/User Interface/View/AnyView.swift | 8 +- .../Model/User Interface/View/Widget.swift | 5 + Sources/View/StateWrapper.swift | 37 +++--- Tests/DemoApp/DemoApp.swift | 67 +++++++---- Tests/SampleBackends/Backend1.swift | 7 +- 11 files changed, 231 insertions(+), 100 deletions(-) create mode 100644 Sources/Model/Data Flow/StateManager.swift delete mode 100644 Sources/Model/Data Flow/UpdateManager.swift diff --git a/Package.swift b/Package.swift index bf67a85..c40594d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.10 // // Package.swift // Meta @@ -11,6 +11,10 @@ import PackageDescription /// The Meta package is the foundation of the Aparoksha project. let package = Package( name: "Meta", + platforms: [ + .macOS(.v10_15), + .iOS(.v13) + ], products: [ .library( name: "Meta", diff --git a/Sources/Model/Data Flow/State.swift b/Sources/Model/Data Flow/State.swift index d902122..45b3854 100644 --- a/Sources/Model/Data Flow/State.swift +++ b/Sources/Model/Data Flow/State.swift @@ -6,6 +6,7 @@ // import Foundation +import Observation /// A property wrapper for properties in a view that should be stored throughout view updates. @propertyWrapper @@ -18,8 +19,8 @@ public struct State: StateProtocol { } nonmutating set { rawValue = newValue - content.storage.update = true - UpdateManager.updateViews(force: forceUpdates) + StateManager.updateState(id: id) + StateManager.updateViews(force: forceUpdates) } } @@ -32,46 +33,45 @@ public struct State: StateProtocol { } } - // swiftlint:disable force_cast /// Get and set the value without updating the views. public var rawValue: Value { get { - content.storage.value as! Value + guard let value = StateManager.getState(id: id) as? Value else { + let initialValue = getInitialValue() + StateManager.setState(id: id, value: initialValue) + return initialValue + } + return value } nonmutating set { - content.storage.value = newValue - writeValue?(newValue) + StateManager.setState(id: id, value: newValue) } } - // swiftlint:enable force_cast - /// The stored value. - let content: StateContent + /// Whether the value is an observable object. + var isObservable: Bool { + if #available(macOS 14, *), #available(iOS 17, *) { + return Value.self as? Observable.Type != nil + } else { + return false + } + } + + /// The state's identifier for the stored value. + var id: UUID = .init() /// Whether to force update the views when the value changes. - public var forceUpdates: Bool + var forceUpdates: Bool - /// The function for updating the value in the settings file. - private var writeValue: ((Value) -> Void)? - - /// The value with an erased type. - public var value: Any { - get { - wrappedValue - } - nonmutating set { - if let newValue = newValue as? Value { - content.storage.value = newValue - } - } - } + /// The closure for initializing the state property's value. + var getInitialValue: () -> Value /// Initialize a property representing a state in the view with an autoclosure. /// - Parameters: /// - wrappedValue: The wrapped value. /// - forceUpdates: Whether to force update all available views when the property gets modified. public init(wrappedValue: @autoclosure @escaping () -> Value, forceUpdates: Bool = false) { - content = .init(getInitialValue: wrappedValue) + getInitialValue = wrappedValue self.forceUpdates = forceUpdates } diff --git a/Sources/Model/Data Flow/StateManager.swift b/Sources/Model/Data Flow/StateManager.swift new file mode 100644 index 0000000..335d6b8 --- /dev/null +++ b/Sources/Model/Data Flow/StateManager.swift @@ -0,0 +1,106 @@ +// +// StateManager.swift +// Meta +// +// Created by david-swift on 21.06.24. +// + +import Foundation + +/// This type manages view updates. +public enum StateManager { + + /// Whether to block updates in general. + public static var blockUpdates = false + /// Whether to save state. + public static var saveState = true + /// The functions handling view updates. + static var updateHandlers: [(Bool) -> Void] = [] + /// The state. + static var state: [State] = [] + + /// Information about a piece of state. + struct State { + + /// The state's identifiers. + var ids: [UUID] + /// The state value. + var value: Any? + /// Whether to update in the next iteration. + var update = false + + } + + /// Update all of the views. + /// - Parameter force: Whether to force all views to update. + /// + /// Nothing happens if ``UpdateManager/blockUpdates`` is true. + static func updateViews(force: Bool = false) { + if !blockUpdates { + for handler in updateHandlers { + handler(force) + } + } + } + + /// 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. + public static func addUpdateHandler(handler: @escaping (Bool) -> Void) { + updateHandlers.append(handler) + } + + /// Set the state value for a certain ID. + /// - Parameters: + /// - id: The identifier. + /// - value: The new value. + static func setState(id: UUID, value: Any?) { + if saveState { + guard let index = state.firstIndex(where: { $0.ids.contains(id) }) else { + state.append(.init(ids: [id], value: value)) + return + } + state[safe: index]?.value = value + } + } + + /// Get the state value for a certain ID. + /// - Parameter id: The identifier. + /// - Returns: The value. + static func getState(id: UUID) -> Any? { + state[safe: state.firstIndex { $0.ids.contains(id) }]?.value + } + + /// Mark the state of a certain id as updated. + /// - Parameter id: The identifier. + static func updateState(id: UUID) { + if saveState { + state[safe: state.firstIndex { $0.ids.contains(id) }]?.update = true + } + } + + /// Mark the state of a certain id as not updated. + /// - Parameter id: The identifier. + static func updatedState(id: UUID) { + if saveState { + state[safe: state.firstIndex { $0.ids.contains(id) }]?.update = false + } + } + + /// Get whether to update the state of a certain id. + /// - Parameter id: The identifier. + /// - Returns: Whether to update the state. + static func getUpdateState(id: UUID) -> Bool { + state[safe: state.firstIndex { $0.ids.contains(id) }]?.update ?? false + } + + /// Change the identifier for a certain state value. + /// - Parameters: + /// - oldID: The old identifier. + /// - newID: The new identifier. + static func changeID(old oldID: UUID, new newID: UUID) { + if saveState { + state[safe: state.firstIndex { $0.ids.contains(oldID) }]?.ids.append(newID) + } + } + +} diff --git a/Sources/Model/Data Flow/StateProtocol.swift b/Sources/Model/Data Flow/StateProtocol.swift index d92e793..430d6fe 100644 --- a/Sources/Model/Data Flow/StateProtocol.swift +++ b/Sources/Model/Data Flow/StateProtocol.swift @@ -5,10 +5,14 @@ // Created by david-swift on 26.05.24. // +import Foundation + /// An interface for accessing `State` without specifying the generic type. protocol StateProtocol { - /// The `StateContent`. - var content: StateContent { get } + /// The identifier for the state property's value. + var id: UUID { get set } + /// Whether the state value is an observable object. + var isObservable: Bool { get } } diff --git a/Sources/Model/Data Flow/UpdateManager.swift b/Sources/Model/Data Flow/UpdateManager.swift deleted file mode 100644 index 4bb6d7d..0000000 --- a/Sources/Model/Data Flow/UpdateManager.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// UpdateManager.swift -// Meta -// -// Created by david-swift on 26.05.24. -// - -/// This type manages view updates. -public enum UpdateManager { - - /// Whether to block updates in general. - public static var blockUpdates = false - /// The functions handling view updates. - static var updateHandlers: [(Bool) -> Void] = [] - - /// Update all of the views. - /// - Parameter force: Whether to force all views to update. - /// - /// Nothing happens if ``UpdateManager/blockUpdates`` is true. - static func updateViews(force: Bool = false) { - if !blockUpdates { - for handler in updateHandlers { - handler(force) - } - } - } - - /// 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. - public static func addUpdateHandler(handler: @escaping (Bool) -> Void) { - updateHandlers.append(handler) - } - -} diff --git a/Sources/Model/Extensions/Array.swift b/Sources/Model/Extensions/Array.swift index 146e0ae..790a5d0 100644 --- a/Sources/Model/Extensions/Array.swift +++ b/Sources/Model/Extensions/Array.swift @@ -20,6 +20,8 @@ extension Array: AnyView where Element == AnyView { type: WidgetType.Type, modifiers: [(AnyView) -> AnyView] = [] ) -> String { + let oldValue = StateManager.saveState + StateManager.saveState = false var description = "" for view in self where view.renderable(type: type, modifiers: modifiers) { description += view.getDebugTree(parameters: parameters, type: type, modifiers: modifiers) + "\n" @@ -27,6 +29,7 @@ extension Array: AnyView where Element == AnyView { if !description.isEmpty { description.removeLast() } + StateManager.saveState = oldValue return description } diff --git a/Sources/Model/User Interface/View/AnyView.swift b/Sources/Model/User Interface/View/AnyView.swift index 3212a44..5fa6aa4 100644 --- a/Sources/Model/User Interface/View/AnyView.swift +++ b/Sources/Model/User Interface/View/AnyView.swift @@ -26,12 +26,17 @@ extension AnyView { type: WidgetType.Type, modifiers: [(AnyView) -> AnyView] = [] ) -> String { + let oldValue = StateManager.saveState + StateManager.saveState = false + defer { + StateManager.saveState = oldValue + } if let body = getModified(modifiers: modifiers) as? Body { return body.getBodyDebugTree(parameters: parameters, type: type, modifiers: modifiers) } else if let widget = getModified(modifiers: modifiers) as? Widget { return widget.getViewDescription(parameters: parameters, type: type, modifiers: modifiers) } - return """ + let string = """ \(Self.self) { \(indented: viewContent .map { view in @@ -40,6 +45,7 @@ extension AnyView { .getBodyDebugTree(parameters: parameters, type: type, modifiers: modifiers)) } """ + return string } func getModified(modifiers: [(AnyView) -> AnyView]) -> AnyView { diff --git a/Sources/Model/User Interface/View/Widget.swift b/Sources/Model/User Interface/View/Widget.swift index fde0f26..980a17e 100644 --- a/Sources/Model/User Interface/View/Widget.swift +++ b/Sources/Model/User Interface/View/Widget.swift @@ -43,6 +43,11 @@ extension Widget { type: WidgetType.Type, modifiers: [(AnyView) -> AnyView] ) -> String { + let oldValue = StateManager.saveState + StateManager.saveState = false + defer { + StateManager.saveState = oldValue + } var content = "" for element in debugTreeContent { if content.isEmpty { diff --git a/Sources/View/StateWrapper.swift b/Sources/View/StateWrapper.swift index 065d168..ee64746 100644 --- a/Sources/View/StateWrapper.swift +++ b/Sources/View/StateWrapper.swift @@ -11,7 +11,7 @@ import Observation public struct StateWrapper: ConvenienceWidget { /// The content. - var content: Body + var content: () -> Body /// The state information (from properties with the `State` wrapper). var state: [String: StateProtocol] = [:] @@ -24,7 +24,7 @@ public struct StateWrapper: ConvenienceWidget { /// The debug tree's content. public var debugTreeContent: [(String, body: Body)] { - [("content", body: content)] + [("content", body: content())] } /// The identifier of the field storing whether to update the wrapper's content. @@ -33,7 +33,7 @@ public struct StateWrapper: ConvenienceWidget { /// Initialize a `StateWrapper`. /// - Parameter content: The view content. public init(@ViewBuilder content: @escaping () -> Body) { - self.content = content() + self.content = content } /// Initialize a `StateWrapper`. @@ -41,7 +41,7 @@ public struct StateWrapper: ConvenienceWidget { /// - content: The view content. /// - state: The state information. init(content: @escaping () -> Body, state: [String: StateProtocol]) { - self.content = content() + self.content = content self.state = state } @@ -60,18 +60,19 @@ public struct StateWrapper: ConvenienceWidget { var updateProperties = storage.fields[updateID] as? Bool ?? false storage.fields[updateID] = false for property in state { - if let storage = storage.state[property.key]?.content.storage { - property.value.content.storage = storage + if let oldID = storage.state[property.key]?.id { + StateManager.changeID(old: oldID, new: property.value.id) + storage.state[property.key]?.id = property.value.id } - if property.value.content.storage.update { + if StateManager.getUpdateState(id: property.value.id) { updateProperties = true - property.value.content.storage.update = false + StateManager.updatedState(id: property.value.id) } } guard let storages = storage.content[.mainContent] else { return } - content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type) + content().update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type) } /// Get a view storage. @@ -80,22 +81,28 @@ public struct StateWrapper: ConvenienceWidget { /// - type: The type of the widgets. /// - Returns: The view storage. public func container(modifiers: [(AnyView) -> AnyView], type: WidgetType.Type) -> ViewStorage { - let content = content.storages(modifiers: modifiers, type: type) + let content = content().storages(modifiers: modifiers, type: type) let storage = ViewStorage(nil, content: [.mainContent: content]) storage.state = state - observe(storage: storage) + if #available(macOS 14, *), #available(iOS 17, *), state.contains(where: { $0.value.isObservable }) { + observe(storage: storage) + } return storage } /// Observe the observable properties accessed in the view. /// - Parameter storage: The view storage + @available(macOS, introduced: 14) + @available(iOS, introduced: 17) func observe(storage: ViewStorage) { withObservationTracking { - _ = content.getDebugTree(parameters: true, type: AnyView.self) + _ = content().getDebugTree(parameters: true, type: AnyView.self) } onChange: { - storage.fields[updateID] = true - UpdateManager.updateViews() - observe(storage: storage) + Task { + StateManager.updateState(id: storage.state.first?.value.id ?? .init()) + StateManager.updateViews() + observe(storage: storage) + } } } diff --git a/Tests/DemoApp/DemoApp.swift b/Tests/DemoApp/DemoApp.swift index 9399bdc..a7bea6b 100644 --- a/Tests/DemoApp/DemoApp.swift +++ b/Tests/DemoApp/DemoApp.swift @@ -1,15 +1,18 @@ import Foundation import Meta +import Observation import SampleBackends +@available(macOS 14, *) +@available(iOS 17, *) struct DemoView: View { - @State private var test = "Label" + @State private var test = TestModel() - var view: Body { + var view: Body { Backend1.TestWidget1() - Backend1.Button(test) { - test = "\(Int.random(in: 1...10))" + Backend1.Button(test.test) { + test.test = "\(Int.random(in: 1...10))" } TestView() testContent @@ -23,29 +26,55 @@ struct DemoView: View { } -struct TestView: SimpleView { +struct TestView: View { + + @State private var test = "Label" var view: Body { Backend2.TestWidget4() + Backend1.Button(test) { + Task { + try await Task.sleep(nanoseconds: 100_000_000) + test = "\(Int.random(in: 1...10))" + } + } } } -let backendType = Backend1.BackendWidget.self -let modifiers: [(AnyView) -> AnyView] = [ - { $0 as? Backend2.TestWidget2 != nil ? [Backend1.TestWidget1()] : $0 } -] +@available(macOS 14, *) +@available(iOS 17, *) +@Observable +class TestModel { + + var test = "Label" -print(DemoView().getDebugTree(parameters: true, type: backendType, modifiers: modifiers)) -let storage = DemoView().storage(modifiers: modifiers, type: backendType) -for round in 0...2 { - print("#\(round)") - DemoView().updateStorage(storage, modifiers: modifiers, updateProperties: true, type: backendType) } -UpdateManager.addUpdateHandler { _ in - print("#Handler") - DemoView().updateStorage(storage, modifiers: modifiers, updateProperties: false, type: backendType) -} +@main +@available(macOS 14, *) +@available(iOS 17, *) +struct DemoApp { -sleep(2) + static func main() { + let backendType = Backend1.BackendWidget.self + let modifiers: [(AnyView) -> AnyView] = [ + { $0 as? Backend2.TestWidget2 != nil ? [Backend1.TestWidget1()] : $0 } + ] + + print(DemoView().getDebugTree(parameters: true, type: backendType, modifiers: modifiers)) + let storage = DemoView().storage(modifiers: modifiers, type: backendType) + for round in 0...2 { + print("#\(round)") + DemoView().updateStorage(storage, modifiers: modifiers, updateProperties: true, type: backendType) + } + + StateManager.addUpdateHandler { _ in + DemoView().updateStorage(storage, modifiers: modifiers, updateProperties: false, type: backendType) + } + + sleep(2) + DemoView().updateStorage(storage, modifiers: modifiers, updateProperties: true, type: backendType) + } + +} diff --git a/Tests/SampleBackends/Backend1.swift b/Tests/SampleBackends/Backend1.swift index 2a7714e..0113972 100644 --- a/Tests/SampleBackends/Backend1.swift +++ b/Tests/SampleBackends/Backend1.swift @@ -74,8 +74,9 @@ public enum Backend1 { } public func container(modifiers: [(any AnyView) -> any AnyView], type: WidgetType.Type) -> ViewStorage { + print("Init button") Task { - try await Task.sleep(for: .seconds(1)) + try await Task.sleep(nanoseconds: 1_000_000_000) action() } return .init(nil) @@ -83,9 +84,9 @@ public enum Backend1 { public func update(_ storage: ViewStorage, modifiers: [(any AnyView) -> any AnyView], updateProperties: Bool, type: WidgetType.Type) { if updateProperties { - print("Update button") + print("Update button (label = \(label))") } else { - print("Do not update button") + print("Do not update button (label = \(label))") } }