Skip to content

Commit

Permalink
Swift 6 compatibility (#108)
Browse files Browse the repository at this point in the history
# Swift 6 compatibility

## ♻️ Current situation & Problem
This PR fixes the swift concurrency flag. There weren't any concurrency
warnings left when compiling with the latest Swift 6 toolchain that is
part of Xcode 16 beta 3. Note, that Xcode 15 will trigger some warnings
as some symbols aren't annotated as Sendable in the previous SDK.
Further, some sendability checks changed.

This PR changes some actor isolations of some types and interfaces. Most
importantly, we add `@MainActor` isolation to the `Module/configure()`
method. We found, that a lot of modules manipulate `@MainActor` isolated
properties in their configure method (those that are relevant for UI
components) but cannot do so without spawning a new Task, as the
configure method previously didn't have `@MainActor` guarantees. Except
for out-of-band module loading, `configure()` would have always been
called on the Main Actor (through SwiftUI initialization). Making all
module interactions to be isolated to `@MainActor` greatly simplifies
some of our internal state handling.

## ⚙️ Release Notes 
* Fix swift strict concurrency flag.
* Resolve some strict concurrency warnings.

### Breaking Changes
* `Module/configure()` is now isolated to `@MainActor`.
* `Spezi/loadModule(_:ownership:)` and `Spezi/unloadModule(_:)` are now
isolated to `@MainActor`.

## 📚 Documentation
Minor documentation files.


## ✅ Testing
Test target was updated to latest concurrency changes as well.


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Jul 11, 2024
1 parent 5ada3f3 commit fffb691
Show file tree
Hide file tree
Showing 42 changed files with 272 additions and 288 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ jobs:
scheme: Spezi-Package
resultBundle: Spezi-Package-iOS.xcresult
artifactname: Spezi-Package-iOS.xcresult
buildandtest_ios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: Spezi-Package
xcodeversion: latest
swiftVersion: 6
resultBundle: Spezi-Package-iOS-Latest.xcresult
artifactname: Spezi-Package-iOS-Latest.xcresult
buildandtest_watchos:
name: Build and Test Swift Package watchOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down Expand Up @@ -69,6 +79,17 @@ jobs:
scheme: TestApp
resultBundle: TestApp-iOS.xcresult
artifactname: TestApp-iOS.xcresult
buildandtestuitests_ios_latest:
name: Build and Test UI Tests iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
xcodeversion: latest
swiftVersion: 6
resultBundle: TestApp-iOS-Latest.xcresult
artifactname: TestApp-iOS-Latest.xcresult
buildandtestuitests_visionos:
name: Build and Test UI Tests visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import class Foundation.ProcessInfo
import PackageDescription

#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import XCTRuntimeAssertions
protocol CollectionBasedProvideProperty {
func collectArrayElements<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository)

func clearValues()
func clearValues(isolated: Bool)
}


/// 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()
func clearValues(isolated: Bool)
}


Expand Down Expand Up @@ -69,7 +69,7 @@ public class _ProvidePropertyWrapper<Value> {


deinit {
clear()
clear(isolated: false)
}
}

Expand Down Expand Up @@ -143,15 +143,32 @@ extension _ProvidePropertyWrapper: StorageValueProvider {
collected = true
}

@MainActor
func clear() {
clear(isolated: true)
}

private func clear(isolated: Bool) {
collected = false

if let wrapperWithOptional = self as? OptionalBasedProvideProperty {
wrapperWithOptional.clearValues()
wrapperWithOptional.clearValues(isolated: isolated)
} else if let wrapperWithArray = self as? CollectionBasedProvideProperty {
wrapperWithArray.clearValues()
wrapperWithArray.clearValues(isolated: isolated)
} else {
performClear(isolated: isolated, of: Value.self)
}
}

private func performClear<V>(isolated: Bool, of type: V.Type) {
if isolated {
MainActor.assumeIsolated { [spezi, id] in
spezi?.handleCollectedValueRemoval(for: id, of: type)
}
} else {
spezi?.handleCollectedValueRemoval(for: id, of: Value.self)
Task { @MainActor [spezi, id] in
spezi?.handleCollectedValueRemoval(for: id, of: type)
}
}
}
}
Expand All @@ -162,8 +179,8 @@ extension _ProvidePropertyWrapper: CollectionBasedProvideProperty where Value: A
repository.setValues(for: id, storedValue.unwrappedArray)
}

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

Expand All @@ -175,8 +192,8 @@ extension _ProvidePropertyWrapper: OptionalBasedProvideProperty where Value: Any
}
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SpeziFoundation
protocol StorageValueCollector: SpeziPropertyWrapper {
/// This method is called to retrieve all the requested values from the given ``SpeziStorage`` repository.
/// - Parameter repository: Provides access to the ``SpeziStorage`` repository for read access.
@MainActor
func retrieve<Repository: SharedRepository<SpeziAnchor>>(from repository: Repository)
}

