Skip to content

initial iOS 27 adjustments - #5

Open
lukaskollmer wants to merge 7 commits into
mainfrom
lukas/ios-27
Open

initial iOS 27 adjustments#5
lukaskollmer wants to merge 7 commits into
mainfrom
lukas/ios-27

Conversation

@lukaskollmer

@lukaskollmer lukaskollmer commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

initial iOS 27 adjustments

♻️ Current situation & Problem

Xcode 27 and the iOS/macOS/etc 27 SDKs are introducing some changes that affect various parts of Spezi.
ths purpose of this PR is to make a bunch of small changes to ensure that Spezi works well with Xcode/iOS/etc 27.

⚙️ Release Notes

  • initial iOS 27 compatibility:
    • fixed a bug where ManagedNavigationStack and LocalPreference would not work correctly when compiled using the iOS 27 SDK
    • fixed a bunch of compiler warnings

📚 Documentation

n/a

✅ Testing

n/a. we will continue to test on iOS 26 for the time being, and will switch the CI to 27 down the road.

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability across account, device, and password-reset flows by handling cancellations and sleep interruptions more gracefully.
    • Added better error handling for speech recognition and HealthKit background delivery.
    • Expanded Bluetooth event handling to cover newly discovered accessories.
    • Improved navigation and markdown image loading reliability.
    • Updated study and preference handling to work more consistently in SwiftUI and background workflows.

@lukaskollmer lukaskollmer self-assigned this Jun 30, 2026
@lukaskollmer lukaskollmer added the enhancement New feature or request label Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR applies a broad set of independent correctness and concurrency fixes across Spezi modules: existential type lookups, @MainActor tightening, weak self capture in a Bluetooth subscription, CancellationError suppression and try? sleep changes, HealthKit background delivery reworked to a semaphore-gated single-type API, LocalPreference context-aware observer selection, StudyManager configure() refactor with error logging, and minor view/import/visibility cleanups.

Changes

Cross-module fixes and improvements

