Skip to content

Commit 56583da

Browse files
authored
Merge pull request #7 from roanutil/structured-concurrency-release
Replace most Combine based APIs with new async versions
2 parents cc025d0 + c560229 commit 56583da

19 files changed

Lines changed: 840 additions & 1141 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ on:
1111

1212
jobs:
1313
library:
14-
runs-on: macos-11
14+
runs-on: macos-12
1515
environment: default
1616
strategy:
1717
matrix:
1818
platform: [macOS]
19-
xcode: [12.4, 12.5.1, 13.2]
20-
# Swift: 5.3, 5.4.2, 5.5.2
19+
xcode: [13.2.1, 13.4.1, '14.0']
20+
# Swift: 5.5.2, 5.6, 5.7
2121
steps:
2222
- uses: actions/checkout@v2
2323
- name: Select Xcode ${{ matrix.xcode }}

.swiftpm/xcode/xcshareddata/xcschemes/CoreDataRepository.xcscheme

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
buildConfiguration = "Debug"
5555
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
5656
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
57-
shouldUseLaunchSchemeArgsEnv = "NO">
57+
shouldUseLaunchSchemeArgsEnv = "NO"
58+
enableAddressSanitizer = "YES"
59+
enableASanStackUseAfterReturn = "YES">
5860
<CommandLineArguments>
5961
<CommandLineArgument
6062
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
@@ -72,6 +74,18 @@
7274
isEnabled = "YES">
7375
</EnvironmentVariable>
7476
</EnvironmentVariables>
77+
<AdditionalOptions>
78+
<AdditionalOption
79+
key = "NSZombieEnabled"
80+
value = "YES"
81+
isEnabled = "YES">
82+
</AdditionalOption>
83+
<AdditionalOption
84+
key = "MallocScribble"
85+
value = ""
86+
isEnabled = "YES">
87+
</AdditionalOption>
88+
</AdditionalOptions>
7589
<Testables>
7690
<TestableReference
7791
skipped = "NO"

