Improve performance of state system
This commit is contained in:
parent
4b9ea1e883
commit
5a59a01980
@ -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",
|
||||
|
||||
@ -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<Value>: 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<Value>: 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
|
||||
}
|
||||
|
||||
|
||||
106
Sources/Model/Data Flow/StateManager.swift
Normal file
106
Sources/Model/Data Flow/StateManager.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 }
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<WidgetType>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -74,8 +74,9 @@ public enum Backend1 {
|
||||
}
|
||||
|
||||
public func container<WidgetType>(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<WidgetType>(_ 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))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user