Skip to content

Commit 36361d4

Browse files
committed
release: lower deployment targets to iOS 13
1 parent e498f79 commit 36361d4

File tree

10 files changed

+575
-474
lines changed

10 files changed

+575
-474
lines changed

Docs/MigrationGuide.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ new extension points, and different cleanup semantics.
99

1010
Move to V2 if all of the following are true:
1111

12-
- You target iOS 17+, macOS 14+, tvOS 17+, or watchOS 10+
12+
- You target iOS 13+, macOS 10.15+, macCatalyst 13+, tvOS 13+, watchOS 6+, or visionOS 1+
1313
- You are ready to adopt Swift 6
1414
- You want actor-based internals and async hook APIs
1515
- You want Observer Hooks, Merge Hooks, and tombstone support
1616

17+
V2 now declares lower deployment targets than the initial V2 release, but current runtime
18+
validation has only been performed on iOS 15+ with modern Xcode toolchains.
19+
1720
Stay on V1 if any of the following are true:
1821

19-
- You still support older OS versions
22+
- You want to stay on a pre-Swift-6 toolchain
2023
- You are not ready to move to Swift 6
2124
- You prefer the existing V1 fetch / merge / cleaner customization model
2225

@@ -48,11 +51,15 @@ Stay on V1 if any of the following are true:
4851
### V2
4952

5053
- Swift 6
51-
- iOS 17+
52-
- macOS 14+
53-
- macCatalyst 17+
54-
- tvOS 17+
55-
- watchOS 10+
54+
- iOS 13+
55+
- macOS 10.15+
56+
- macCatalyst 13+
57+
- tvOS 13+
58+
- watchOS 6+
59+
- visionOS 1+
60+
61+
These are the declared deployment targets. In the current toolchain environment, runtime
62+
validation has only been performed on iOS 15+.
5663

5764
## Package and Dependency Changes
5865

