Skip to content

Commit

Permalink
Introduce EnvironmentAccessible and allow for later intialization of @…
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Nov 7, 2023
1 parent 3c0a44e commit c408115
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 15 deletions.
38 changes: 38 additions & 0 deletions Sources/Spezi/Capabilities/Observable/EnvironmentAccessible.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


/// Places a ``Module`` into the SwiftUI environment.
///
/// Below is a short code example how you would declare an environment accessible module,
/// and how to access it within SwiftUI, if it is configured in your ``Configuration``.
///
/// ```swift
/// public class ExampleModule: Module, EnvironmentAccessible {
/// // ... implement your functionality
/// }
///
///
/// struct ExampleView: View {
/// @Environment(ExampleModule.self) var module
///
/// var body: some View {
/// // ... access module functionality
/// }
/// }
/// ```
public protocol EnvironmentAccessible: AnyObject, Observable {}


extension EnvironmentAccessible {
var viewModifier: any ViewModifier {
ModelModifier(model: self)
}
}
18 changes: 15 additions & 3 deletions Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ public class _ModelPropertyWrapper<Model: Observable & AnyObject> {
// swiftlint:disable:previous type_name
// We want the type to be hidden from autocompletion and documentation generation

private var storedValue: Model
private var storedValue: Model?
private var collected = false


/// Access the store model.
/// - Note: You cannot access the value once it was collected.
public var wrappedValue: Model {
get {
storedValue
guard let storedValue else {
preconditionFailure("@Model was accessed before it was initialized for the first time.")
}
return storedValue

Check warning on line 29 in Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift#L26-L29

Added lines #L26 - L29 were not covered by tests
}
set {
precondition(!collected, "You cannot reassign a @Model property after it was already collected.")
Expand All @@ -32,6 +35,9 @@ public class _ModelPropertyWrapper<Model: Observable & AnyObject> {
}


/// Initialize a new `@Model` property wrappe
public init() {}

Check warning on line 39 in Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift#L39

Added line #L39 was not covered by tests

/// Initialize a new `@Model` property wrapper.
/// - Parameter wrappedValue: The initial value.
public init(wrappedValue: Model) {
Expand Down Expand Up @@ -67,8 +73,14 @@ extension Module {


extension _ModelPropertyWrapper: ViewModifierProvider {
var viewModifier: any ViewModifier {
var viewModifier: (any ViewModifier)? {
collected = true

guard let storedValue else {
assertionFailure("@Model with type \(Model.self) was collected but no value was provided!")
return nil

Check warning on line 81 in Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift#L80-L81

Added lines #L80 - L81 were not covered by tests
}

return ModelModifier(model: storedValue)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ public class _ModifierPropertyWrapper<Modifier: ViewModifier> {
// swiftlint:disable:previous type_name
// We want the type to be hidden from autocompletion and documentation generation

private var storedValue: Modifier
private var storedValue: Modifier?
private var collected = false


/// Access the store value.
/// - Note: You cannot access the value once it was collected.
public var wrappedValue: Modifier {
get {
storedValue
guard let storedValue else {
preconditionFailure("@Modifier was accessed before it was initialized for the first time.")
}
return storedValue

Check warning on line 29 in Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift#L26-L29

Added lines #L26 - L29 were not covered by tests
}
set {
precondition(!collected, "You cannot update a @Modifier property after it was already collected.")
Expand All @@ -32,6 +35,9 @@ public class _ModifierPropertyWrapper<Modifier: ViewModifier> {
}


/// Initialize a new `@Modifier` property wrapper.
public init() {}

/// Initialize a new `@Modifier` property wrapper.
/// - Parameter wrappedValue: The initial value.
public init(wrappedValue: Modifier) {
Expand Down Expand Up @@ -69,8 +75,14 @@ extension Module {


extension _ModifierPropertyWrapper: ViewModifierProvider {
var viewModifier: any ViewModifier {
var viewModifier: (any ViewModifier)? {
collected = true

guard let storedValue else {
assertionFailure("@Modifier with type \(Modifier.self) was collected but no value was provided!")
return nil

Check warning on line 83 in Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift#L82-L83

Added lines #L82 - L83 were not covered by tests
}

return storedValue
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ enum ModifierPlacement: Int, Comparable {
/// [`ViewModifier`](https://developer.apple.com/documentation/swiftui/viewmodifier) to be injected into the global view hierarchy.
protocol ViewModifierProvider {
/// The view modifier instance that should be injected into the SwiftUI view hierarchy.
var viewModifier: any ViewModifier { get }
///
/// Does nothing if `nil` is provided.
var viewModifier: (any ViewModifier)? { get }

/// Defines the placement order of this view modifier.
///
Expand All @@ -47,7 +49,7 @@ extension Module {
.sorted { lhs, rhs in
lhs.placement < rhs.placement
}
.map { provider in
.compactMap { provider in
provider.viewModifier
}
}
Expand Down
6 changes: 2 additions & 4 deletions Sources/Spezi/Spezi.docc/Module.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,11 @@ class ExampleModule: Module {
- ``Module/Provide``
- ``Module/Collect``

### Managing Model state
### Interaction with SwiftUI

- ``Module/Model``

### Modifying the View hierarchy

- ``Module/Modifier``
- ``EnvironmentAccessible``

### Lifecycle Handling

Expand Down
5 changes: 5 additions & 0 deletions Sources/Spezi/Spezi/Spezi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public actor Spezi<S: Standard>: AnySpezi {
module.storeModule(into: &storage)

collectedModifiers.append(contentsOf: module.viewModifiers)

// If a module is @Observable, we automatically inject it view the `ModelModifier` into the environment.
if let observable = module as? EnvironmentAccessible {
collectedModifiers.append(observable.viewModifier)
}
}

self.storage = storage
Expand Down
12 changes: 9 additions & 3 deletions Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,23 @@ class MyModel2 {
private struct MyModifier: ViewModifier {
@Environment(MyModel2.self)
var model
@Environment(ModuleWithModel.self)
var module

func body(content: Content) -> some View {
content
.environment(\.customKey, model.message == "Hello World")
.environment(\.customKey, model.message == "Hello World" && module.message == "MODEL")
}
}


class ModuleWithModel: Module {
class ModuleWithModel: Module, EnvironmentAccessible {
@Model var model = MyModel2(message: "Hello World")
@Modifier fileprivate var modifier = MyModifier() // ensure reordering happens, ViewModifier must be able to access the model from environment

// ensure reordering happens, ViewModifier must be able to access the model from environment
@Modifier fileprivate var modifier = MyModifier()

let message: String = "MODEL"
}


Expand Down

0 comments on commit c408115

Please sign in to comment.