Improve performance of state system

This commit is contained in:
david-swift 2024-06-27 21:58:59 +02:00
parent 4b9ea1e883
commit 5a59a01980
11 changed files with 231 additions and 100 deletions

View File

@ -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",

View File

@ -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
}

View 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)
}
}
}

View File

@ -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 }
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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))")
}
}