@@ -241,7 +248,7 @@ struct MyLogger: PersistentHistoryTrackingKitLoggerProtocol {
241248

242249
## Migration Checklist
243250

244-
1. Confirm your deployment targets and Swift version meet V2 requirements.
251+
1. Confirm your deployment targets and Swift version meet V2 requirements, and note that current runtime validation has only been performed on iOS 15+.
245252
2. Replace any V1 merger or deduplicator customization with Merge Hooks.
246253
3. Add Observer Hooks where you need read-only monitoring.
247254
4. Re-evaluate `allAuthors` and `batchAuthors`.

Package.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import PackageDescription
66
let package = Package(
77
name: "PersistentHistoryTrackingKit",
88
platforms: [
9-
.iOS(.v17),
10-
.macOS(.v14),
11-
.macCatalyst(.v17),
12-
.tvOS(.v17),
13-
.watchOS(.v10),
9+
.iOS(.v13),
10+
.macOS(.v10_15),
11+
.macCatalyst(.v13),
12+
.tvOS(.v13),
13+
.watchOS(.v6),
1414
.visionOS(.v1),
1515
],
1616
products: [
@@ -20,7 +20,7 @@ let package = Package(
2020
)
2121
],
2222
dependencies: [
23-
// CoreDataEvolution - iOS 17+, Swift 6
23+
// CoreDataEvolution - Swift 6
2424
.package(
2525
url: "https://github.com/fatbobman/CoreDataEvolution.git", .upToNextMajor(from: "0.7.5"))
2626
],

README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
A modern, production-ready library for handling Core Data's Persistent History Tracking with full Swift 6 concurrency support.
66

7-
![Platform](https://img.shields.io/badge/Platform-iOS%2017%2B%20|%20macOS%2014%2B%20|%20tvOS%2017%2B%20|%20watchOS%2010%2B-blue)
7+
![Platform](https://img.shields.io/badge/Platform-iOS%2013%2B%20%7C%20macOS%2010.15%2B%20%7C%20macCatalyst%2013%2B%20%7C%20tvOS%2013%2B%20%7C%20watchOS%206%2B%20%7C%20visionOS%201%2B-blue)
88
![Swift](https://img.shields.io/badge/Swift-6.0-orange)
99
![License](https://img.shields.io/badge/License-MIT-green)[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fatbobman/ObservableDefaults)
1010

@@ -23,8 +23,9 @@ Version 2 is a **complete rewrite** with modern Swift concurrency:
2323
-**Hook System** - Powerful Observer and Merge Hooks for custom behaviors
2424
-**Modern API** - Async/await throughout, UUID-based hook management
2525

26-
**Migration from V1:** V2 requires iOS 17+, macOS 14+, and Swift 6. See the
27-
[Migration Guide](Docs/MigrationGuide.md) for migration steps and behavior changes.
26+
**Migration from V1:** V2 declares iOS 13+, macOS 10.15+, macCatalyst 13+, tvOS 13+,
27+
watchOS 6+, visionOS 1+, and Swift 6. Current runtime validation has been performed on iOS 15+.
28+
See the [Migration Guide](Docs/MigrationGuide.md) for migration steps and behavior changes.
2829

2930
---
3031

@@ -54,19 +55,20 @@ When you enable Persistent History Tracking, Core Data creates **transactions**
5455

5556
### V2 (Current Branch)
5657

57-
- **Minimum Requirements**: iOS 17+, macOS 14+, Swift 6.0+
58+
- **Minimum Requirements**: iOS 13+, macOS 10.15+, macCatalyst 13+, tvOS 13+, watchOS 6+, visionOS 1+, Swift 6.0+
5859
- **Features**: Actor-based architecture, Hook system, full Swift 6 concurrency
59-
- **Recommended for**: New projects targeting modern platforms
60+
- **Runtime Validation**: Currently tested on iOS 15+ with current Xcode toolchains
61+
- **Recommended for**: New projects adopting Swift 6
6062

6163
### V1 (Stable)
6264

6365
- **Minimum Requirements**: iOS 13+, macOS 10.15+, Swift 5.5+
6466
- **Features**: Proven stability, lower system requirements
65-
- **Recommended for**: Projects that need to support older platforms
67+
- **Recommended for**: Projects that prefer a pre-Swift-6 toolchain or the battle-tested V1 API
6668

6769
**Use V1 if:**
6870

69-
- You need to support iOS 13-16 or macOS 10.15-13
71+
- You need to stay on a pre-Swift-6 toolchain
7072
- You're not ready to migrate to Swift 6
7173
- You prefer the battle-tested V1 API
7274

@@ -487,10 +489,13 @@ let hookB = await kit.registerMergeHook(before: hookA) { _ in
487489

488490
## Requirements
489491

490-
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+
492+
- iOS 13.0+ / macOS 10.15+ / macCatalyst 13.0+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+
491493
- Swift 6.0+
492494
- Xcode 16.0+
493495

496+
Runtime validation in the current toolchain environment has been performed on iOS 15+.
497+
Older declared deployment targets are compiler-checked but have not been runtime-validated here.
498+
494499
---
495500

496501
## Documentation
@@ -505,6 +510,9 @@ let hookB = await kit.registerMergeHook(before: hookA) { _ in
505510

506511
Tests are validated under parallel execution. The test infrastructure serializes `NSPersistentContainer` creation internally to avoid Core Data store-loading crashes while preserving parallel suite execution.
507512

513+
Current runtime validation has been performed on iOS 15+.
514+
Although the package declares support for older OS versions, iOS 13 and iOS 14 have not been runtime-validated in the current Xcode environment.
515+
508516
### Recommended: Use the test script
509517

510518
```bash

Sources/PersistentHistoryTrackingKit/PersistentHistoryTrackingKit.swift

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ public final class PersistentHistoryTrackingKit: @unchecked Sendable {
5353
/// Background processing task.
5454
private var processingTask: Task<Void, Never>?
5555

56+
/// Legacy notification observer used on platforms without async notification sequences.
57+
private var remoteChangeObserver: NSObjectProtocol?
58+
59+
/// Protects listener state shared across notification callbacks.
60+
private let listenerStateLock = NSLock()
61+
62+
/// Identifier for the currently active notification listener.
63+
private var activeListenerID: UUID?
64+
5665
/// Persistent container (used to build manual cleaners).
5766
private let container: NSPersistentContainer
5867

@@ -295,32 +304,31 @@ public final class PersistentHistoryTrackingKit: @unchecked Sendable {
295304

296305
/// Start the background processing task.
297306
public func start() {
298-
guard processingTask == nil else { return }
299-
300-
processingTask = Task { @Sendable [weak self, coordinator] in
301-
guard let self else { return }
302-
303-
log(.info, level: 1, "Persistent History Tracking Kit V2 Started")
304-
305-
// Listen for NSPersistentStoreRemoteChange notifications.
306-
let center = NotificationCenter.default
307-
let name = NSNotification.Name.NSPersistentStoreRemoteChange
308-
309-
for await _ in center.notifications(named: name, object: coordinator)
310-
where !Task.isCancelled {
311-
await self.handleRemoteChangeNotification()
312-
}
307+
guard processingTask == nil, remoteChangeObserver == nil else { return }
308+
let listenerID = activateNotificationListener()
313309

314-
log(.info, level: 1, "Persistent History Tracking Kit V2 Stopped")
310+
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, macCatalyst 15.0, visionOS 1.0, *) {
311+
startAsyncNotificationListener(listenerID: listenerID)
312+
} else {
313+
startLegacyNotificationObserver(listenerID: listenerID)
315314
}
316315

316+
log(.info, level: 1, "Persistent History Tracking Kit V2 Started")
317317
log(.info, level: 2, "Started transaction processing task")
318318
}
319319

320320
/// Stop the background processing task.
321321
public func stop() {
322+
guard processingTask != nil || remoteChangeObserver != nil else { return }
323+
deactivateNotificationListener()
324+
322325
processingTask?.cancel()
323326
processingTask = nil
327+
if let remoteChangeObserver {
328+
NotificationCenter.default.removeObserver(remoteChangeObserver)
329+
self.remoteChangeObserver = nil
330+
}
331+
log(.info, level: 1, "Persistent History Tracking Kit V2 Stopped")
324332
log(.info, level: 2, "Stopped transaction processing task")
325333
}
326334

@@ -360,4 +368,54 @@ public final class PersistentHistoryTrackingKit: @unchecked Sendable {
360368
guard level <= _logLevel else { return }
361369
logger.log(type: type, message: message)
362370
}
371+
372+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, macCatalyst 15.0, visionOS 1.0, *)
373+
private func startAsyncNotificationListener(listenerID: UUID) {
374+
processingTask = Task { @Sendable [weak self, coordinator] in
375+
guard let self else { return }
376+
377+
let center = NotificationCenter.default
378+
let name = NSNotification.Name.NSPersistentStoreRemoteChange
379+
380+
for await _ in center.notifications(named: name, object: coordinator)
381+
where !Task.isCancelled {
382+
guard isActiveNotificationListener(listenerID) else { return }
383+
await self.handleRemoteChangeNotification()
384+
}
385+
}
386+
}
387+
388+
private func startLegacyNotificationObserver(listenerID: UUID) {
389+
remoteChangeObserver = NotificationCenter.default.addObserver(
390+
forName: .NSPersistentStoreRemoteChange,
391+
object: coordinator,
392+
queue: nil
393+
) { [weak self] _ in
394+
Task { @Sendable [weak self] in
395+
guard let self, self.isActiveNotificationListener(listenerID) else { return }
396+
await self.handleRemoteChangeNotification()
397+
}
398+
}
399+
}
400+
401+
private func activateNotificationListener() -> UUID {
402+
listenerStateLock.lock()
403+
defer { listenerStateLock.unlock() }
404+
405+
let listenerID = UUID()
406+
activeListenerID = listenerID
407+
return listenerID
408+
}
409+
410+
private func deactivateNotificationListener() {
411+
listenerStateLock.lock()
412+
defer { listenerStateLock.unlock() }
413+
activeListenerID = nil
414+
}
415+
416+
private func isActiveNotificationListener(_ listenerID: UUID) -> Bool {
417+
listenerStateLock.lock()
418+
defer { listenerStateLock.unlock() }
419+
return activeListenerID == listenerID
420+
}
363421
}

Tests/PersistentHistoryTrackingKitTests/ConcurrencyTests.swift

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,6 @@ import Testing
1212

1313
@Suite("Concurrency Safety Tests")
1414
struct ConcurrencyTests {
15-
@Test("Multithreaded concurrent writes")
16-
func concurrentWrites() async throws {
17-
let container = TestModelBuilder.createContainer(author: "App1")
18-
19-
await withTaskGroup(of: Void.self) { group in
20-
for i in 0..<5 {
21-
group.addTask {
22-
let handler = TestAppDataHandler(
23-
container: container,
24-
viewName: "Writer\(i)")
25-
26-
do {
27-
_ = try await handler.createPerson(
28-
name: "Person\(i)",
29-
age: Int32(20 + i),
30-
author: "App\(i)")
31-
} catch {
32-
Issue.record("Failed to save in concurrent write: \(error)")
33-
}
34-
}
35-
}
36-
}
37-
38-
let reader = TestAppDataHandler(container: container, viewName: "Reader")
39-
let count = try await reader.personCount()
40-
#expect(count == 5)
41-
}
42-
4315
@Test("Multiple actors accessing concurrently")
4416
func multipleActorsConcurrentAccess() async throws {
4517
let container = TestModelBuilder.createContainer(author: "App1")

0 commit comments

Comments
 (0)