Expand All @@ -25,6 +26,7 @@ extension Module {
retrieveProperties(ofType: StorageValueCollector.self)
}

@MainActor
func injectModuleValues<Repository: SharedRepository<SpeziAnchor>>(from repository: Repository) {
for collector in storageValueCollectors {
collector.retrieve(from: repository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SpeziFoundation
protocol StorageValueProvider: SpeziPropertyWrapper {
/// This method is called to collect all provided values into the given ``SpeziStorage`` repository.
/// - Parameter repository: Provides access to the ``SpeziStorage`` repository.
@MainActor
func collect<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository)
}

Expand All @@ -25,6 +26,7 @@ extension Module {
retrieveProperties(ofType: StorageValueProvider.self)
}

@MainActor
func collectModuleValues<Repository: SharedRepository<SpeziAnchor>>(into repository: inout Repository) {
for provider in storageValueProviders {
provider.collect(into: &repository)
Expand Down
18 changes: 6 additions & 12 deletions Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,28 +138,22 @@ public protocol LifecycleHandler {
)
extension LifecycleHandler {
#if os(iOS) || os(visionOS) || os(tvOS)
// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) {}

// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func sceneWillEnterForeground(_ scene: UIScene) { }

// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func sceneDidBecomeActive(_ scene: UIScene) { }

// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func sceneWillResignActive(_ scene: UIScene) { }

// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func sceneDidEnterBackground(_ scene: UIScene) { }

// A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize.
// swiftlint:disable:next missing_docs
/// Empty default implementation.
public func applicationWillTerminate(_ application: UIApplication) { }
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SpeziFoundation
import SwiftUI


private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSource {
private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSource, Sendable {
typealias Anchor = SpeziAnchor

static let defaultValue = RemoteNotificationContinuation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public protocol EnvironmentAccessible: AnyObject, Observable {}


extension EnvironmentAccessible {
var viewModifierInitialization: any ViewModifierInitialization {
ModelModifierInitialization(model: self)
@MainActor var viewModifier: any ViewModifier {
ModelModifier(model: self)
}
}
11 changes: 8 additions & 3 deletions Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public class _ModelPropertyWrapper<Model: Observable & AnyObject> {


deinit {
clear()
// mirrors implementation of clear, however in a compiler proven, concurrency safe way (:
collected = false
Task { @MainActor [spezi, id] in
spezi?.handleViewModifierRemoval(for: id)
}
}
}

Expand All @@ -61,6 +65,7 @@ extension _ModelPropertyWrapper: SpeziPropertyWrapper {
spezi?.handleViewModifierRemoval(for: id)
}


func inject(spezi: Spezi) {
self.spezi = spezi
}
Expand Down Expand Up @@ -97,15 +102,15 @@ extension Module {


extension _ModelPropertyWrapper: ViewModifierProvider {
var viewModifierInitialization: (any ViewModifierInitialization)? {
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
}

return ModelModifierInitialization(model: storedValue)
return ModelModifier(model: storedValue)
}

var placement: ModifierPlacement {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ public class _ModifierPropertyWrapper<Modifier: ViewModifier> {
}

deinit {
clear()
// mirrors implementation of clear, however in a compiler proven, concurrency safe way (:
collected = false
Task { @MainActor [spezi, id] in
spezi?.handleViewModifierRemoval(for: id)
}
}
}

Expand Down Expand Up @@ -97,14 +101,14 @@ extension Module {


extension _ModifierPropertyWrapper: ViewModifierProvider {
var viewModifierInitialization: (any ViewModifierInitialization)? {
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
}

return WrappedViewModifier(modifier: storedValue)
return storedValue
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import OrderedCollections
import SwiftUI


enum ModifierPlacement: Int, Comparable {
case regular
case outermost
Expand All @@ -28,7 +29,7 @@ protocol ViewModifierProvider {
/// The view modifier instance that should be injected into the SwiftUI view hierarchy.
///
/// Does nothing if `nil` is provided.
var viewModifierInitialization: (any ViewModifierInitialization)? { get }
@MainActor var viewModifier: (any ViewModifier)? { get }

/// Defines the placement order of this view modifier.
///
Expand All @@ -48,13 +49,14 @@ extension ViewModifierProvider {

extension Module {
/// All SwiftUI `ViewModifier` the module wants to modify the global view hierarchy with.
var viewModifierEntires: [(UUID, any ViewModifierInitialization)] {
@MainActor
var viewModifierEntires: [(UUID, any ViewModifier)] {
retrieveProperties(ofType: ViewModifierProvider.self)
.sorted { lhs, rhs in
lhs.placement < rhs.placement
}
.compactMap { provider in
guard let modifier = provider.viewModifierInitialization else {
guard let modifier = provider.viewModifier else {
return nil
}
return (provider.id, modifier)
Expand Down
25 changes: 0 additions & 25 deletions Sources/Spezi/Capabilities/ViewModifier/WrappedViewModifier.swift

This file was deleted.

Loading

0 comments on commit fffb691

Please sign in to comment.