Package.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.5
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -7,10 +7,10 @@ let package = Package(
77
name: "CoreDataRepository",
88
defaultLocalization: "en",
99
platforms: [
10-
.iOS(.v13),
11-
.macOS(.v10_15),
12-
.tvOS(.v13),
13-
.watchOS(.v6),
10+
.iOS(.v15),
11+
.macOS(.v12),
12+
.tvOS(.v15),
13+
.watchOS(.v8),
1414
],
1515
products: [
1616
.library(

README.md

Lines changed: 46 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# CoreDataRepository
2+
23
[![CI](https://github.com/roanutil/CoreDataRepository/actions/workflows/ci.yml/badge.svg)](https://github.com/roanutil/CoreDataRepository/actions/workflows/ci.yml)
34
[![codecov](https://codecov.io/gh/roanutil/CoreDataRepository/branch/main/graph/badge.svg?token=WRO4CXYWRG)](https://codecov.io/gh/roanutil/CoreDataRepository)
45
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Froanutil%2FCoreDataRepository%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/roanutil/CoreDataRepository)
56
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Froanutil%2FCoreDataRepository%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/roanutil/CoreDataRepository)
67

7-
CoreDataRepository is a reactive library (Combine) for using CoreData on a background queue. It features endpoints for CRUD, batch, fetch, and aggregate operations. Also, it offers a stream like subscription for fetch and read.
8+
CoreDataRepository is a library for using CoreData on a background queue. It features endpoints for CRUD, batch, fetch, and aggregate operations. Also, it offers a stream like subscription for fetch and read.
89

910
Since ```NSManagedObject```s are not thread safe, a value type model must exist for each ```NSMangaedObject``` subclass.
1011

@@ -14,8 +15,9 @@ Since ```NSManagedObject```s are not thread safe, a value type model must exist
1415
CoreData is a great framework for local persistence on Apple's platforms. However, it can be tempting to create strong dependencies on it throughout an app. Even worse, the `viewContext` runs on the main `DispatchQueue` along with the UI. Even fetching data from the store can be enough to cause performance problems.
1516

1617
The goals of `CoreDataRepository` are:
18+
1719
- Ease isolation of `CoreData` related code away from the rest of the app.
18-
- Improve ergonomics by providing an asynchronous API with `Combine`.
20+
- Improve ergonomics by providing an asynchronous API.
1921
- Improve usability of private contexts to relieve load from the main `DispatchQueue`.
2022
- Make local persistence with `CoreData` feel more 'Swift-like' by allowing the model layer to use value types.
2123

@@ -33,9 +35,11 @@ To give some weight to this idea, here's a quote from the Q&A portion of [this](
3335
## Basic Usage
3436

3537
### Model Bridging
38+
3639
There are two protocols that handle bridging between the value type and managed models.
3740

3841
#### RepositoryManagedModel
42+
3943
```swift
4044
@objc(RepoMovie)
4145
public final class RepoMovie: NSManagedObject {
@@ -74,7 +78,9 @@ extension RepoMovie: RepositoryManagedModel {
7478
}
7579
}
7680
```
81+
7782
#### UnmanagedModel
83+
7884
```swift
7985
public struct Movie: Hashable {
8086
public let id: UUID
@@ -106,79 +112,64 @@ extension Movie: UnmanagedModel {
106112
```
107113

108114
### CRUD
115+
109116
```swift
110117
var movie = Movie(id: UUID(), title: "The Madagascar Penguins in a Christmas Caper", releaseDate: Date(), boxOffice: 100)
111-
_ = repository.create(movie).subscribe(on: self.userInitSerialQueue)
112-
.receive(on: mainQueue)
113-
.sink(
114-
receiveCompletion: { completion in
115-
switch completion {
116-
case .finished:
117-
os_log("Successfully created new movie")
118-
case .failure:
119-
fatalError("Failed to create new movie")
120-
}
121-
},
122-
receiveValue: { result in
123-
switch result {
124-
case .create(let resultMovie):
125-
os_log("Created movie with title - \(resultMovie.title)")
126-
default:
127-
fatalError("I asked for a create operation!")
128-
}
129-
}
130-
)
118+
let result: Result<Movie, CoreDataRepositoryError> = await repository.create(movie)
119+
if case let .success(movie) = result {
120+
os_log("Created movie with title - \(movie.title)")
121+
}
131122
```
123+
132124
### Fetch
125+
133126
```swift
134127
let fetchRequest = NSFetchRequest<RepoMovie>(entityName: "RepoMovie")
135128
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \RepoMovie.title, ascending: true)]
136129
fetchRequest.predicate = NSPredicate(value: true)
137-
let result: AnyPublisher<[Movie], Error> = repository.fetch(fetchRequest)
130+
let result: Result<[Movie], CoreDataRepositoryError> = await repository.fetch(fetchRequest)
131+
if case let .success(movies) = result {
132+
os_log("Fetched \(movies.count) movies")
133+
}
134+
```
135+
136+
### Fetch Subscription
137+
138+
Similar to a regular fe:
139+
140+
```swift
141+
let result: AnyPublisher<[Movie], CoreDataRepositoryError> = repository.fetchSubscription(fetchRequest)
138142
let cancellable = result.subscribe(on: userInitSerialQueue)
139143
.receive(on: mainQueue)
140144
.sink(receiveCompletion: { completion in
141145
switch completion {
142146
case .finished:
143-
os_log("Fetched a bunch of moview")
147+
os_log("Fetched a bunch of movies")
144148
default:
145149
fatalError("Failed to fetch all the movies!")
146150
}
147151
}, receiveValue: { value in
148152
os_log("Fetched \(value.items.count) movies")
149153
})
150-
```
151-
### Fetch Subscription
152-
Similar to a regular fetch:
153-
```swift
154-
...
155-
let result: AnyPublisher<[Movie], Error> = repository.fetchSubscription(fetchRequest)
156154
...
157155
cancellable.cancel()
158156
```
159157

160158
### Aggregate
159+
161160
```swift
162-
let result: AnyPublisher<[[String: Decimal]], Error> = repository.sum(
161+
let result: Result<[[String: Decimal]], CoreDataRepositoryError> = await repository.sum(
163162
predicate: NSPredicate(value: true),
164163
entityDesc: RepoMovie.entity(),
165164
attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })!
166165
)
167-
_ = result.subscribe(on: backgroundQueue)
168-
.receive(on: mainQueue)
169-
.sink(receiveCompletion: { completion in
170-
switch completion {
171-
case .finished:
172-
os_log("Finished getting the sum all the movies' boxOffice")
173-
default:
174-
fatalError("Failed to get the sum")
175-
}
176-
}, receiveValue: { value in
177-
os_log("The sum of all movies' boxOffice is \(value.result.first!.values.first!)")
178-
})
166+
if case let .success(values) = result {
167+
os_log("The sum of all movies' boxOffice is \(values.first!.values.first!)")
168+
}
179169
```
180170

181171
### Batch
172+
182173
```swift
183174
let movies: [[String: Any]] = [
184175
["id": UUID(), "title": "A", "releaseDate": Date()],
@@ -188,39 +179,8 @@ let movies: [[String: Any]] = [
188179
["id": UUID(), "title": "E", "releaseDate": Date()]
189180
]
190181
let request = NSBatchInsertRequest(entityName: RepoMovie.entity().name!, objects: movies)
191-
_ = self.repository.insert(request)
192-
.subscribe(on: userInitSerialQueue)
193-
.receive(on: mainQueue)
194-
.sink(
195-
receiveCompletion: { completion in
196-
switch completion {
197-
case .finished:
198-
os_log("Finished inserting A LOT of movies")
199-
default:
200-
fatalError("Failed to insert a lot of movies")
201-
}
202-
},
203-
receiveValue: { value in
204-
switch value {
205-
case let .insert(_, result):
206-
switch result.resultType {
207-
case .count:
208-
if let count = result.result as? Int {
209-
os_log("Batch inserted \(count) movies!")
210-
}
211-
case .objectIDs:
212-
if let objectIDs = result.result as? [NSManagedObjectID] {
213-
os_log("Batch inserted \(objectIDs.count) movies!")
214-
}
215-
case .statusOnly:
216-
let resultIsSuccessful = result.result as? Bool ?? false
217-
os_log("Batch insert - isSuccessful = \(resultIsSuccessful)")
218-
}
219-
default:
220-
fatalError("I asked for a batch INSERT!")
221-
}
222-
}
223-
)
182+
let result: Result<NSBatchInsertResult, CoreDataRepositoryError> = await repository.insert(request)
183+
224184
```
225185

226186
#### OR
@@ -233,29 +193,20 @@ let movies: [[String: Any]] = [
233193
Movie(id: UUID(), title: "D", releaseDate: Date()),
234194
Movie(id: UUID(), title: "E", releaseDate: Date())
235195
]
236-
let publisher: AnyPublisher<(success: [Movie], failed: [Movie]), Never> = repository.create(movies)
237-
_ = publisher
238-
.subscribe(on: backgroundQueue)
239-
.receive(on: mainQueue)
240-
.sink(
241-
receiveCompletion: { completion in
242-
switch completion {
243-
case .finished:
244-
os_log("Finished inserting A LOT of movies")
245-
default:
246-
fatalError("Failed to insert a lot of movies")
247-
}
248-
},
249-
receiveValue: { createdMovies in
250-
os_log("Created these movies: \(createdMovies)")
251-
}
252-
)_
196+
let result: (success: [Movie], failed: [Movie]) = await repository.create(movies)
197+
os_log("Created these movies: \(result.success)")
198+
os_log("Failed to create these movies: \(result.failed)")
253199
```
254200

255-
256201
## TODO
257-
- Add a subscription feature for aggregate functions
258202

203+
- Add a subscription feature for aggregate functions
204+
- Migrate subscription endpoints to AsyncSequence instead of Publisher
205+
- Simplify model protocols (require only one protocol for the value type)
206+
- Allow older platform support by working around the newer variants of `NSManagedObjectContext.perform` and `NSManagedObjectContext.performAndWait`
259207

260208
## Contributing
209+
261210
I welcome any feedback or contributions. It's probably best to create an issue where any possible changes can be discussed before doing the work and creating a PR.
211+
212+
The above [TODO](#todo) section is a good place to start if you would like to contribute but don't already have a change in mind.

0 commit comments

Comments
 (0)