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 11, 2024
1 parent da69a57 commit 55f916a
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 49 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")
]
)
]
)
3 changes: 3 additions & 0 deletions Sources/Model/Data Flow/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Observation
public struct State<Value>: StateProtocol {

/// Access the stored value. This updates the views when being changed.
@StateManager
public var wrappedValue: Value {

Check failure on line 17 in Sources/Model/Data Flow/State.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)
get {
rawValue
Expand All @@ -25,6 +26,7 @@ public struct State<Value>: StateProtocol {
}

/// Get the value as a binding using the `$` prefix.
@StateManager
public var projectedValue: Binding<Value> {

Check failure on line 30 in Sources/Model/Data Flow/State.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)
.init {
wrappedValue
Expand All @@ -34,6 +36,7 @@ public struct State<Value>: StateProtocol {
}

/// Get and set the value without updating the views.
@StateManager
public var rawValue: Value {

Check failure on line 40 in Sources/Model/Data Flow/State.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)
get {
guard let value = StateManager.getState(id: id) as? Value else {
Expand Down
36 changes: 28 additions & 8 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.
public enum StateManager {
@globalActor
public actor StateManager {

public static let shared = StateManager()

Check failure on line 14 in Sources/Model/Data Flow/StateManager.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Missing Docs Violation: public declarations should be documented (missing_docs)

/// Whether to block updates in general.
public static var blockUpdates = false
@StateManager static var blockUpdates = false
/// Whether to save state.
public static var saveState = true
@StateManager 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) -> 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,24 +53,29 @@ public enum 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) {
if !blockUpdates {
for handler in updateHandlers {
handler(force)
Task {
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) {
@StateManager
public static func addUpdateHandler(handler: @escaping (Bool) async -> Void) {
updateHandlers.append(handler)
}

/// Set the state value for a certain ID.
/// - 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 +89,14 @@ public enum 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 +105,7 @@ public enum 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 +115,7 @@ public enum 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 +124,18 @@ public enum 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
}

}
32 changes: 18 additions & 14 deletions Sources/Model/User Interface/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public protocol App {
/// The app's application ID.
var id: String { get }
/// The app's scene.
@SceneBuilder var scene: Scene { get }
@StateManager @SceneBuilder var scene: Scene { get }
// swiftlint:disable implicitly_unwrapped_optional
/// The app storage.
var app: Storage! { get set }
Expand All @@ -44,8 +44,9 @@ public protocol App {
extension App {

/// The application's entry point.
public static func main() {
let app = setupApp()
@StateManager
public static func main() async {
let app = await setupApp()
app.app.run {
for element in app.scene {
element.setupInitialContainers(app: app.app)
Expand All @@ -56,26 +57,26 @@ extension App {
/// Initialize and get the app with the app storage.
///
/// To run the app, call the ``AppStorage/run(automaticSetup:manualSetup:)`` function.
public static func setupApp() -> Self {
public static func setupApp() async -> Self {
var appInstance = self.init()
appInstance.app = Storage(id: appInstance.id) { appInstance }
StateManager.addUpdateHandler { force in
await 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)
await StateManager.changeID(old: oldID, new: property.value.id)
appInstance.app.storage.stateStorage[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)
}
}
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(
} else if let scene = await appInstance.scene.first(
where: { $0.id == element.id }
) as? Storage.SceneElementType as? SceneElement {
scene.update(element, app: appInstance.app, updateProperties: updateProperties)
Expand All @@ -85,11 +86,13 @@ extension App {
appInstance.app.storage.sceneStorage.remove(at: index)
}
}
StateManager.appID = appInstance.id
Task { [appInstance] in
await StateManager.setAppID(appInstance.id)
}
let state = appInstance.getState()
appInstance.app.storage.stateStorage = state
if #available(macOS 14, *), #available(iOS 17, *), state.contains(where: { $0.value.isObservable }) {
appInstance.observe()
await appInstance.observe()
}
return appInstance
}
Expand All @@ -107,14 +110,15 @@ extension App {
/// Observe the observable properties accessed in the app.
@available(macOS, introduced: 14)
@available(iOS, introduced: 17)
@StateManager
func observe() {
withObservationTracking {
_ = scene
} onChange: {
Task {
StateManager.updateState(id: app.storage.stateStorage.first?.value.id ?? .init())
StateManager.updateViews()
observe()
await StateManager.updateState(id: app.storage.stateStorage.first?.value.id ?? .init())
await StateManager.updateViews()
await observe()
}
}
}
Expand Down
13 changes: 8 additions & 5 deletions Sources/Model/User Interface/App/AppStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public protocol AppStorage: AnyObject {

/// Run the application.
/// - Parameter setup: A closure that is expected to be executed right at the beginning.
func run(setup: @escaping () -> Void)
@StateManager
func run(setup: @StateManager @escaping () -> Void)

/// Terminate the application.
func quit()
Expand All @@ -47,10 +48,12 @@ extension AppStorage {
/// Add a new scene element with the content of the scene element with a certain id.
/// - Parameter id: The element's id.
public func addSceneElement(_ id: String) {
if let element = app().scene.last(where: { $0.id == id }) {
let container = element.container(app: self)
storage.sceneStorage.append(container)
showSceneElement(id)
Task {
if let element = await app().scene.last(where: { $0.id == id }) {
let container = element.container(app: self)
storage.sceneStorage.append(container)
showSceneElement(id)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/Model/User Interface/View/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
public protocol View: AnyView {

/// The view's content.
@ViewBuilder var view: Body { get }
@StateManager @ViewBuilder var view: Body { get }

}

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
30 changes: 16 additions & 14 deletions Sources/View/StateWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,23 @@ struct StateWrapper: ConvenienceWidget {
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
var updateProperties = updateProperties
for property in state {
if let oldID = storage.state[property.key]?.id {
StateManager.changeID(old: oldID, new: property.value.id)
storage.state[property.key]?.id = property.value.id
Task {
var updateProperties = updateProperties
for property in state {
if let oldID = storage.state[property.key]?.id {
await StateManager.changeID(old: oldID, new: property.value.id)
storage.state[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)
guard let storage = storage.content[.mainContent]?.first else {
return
}
content().updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
guard let storage = storage.content[.mainContent]?.first else {
return
}
content().updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
}

/// Get a view storage.
Expand Down Expand Up @@ -86,8 +88,8 @@ struct StateWrapper: ConvenienceWidget {
_ = content()
} onChange: {
Task {
StateManager.updateState(id: storage.state.first?.value.id ?? .init())
StateManager.updateViews()
await StateManager.updateState(id: storage.state.first?.value.id ?? .init())
await StateManager.updateViews()
observe(storage: storage)
}
}
Expand Down
5 changes: 3 additions & 2 deletions Tests/DemoApp/DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import Observation
import SampleBackends

@main
@StateManager
@available(macOS 14, *)
@available(iOS 17, *)
struct TestExecutable {

public static func main() {
DemoApp.main()
public static func main() async {
await DemoApp.main()
sleep(2)
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/SampleBackends/Backend1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public enum Backend1 {
self.app = app
}

public func run(setup: @escaping () -> Void) {
public func run(setup: @StateManager @escaping () -> Void) {
setup()
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/SampleBackends/Backend2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public enum Backend2 {
self.app = app
}

public func run(setup: @escaping () -> Void) {
public func run(setup: @StateManager @escaping () -> Void) {
setup()
}

Expand Down

0 comments on commit 55f916a

Please sign in to comment.