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

Commit

Permalink
Implement strict concurrency
Browse files Browse the repository at this point in the history
  • Loading branch information
david-swift committed Jul 10, 2024
1 parent da69a57 commit 5341bb8
Show file tree
Hide file tree
Showing 25 changed files with 449 additions and 230 deletions.
15 changes: 12 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,26 @@ let package = Package(
targets: [
.target(
name: "Meta",
path: "Sources"
path: "Sources",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.target(
name: "SampleBackends",
dependencies: ["Meta"],
path: "Tests/SampleBackends"
path: "Tests/SampleBackends",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.executableTarget(
name: "DemoApp",
dependencies: ["SampleBackends"],
path: "Tests/DemoApp"
path: "Tests/DemoApp",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)
8 changes: 5 additions & 3 deletions Sources/Model/Data Flow/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public struct State<Value>: StateProtocol {
nonmutating set {
rawValue = newValue
StateManager.updateState(id: id)
StateManager.updateViews(force: forceUpdates)
Task {
await StateManager.updateViews(force: forceUpdates)
}
}
}

Expand Down Expand Up @@ -64,13 +66,13 @@ public struct State<Value>: StateProtocol {
var forceUpdates: Bool

/// The closure for initializing the state property's value.
var getInitialValue: () -> Value
var getInitialValue: @Sendable () -> 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) {
public init(wrappedValue: @Sendable @autoclosure @escaping () -> Value, forceUpdates: Bool = false) {
getInitialValue = wrappedValue
self.forceUpdates = forceUpdates
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/Model/Data Flow/StateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// This type manages view updates.
public enum StateManager {
public actor StateManager {

/// Whether to block updates in general.
public static var blockUpdates = false
Expand All @@ -17,7 +17,7 @@ public enum StateManager {
/// The application identifier.
static var appID: String?
/// The functions handling view updates.
static var updateHandlers: [(Bool) -> Void] = []
static var updateHandlers: [(Bool) async -> Void] = []
/// The state.
static var state: [State] = []

Expand Down Expand Up @@ -50,17 +50,17 @@ public enum StateManager {
/// - Parameter force: Whether to force all views to update.
///
/// Nothing happens if ``UpdateManager/blockUpdates`` is true.
public static func updateViews(force: Bool = false) {
public static func updateViews(force: Bool = false) async {
if !blockUpdates {
for handler in updateHandlers {
handler(force)
await 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) {
public static func addUpdateHandler(handler: @escaping (Bool) async -> Void) {
updateHandlers.append(handler)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Model/Data Flow/StateProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// An interface for accessing `State` without specifying the generic type.
protocol StateProtocol {
protocol StateProtocol: Sendable {

/// The identifier for the state property's value.
var id: UUID { get set }
Expand Down
48 changes: 38 additions & 10 deletions Sources/Model/Extensions/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ extension Array: AnyView where Element == AnyView {
modifiers: [(AnyView) -> AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
) async where Storage: AppStorage {
for (index, element) in filter({ $0.renderable(type: type, modifiers: modifiers) }).enumerated() {
if let storage = storages[safe: index] {
element
await element
.widget(modifiers: modifiers, type: type)
.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
Expand All @@ -63,9 +63,9 @@ extension Array: AnyView where Element == AnyView {
public func storages<Storage>(
modifiers: [(AnyView) -> AnyView],
type: Storage.Type
) -> [ViewStorage] where Storage: AppStorage {
compactMap { view in
view.renderable(type: type, modifiers: modifiers) ? view.storage(modifiers: modifiers, type: type) : nil
) async -> [ViewStorage] where Storage: AppStorage {
await compactMap { view in
await view.renderable(type: type, modifiers: modifiers) ? view.storage(modifiers: modifiers, type: type) : nil
}
}

Expand Down Expand Up @@ -96,6 +96,34 @@ extension Array {
}
}

public func first(where predicate: (Element) async throws -> Bool) async rethrows -> Element? {
for element in self {
let matches = try await predicate(element)
if matches {
return element
}
}
return nil
}

public func last(where predicate: (Element) async throws -> Bool) async rethrows -> Element? {
try await reversed().first(where: predicate)
}

public func compactMap<ElementOfResult>(_ transform: (Element) async throws -> ElementOfResult?) async rethrows -> [ElementOfResult] {
var result: [ElementOfResult] = []
for element in self {
if let element = try await transform(element) {
result.append(element)
}
}
return result
}

public func map<ElementOfResult>(_ transform: (Element) async throws -> ElementOfResult) async rethrows -> [ElementOfResult] {
try await compactMap { try await transform($0) }
}

}

extension Array where Element: Identifiable {
Expand Down Expand Up @@ -128,10 +156,10 @@ extension Array where Element == Renderable {
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
) {
) async {
for (index, element) in 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 @@ -145,10 +173,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
20 changes: 20 additions & 0 deletions Sources/Model/Pointer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Pointer.swift
// Meta
//
// Created by david-swift on 10.07.24.
//

public struct Pointer: Sendable {

var bitPattern: Int

public init(_ pointer: OpaquePointer) {
bitPattern = .init(bitPattern: pointer)
}

public var opaquePointer: OpaquePointer? {
.init(bitPattern: bitPattern)
}

}
60 changes: 32 additions & 28 deletions Sources/Model/User Interface/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Observation
/// }
/// ```
///
public protocol App {
public protocol App: Sendable {

/// The app storage typ.
associatedtype Storage: AppStorage
Expand All @@ -44,11 +44,11 @@ public protocol App {
extension App {

/// The application's entry point.
public static func main() {
public static func main() async {
let app = setupApp()
app.app.run {
await app.app.run {
for element in app.scene {
element.setupInitialContainers(app: app.app)
await element.setupInitialContainers(app: app.app)
}
}
}
Expand All @@ -58,36 +58,40 @@ extension App {
/// To run the app, call the ``AppStorage/run(automaticSetup:manualSetup:)`` function.
public static func setupApp() -> Self {
var appInstance = self.init()
appInstance.app = Storage(id: appInstance.id) { appInstance }
appInstance.app = Storage(id: appInstance.id)
StateManager.addUpdateHandler { force in
var updateProperties = force
for property in appInstance.getState() {
if let oldID = appInstance.app.storage.stateStorage[property.key]?.id {
StateManager.changeID(old: oldID, new: property.value.id)
appInstance.app.storage.stateStorage[property.key]?.id = property.value.id
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) }
}
if StateManager.getUpdateState(id: property.value.id) {
updateProperties = true
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 appInstance.app.storage.sceneStorage.enumerated() {
if element.destroy {
removeIndices.insert(index, at: 0)
} else if let scene = appInstance.scene.first(
where: { $0.id == element.id }
) as? Storage.SceneElementType as? SceneElement {
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 {
appInstance.app.storage.sceneStorage.remove(at: index)
}
}
StateManager.appID = appInstance.id
let state = appInstance.getState()
appInstance.app.storage.stateStorage = state
Task { [appInstance] in
await appInstance.app.editStorage { await $0.setState(state) }
}
if #available(macOS 14, *), #available(iOS 17, *), state.contains(where: { $0.value.isObservable }) {
appInstance.observe()
}
Expand All @@ -112,8 +116,8 @@ extension App {
_ = scene
} onChange: {
Task {
StateManager.updateState(id: app.storage.stateStorage.first?.value.id ?? .init())
StateManager.updateViews()
await StateManager.updateState(id: app.getStorage().stateStorage.first?.value.id ?? .init())
await StateManager.updateViews()
observe()
}
}
Expand Down
Loading

0 comments on commit 5341bb8

Please sign in to comment.