Skip to content

Commit

Permalink
Allow external ownership for Modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Jun 23, 2024
1 parent 734f90c commit 3de8dcc
Show file tree
Hide file tree
Showing 15 changed files with 467 additions and 176 deletions.
22 changes: 17 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,41 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "1.0.2"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.1")
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1"),
.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))
],
targets: [
.target(
name: "Spezi",
dependencies: [
.product(name: "SpeziFoundation", package: "SpeziFoundation"),
.product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions")
]
.product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions"),
.product(name: "OrderedCollections", package: "swift-collections")
],
plugins: [.swiftLintPlugin]
),
.target(
name: "XCTSpezi",
dependencies: [
.target(name: "Spezi")
]
],
plugins: [.swiftLintPlugin]
),
.testTarget(
name: "SpeziTests",
dependencies: [
.target(name: "Spezi"),
.target(name: "XCTSpezi"),
.product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions")
]
],
plugins: [.swiftLintPlugin]
)
]
)

extension Target.PluginUsage {
static var swiftLintPlugin: Target.PluginUsage {
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ public class _CollectPropertyWrapper<Value> {

extension _CollectPropertyWrapper: StorageValueCollector {
public func retrieve<Repository: SharedRepository<SpeziAnchor>>(from repository: Repository) {
injectedValues = repository[CollectedModuleValues<Value>.self]?.map { $0.value } ?? []
injectedValues = repository[CollectedModuleValues<Value>.self].reduce(into: []) { partialResult, entry in
partialResult.append(contentsOf: entry.value)
}
}

func clear() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,16 @@
// SPDX-License-Identifier: MIT
//

import Foundation
import OrderedCollections
import SpeziFoundation

protocol AnyCollectModuleValue {
associatedtype Value

var moduleReference: ModuleReference { get }
}

protocol AnyCollectModuleValues {
associatedtype Value

var values: [any AnyCollectModuleValue] { get }

mutating func removeValues(from module: any Module) -> Bool

func store(into storage: inout SpeziStorage)
}


struct CollectModuleValue<Value>: AnyCollectModuleValue {
let value: Value
let moduleReference: ModuleReference

init(_ value: Value) {
self.value = value

guard let module = Spezi.moduleInitContext else {
preconditionFailure("Tried to initialize CollectModuleValue with unknown module init context.")
}
self.moduleReference = ModuleReference(module)
}
}

/// Provides the ``KnowledgeSource`` for any value we store in the ``SpeziStorage`` that is
/// provided or request from am ``Module``.
///
/// For more information, look at the ``Module/Provide`` or ``Module/Collect`` property wrappers.
struct CollectedModuleValues<ModuleValue>: DefaultProvidingKnowledgeSource {
typealias Anchor = SpeziAnchor

typealias Value = [CollectModuleValue<ModuleValue>]

typealias Value = OrderedDictionary<UUID, [ModuleValue]>

static var defaultValue: Value {
[]
}
}


extension Array: AnyCollectModuleValues where Element: AnyCollectModuleValue {
typealias Value = Element.Value

var values: [any AnyCollectModuleValue] {
self
}

mutating func removeValues(from module: any Module) -> Bool {
let previousCount = count
removeAll { entry in
entry.moduleReference == ModuleReference(module)
}
return previousCount != count
}

func store(into storage: inout SpeziStorage) {
guard let values = self as? [CollectModuleValue<Value>] else {
preconditionFailure("Unexpected array type: \(type(of: self))")
}
storage[CollectedModuleValues<Value>.self] = values
[:]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@
// SPDX-License-Identifier: MIT
//


import Foundation
import SpeziFoundation
import XCTRuntimeAssertions


/// A protocol that identifies a ``_ProvidePropertyWrapper`` which `Value` type is a `Collection`.
protocol CollectionBasedProvideProperty {
func collectArrayElements<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository)

func clearValues()
}


/// A protocol that identifies a ``_ProvidePropertyWrapper`` which `Value` type is a `Optional`.
protocol OptionalBasedProvideProperty {
func collectOptional<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository)

func clearValues()
}


Expand All @@ -28,10 +34,16 @@ public class _ProvidePropertyWrapper<Value> {
// swiftlint:disable:previous type_name
// We want the type to be hidden from autocompletion and documentation generation

/// Persistent identifier to store and remove @Provide values.
fileprivate let id = UUID()

private var storedValue: Value
private var collected = false


private weak var spezi: Spezi?


/// Access the store value.
/// - Note: You cannot access the value once it was collected.
public var wrappedValue: Value {
Expand All @@ -50,6 +62,15 @@ public class _ProvidePropertyWrapper<Value> {
public init(wrappedValue value: Value) {
self.storedValue = value
}

func inject(spezi: Spezi) {
self.spezi = spezi
}


deinit {
clear()
}
}


Expand Down Expand Up @@ -116,38 +137,54 @@ extension _ProvidePropertyWrapper: StorageValueProvider {
} else if let wrapperWithArray = self as? CollectionBasedProvideProperty {
wrapperWithArray.collectArrayElements(into: &repository)
} else {
repository.appendValues([CollectModuleValue(storedValue)])
repository.setValues(for: id, [storedValue])
}

collected = true
}

func clear() {
collected = false

if let wrapperWithOptional = self as? OptionalBasedProvideProperty {
wrapperWithOptional.clearValues()
} else if let wrapperWithArray = self as? CollectionBasedProvideProperty {
wrapperWithArray.clearValues()
} else {
spezi?.handleCollectedValueRemoval(for: id, of: Value.self)
}
}
}


extension _ProvidePropertyWrapper: CollectionBasedProvideProperty where Value: AnyArray {
func collectArrayElements<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository) {
repository.appendValues(storedValue.unwrappedArray.map { CollectModuleValue($0) })
repository.setValues(for: id, storedValue.unwrappedArray)
}

func clearValues() {
spezi?.handleCollectedValueRemoval(for: id, of: Value.Element.self)
}
}


extension _ProvidePropertyWrapper: OptionalBasedProvideProperty where Value: AnyOptional {
func collectOptional<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository) {
if let storedValue = storedValue.unwrappedOptional {
repository.appendValues([CollectModuleValue(storedValue)])
repository.setValues(for: id, [storedValue])
}
}

func clearValues() {
spezi?.handleCollectedValueRemoval(for: id, of: Value.Wrapped.self)
}
}


extension SharedRepository where Anchor == SpeziAnchor {
fileprivate mutating func appendValues<Value>(_ values: [CollectModuleValue<Value>]) {
fileprivate mutating func setValues<Value>(for id: UUID, _ values: [Value]) {
var current = self[CollectedModuleValues<Value>.self]
current.append(contentsOf: values)
current[id] = values
self[CollectedModuleValues<Value>.self] = current
}
}
13 changes: 13 additions & 0 deletions Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ public class _ModelPropertyWrapper<Model: Observable & AnyObject> {
// swiftlint:disable:previous type_name
// We want the type to be hidden from autocompletion and documentation generation

let id = UUID()
private var storedValue: Model?
private var collected = false

private weak var spezi: Spezi?


/// Access the store model.
///
Expand All @@ -44,12 +47,22 @@ public class _ModelPropertyWrapper<Model: Observable & AnyObject> {
public init(wrappedValue: Model) {
self.storedValue = wrappedValue
}


deinit {
clear()
}
}


extension _ModelPropertyWrapper: SpeziPropertyWrapper {
func clear() {
collected = false
spezi?.handleViewModifierRemoval(for: id)
}

func inject(spezi: Spezi) {
self.spezi = spezi
}
}

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

let id = UUID()
private var storedValue: Modifier?
private var collected = false

private weak var spezi: Spezi?


/// Access the store value.
/// - Note: You cannot access the value once it was collected.
Expand All @@ -43,12 +46,21 @@ public class _ModifierPropertyWrapper<Modifier: ViewModifier> {
public init(wrappedValue: Modifier) {
self.storedValue = wrappedValue
}

deinit {
clear()
}
}


extension _ModifierPropertyWrapper: SpeziPropertyWrapper {
func clear() {
collected = false
spezi?.handleViewModifierRemoval(for: id)
}

func inject(spezi: Spezi) {
self.spezi = spezi
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
//

import OrderedCollections
import SwiftUI

enum ModifierPlacement: Int, Comparable {
Expand All @@ -21,6 +22,9 @@ enum ModifierPlacement: Int, Comparable {
/// An adopter of this protocol is a property of a ``Module`` that provides a SwiftUI
/// [`ViewModifier`](https://developer.apple.com/documentation/swiftui/viewmodifier) to be injected into the global view hierarchy.
protocol ViewModifierProvider {
/// The persistent identifier for the view modifier provider.
var id: UUID { get }

/// The view modifier instance that should be injected into the SwiftUI view hierarchy.
///
/// Does nothing if `nil` is provided.
Expand All @@ -44,13 +48,16 @@ extension ViewModifierProvider {

extension Module {
/// All SwiftUI `ViewModifier` the module wants to modify the global view hierarchy with.
var viewModifiers: [any SwiftUI.ViewModifier] {
var viewModifierEntires: [(UUID, any SwiftUI.ViewModifier)] {
retrieveProperties(ofType: ViewModifierProvider.self)
.sorted { lhs, rhs in
lhs.placement < rhs.placement
}
.compactMap { provider in
provider.viewModifier
guard let modifier = provider.viewModifier else {
return nil
}
return (provider.id, modifier)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ public struct DependencyCollection: DependencyDeclaration {
}
}

func uninjectDependencies() {
func uninjectDependencies(notifying spezi: Spezi) {
for entry in entries {
entry.uninjectDependencies()
entry.uninjectDependencies(notifying: spezi)
}
}

Expand Down
Loading

0 comments on commit 3de8dcc

Please sign in to comment.