Layer / File(s) Summary
LocalPreference observer context and HealthKit background delivery rework
Sources/SpeziFoundation/LocalPreferences/LocalPreference.swift, Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift, Sources/SpeziHealthKit/Health Data Collection/HealthKitSampleCollector.swift
LocalPreference adds a UsageContext enum and a computed observer switching between external and SwiftUI @State observers, with update() now mutating; HKHealthStore background delivery moves to a single-HKSampleType/HKObjectType API backed by an AsyncSemaphore-gated refcount map with rollback on failure, and callers pass single sample types with catch-based error logging.
Concurrency safety fixes
Sources/SpeziBluetooth/CoreBluetooth/Model/BluetoothManagerStorage.swift, Sources/SpeziAccount/Mock/InMemoryAccountService.swift, Sources/SpeziAccount/Mock/InMemoryAccountStorageProvider.swift, Sources/SpeziAccount/Views/PasswordResetView.swift, Sources/SpeziDevices/Testing/MockDevice.swift, Sources/SpeziChat/MessageInputView.swift
BluetoothManagerStorage captures self weakly in its stream-start task; InMemoryAccountService suppresses CancellationError; several call sites replace throwing Task.sleep with try?; MessageInputView logs speech recognition errors.
Existential type, actor annotation, and visibility fixes
Sources/Spezi/Spezi/SpeziPropertyWrapper.swift, Sources/SpeziAccount/AccountService/IdentityProvider/SecurityRelatedModifier.swift, Sources/SpeziStudyDefinition/StudyBundle/Validation/StudyBundle+QuestionnaireValidation.swift
Property wrapper lookups use (any SpeziPropertyWrapper).self; SecurityRelatedModifier init requires @MainActor on the wrapped closure; a Path dynamicMember subscript becomes public.
StudyManager configure refactor
Sources/SpeziStudy/Study Manager/StudyManager.swift, Sources/SpeziStudy/Study Manager/StudyManager+Other.swift
configure() delegates to a new _configure() run on the main actor with error logging; autosave and listener tasks use Swift::Task; fixTaskContextAndDuplicateVersions() failures are caught and logged.
View lifecycle, imports, and minor cleanups
Sources/SpeziViews/Views/ManagedNavigationStack/ManagedNavigationStack.swift, Sources/SpeziViews/Views/Text/MarkdownView+ImageProviders.swift, Sources/SpeziContact/Contact Views/ContactView.swift, Sources/SpeziContact/Models/ContactOption.swift, Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift, Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift, Sources/SpeziAccountPhoneNumbers/DataEntry/OTCEntryView.swift, Sources/SpeziLocalStorage/LocalStorageKey.swift, Sources/SpeziBluetooth/AccessorySetupKit/*
ManagedNavigationStack inlines path configuration into .onAppear and removes a helper; ImageLoadingView gets an explicit nonisolated init and private url; MessageUI imports move from implementation-only to private; several files add imports; LocalStorageKey simplifies lock-call syntax; a new .accessoryDiscovered case is handled in two Bluetooth switch statements.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant HKHealthStore as "HKHealthStore+BackgroundDelivery"
  participant Semaphore as "AsyncSemaphore"
  participant HealthKit as "HealthKit API"

  Caller->>HKHealthStore: startBackgroundDelivery(for: sampleType)
  HKHealthStore->>Semaphore: wait
  HKHealthStore->>HKHealthStore: enableBackgroundDelivery(for: objectType)
  alt count was 0
    HKHealthStore->>HealthKit: enableBackgroundDelivery
  end
  HKHealthStore->>Semaphore: signal
  alt enable failed
    HKHealthStore->>HKHealthStore: disableBackgroundDelivery(rollback)
    HKHealthStore-->>Caller: throw error
  else success
    HKHealthStore->>HealthKit: execute observer query
    HKHealthStore-->>Caller: return
  end
Loading

Suggested labels: bug

Suggested reviewers: PSchmiedmayer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the broad compatibility-focused changes for iOS/Xcode 27, though it is high-level rather than specific.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lukas/ios-27

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PSchmiedmayer PSchmiedmayer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the improvements; good to merge as long as all the CI is passing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
Sources/SpeziChat/MessageInputView.swift (1)

210-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a structured Logger over print() for the speech-recognition error.

Catching and surfacing the error is a good addition, but print() output isn't filterable/searchable like OSLog-based logging used elsewhere in Spezi modules.

♻️ Suggested direction
-                } catch {
-                    print("Speech recognition error: \(error)")
-                }
+                } catch {
+                    logger.error("Speech recognition error: \(error)")
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziChat/MessageInputView.swift` around lines 210 - 223, Replace the
speech-recognition error print in MessageInputView’s Task/do-catch block with
the module’s structured Logger approach used elsewhere in Spezi. Keep the
existing catch in the speechRecognizer.start() flow, but log the caught error
through a Logger instance instead of print(), using a clear message that
includes the error details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/SpeziHealthKit/Health` Data
Collection/HealthKitSampleCollector.swift:
- Around line 137-141: The stop path in HealthKitSampleCollector should mirror
startBackgroundDelivery(for:) by disabling every effective sample type that was
enabled, not just sampleType.hkSampleType. Update the cleanup logic in the same
collector flow to iterate over
sampleType.hkSampleType.effectiveObjectTypesForAuthorization and call
disableBackgroundDelivery for each descriptor. Keep the existing error logging
in HealthKitSampleCollector, but make sure it runs per disabled type so expanded
types are fully turned off.

In `@Sources/SpeziStudy/Study` Manager/StudyManager.swift:
- Around line 168-177: The autosave loop in StudyManager.autosaveTask is
retaining self and never exits cleanly. Update the Swift::Task closure to
capture self weakly and guard it before each save, and add a cancellation check
inside the while loop so it stops when the task is cancelled. Also handle
Task.sleep in a way that does not swallow cancellation, using the existing
modelContext.save() path and the autosaveTask property as the main points to
adjust.
- Around line 149-157: The task launches in StudyManager.configure and the other
affected methods use invalid Swift syntax, so replace every Swift::Task
reference with Swift.Task throughout StudyManager.swift. Update the calls in
configure and the other async entry points that use the same typo so they
compile correctly, keeping the existing `@MainActor` and error-handling behavior
intact.

---

Nitpick comments:
In `@Sources/SpeziChat/MessageInputView.swift`:
- Around line 210-223: Replace the speech-recognition error print in
MessageInputView’s Task/do-catch block with the module’s structured Logger
approach used elsewhere in Spezi. Keep the existing catch in the
speechRecognizer.start() flow, but log the caught error through a Logger
instance instead of print(), using a clear message that includes the error
details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 21766938-d010-4fae-9bbd-c48c21feb5c1

📥 Commits

Reviewing files that changed from the base of the PR and between 3191256 and 5b75d96.

📒 Files selected for processing (24)
  • Sources/Spezi/Spezi/SpeziPropertyWrapper.swift
  • Sources/SpeziAccount/AccountService/IdentityProvider/SecurityRelatedModifier.swift
  • Sources/SpeziAccount/Mock/InMemoryAccountService.swift
  • Sources/SpeziAccount/Mock/InMemoryAccountStorageProvider.swift
  • Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift
  • Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift
  • Sources/SpeziAccount/Views/PasswordResetView.swift
  • Sources/SpeziAccountPhoneNumbers/DataEntry/OTCEntryView.swift
  • Sources/SpeziBluetooth/AccessorySetupKit/ASAccessoryEventType+Description.swift
  • Sources/SpeziBluetooth/AccessorySetupKit/AccessorySetupKit.swift
  • Sources/SpeziBluetooth/CoreBluetooth/Model/BluetoothManagerStorage.swift
  • Sources/SpeziChat/MessageInputView.swift
  • Sources/SpeziContact/Contact Views/ContactView.swift
  • Sources/SpeziContact/Models/ContactOption.swift
  • Sources/SpeziDevices/Testing/MockDevice.swift
  • Sources/SpeziFoundation/LocalPreferences/LocalPreference.swift
  • Sources/SpeziHealthKit/Health Data Collection/HealthKitSampleCollector.swift
  • Sources/SpeziHealthKit/HealthKit Extensions/HKHealthStore+BackgroundDelivery.swift
  • Sources/SpeziLocalStorage/LocalStorageKey.swift
  • Sources/SpeziStudy/Study Manager/StudyManager+Other.swift
  • Sources/SpeziStudy/Study Manager/StudyManager.swift
  • Sources/SpeziStudyDefinition/StudyBundle/Validation/StudyBundle+QuestionnaireValidation.swift
  • Sources/SpeziViews/Views/ManagedNavigationStack/ManagedNavigationStack.swift
  • Sources/SpeziViews/Views/Text/MarkdownView+ImageProviders.swift

Comment on lines +137 to +141
do {
try await healthStore.disableBackgroundDelivery(for: sampleType.hkSampleType)
} catch {
healthKit.logger.error("Failed to stop data collection for sample collector for \(self.sampleType.id): \(error)")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Disable the same effective sample types that were enabled.

startBackgroundDelivery(for:) enables every descriptor from sampleType.hkSampleType.effectiveObjectTypesForAuthorization, but stop only disables sampleType.hkSampleType. Expanded types can remain enabled and keep stale observer counts.

🐛 Proposed fix
             invalidator.invalidate()
-            do {
-                try await healthStore.disableBackgroundDelivery(for: sampleType.hkSampleType)
-            } catch {
-                healthKit.logger.error("Failed to stop data collection for sample collector for \(self.sampleType.id): \(error)")
+            let backgroundDeliveryTypes = sampleType.hkSampleType
+                .effectiveObjectTypesForAuthorization
+                .compactMap { $0 as? HKSampleType }
+            for backgroundDeliveryType in backgroundDeliveryTypes {
+                do {
+                    try await healthStore.disableBackgroundDelivery(for: backgroundDeliveryType)
+                } catch {
+                    healthKit.logger.error("Failed to stop data collection for sample collector for \(self.sampleType.id) and sample type \(backgroundDeliveryType): \(error)")
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
do {
try await healthStore.disableBackgroundDelivery(for: sampleType.hkSampleType)
} catch {
healthKit.logger.error("Failed to stop data collection for sample collector for \(self.sampleType.id): \(error)")
}
invalidator.invalidate()
let backgroundDeliveryTypes = sampleType.hkSampleType
.effectiveObjectTypesForAuthorization
.compactMap { $0 as? HKSampleType }
for backgroundDeliveryType in backgroundDeliveryTypes {
do {
try await healthStore.disableBackgroundDelivery(for: backgroundDeliveryType)
} catch {
healthKit.logger.error("Failed to stop data collection for sample collector for \(self.sampleType.id) and sample type \(backgroundDeliveryType): \(error)")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziHealthKit/Health` Data Collection/HealthKitSampleCollector.swift
around lines 137 - 141, The stop path in HealthKitSampleCollector should mirror
startBackgroundDelivery(for:) by disabling every effective sample type that was
enabled, not just sampleType.hkSampleType. Update the cleanup logic in the same
collector flow to iterate over
sampleType.hkSampleType.effectiveObjectTypesForAuthorization and call
disableBackgroundDelivery for each descriptor. Keep the existing error logging
in HealthKitSampleCollector, but make sure it runs per disabled type so expanded
types are fully turned off.

Comment on lines +149 to +157
public func configure() {
Swift::Task { @MainActor in
do {
try await _configure()
} catch {
logger.error("configure failed: \(error)")
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'Swift::Task|Swift\.Task|_Concurrency\.Task|\bTask\s*\{|\bTask\.sleep' "Sources/SpeziStudy/Study Manager/StudyManager.swift"

Repository: SchmiedmayerLab/Spezi

Length of output: 492


🏁 Script executed:

#!/bin/bash
sed -n '140,210p' "Sources/SpeziStudy/Study Manager/StudyManager.swift"

Repository: SchmiedmayerLab/Spezi

Length of output: 2828


Replace Swift::Task with Swift.Task in Sources/SpeziStudy/Study Manager/StudyManager.swift :: is not valid Swift syntax, so these task launches won’t compile as written. The same typo appears at lines 150, 169, 174, 191, and 203.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziStudy/Study` Manager/StudyManager.swift around lines 149 - 157,
The task launches in StudyManager.configure and the other affected methods use
invalid Swift syntax, so replace every Swift::Task reference with Swift.Task
throughout StudyManager.swift. Update the calls in configure and the other async
entry points that use the same typo so they compile correctly, keeping the
existing `@MainActor` and error-handling behavior intact.

Comment on lines +168 to +177
if autosaveTask == nil {
autosaveTask = Swift::Task {
while true {
await MainActor.run {
try? self.modelContext.save()
}
try? await Swift::Task.sleep(for: .seconds(0.25))
}
}
#endif
outcomesObserverToken = scheduler.observeNewOutcomes { [weak self] outcome in
guard let self,
let studyContext = outcome.task.studyContext,
let studyBundle = self.studyEnrollments.first(where: { $0.studyId == studyContext.studyId })?.studyBundle else {
return
}
self.handleStudyLifecycleEvent(
.completedTask(componentId: studyContext.componentId),
for: studyBundle,
at: .now
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Autosave task leaks self and never stops.

The closure has no [weak self], so it strongly captures self, which is itself stored on self.autosaveTask — a retain cycle. Additionally, try? await Task.sleep(...) discards CancellationError, so the while true loop keeps running even if the task is cancelled; there's no Task.isCancelled check either. Simulator-only, but the StudyManager instance and this loop will run indefinitely for the process lifetime.

🔧 Proposed fix
         if autosaveTask == nil {
-            autosaveTask = Swift::Task {
-                while true {
+            autosaveTask = Task { [weak self] in
+                while let self, !Task.isCancelled {
                     await MainActor.run {
                         try? self.modelContext.save()
                     }
-                    try? await Swift::Task.sleep(for: .seconds(0.25))
+                    try? await Task.sleep(for: .seconds(0.25))
                 }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if autosaveTask == nil {
autosaveTask = Swift::Task {
while true {
await MainActor.run {
try? self.modelContext.save()
}
try? await Swift::Task.sleep(for: .seconds(0.25))
}
}
#endif
outcomesObserverToken = scheduler.observeNewOutcomes { [weak self] outcome in
guard let self,
let studyContext = outcome.task.studyContext,
let studyBundle = self.studyEnrollments.first(where: { $0.studyId == studyContext.studyId })?.studyBundle else {
return
}
self.handleStudyLifecycleEvent(
.completedTask(componentId: studyContext.componentId),
for: studyBundle,
at: .now
)
}
if autosaveTask == nil {
autosaveTask = Task { [weak self] in
while let self, !Task.isCancelled {
await MainActor.run {
try? self.modelContext.save()
}
try? await Task.sleep(for: .seconds(0.25))
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziStudy/Study` Manager/StudyManager.swift around lines 168 - 177,
The autosave loop in StudyManager.autosaveTask is retaining self and never exits
cleanly. Update the Swift::Task closure to capture self weakly and guard it
before each save, and add a cancellation check inside the while loop so it stops
when the task is cancelled. Also handle Task.sleep in a way that does not
swallow cancellation, using the existing modelContext.save() path and the
autosaveTask property as the main points to adjust.

@lukaskollmer lukaskollmer mentioned this pull request Jul 13, 2026
1 task
lukaskollmer added a commit that referenced this pull request Jul 14, 2026
### ♻️ Current situation & Problem
@PSchmiedmayer had a stab at various package-wide improvements and fixes
in #17, such as addressing warnings, deprecations, etc. this work also
partly overlaps with some things addressed in #5 (which probably will
end up getting superseded by this PR).
this PR picks up these adjustments (which initially were developed
against #16), and carries them over into the current #20-based main
branch state of the codebase.

somewhat complete list of changes:
- most of what was covered by #17
- address compiler warnings where possible
- address deprecation warnings where possible
- note that this PR intentionally does **not** address the firebase
`updateEmail(to:)` deprecation, as there currently are some issues
around the behaviour of the new API. see also #14
- some testing fixes that were left out of #20
- now that the package's effective availability is iOS 18+, we can
remove a bunch of `if #available(iOS 18, *)` checks from all over the
codebase
- *todo*


### ⚙️ Release Notes
*todo*


### 📚 Documentation
*todo*


### ✅ Testing
*todo*


### Code of Conduct & Contributing Guidelines
By creating and submitting this pull request, you agree to follow our
[Code of
Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md).
@PSchmiedmayer

Copy link
Copy Markdown
Contributor

@lukaskollmer I think most of that should no longer be relevant with the latest betas and setups?

@lukaskollmer

lukaskollmer commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@PSchmiedmayer likely yes, plus also some of the things i think we've already covered in recent other PRs


update: still relevant; will revisit soon, bring in sync with main, and then merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants