Skip to content

Commit afedd4b

Browse files
authored
Update README.md
1 parent f00ddc1 commit afedd4b

1 file changed

Lines changed: 200 additions & 1 deletion

File tree

README.md

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,202 @@
11
# CoreDataRepository
22

3-
A description of this package.
3+
CoreDataRepository is a reactive library (Combine) for using CoreData on a background queue. It features endpoints for CRUD, batch, fetch multiple, and aggregate operations. Also, it offers a stream like subscription function for wrapping a fetch multiple call that will send updates that match the fetch request.
4+
5+
Since ```NSManagedObject```s are not thread safe, a value type model must exist for each ```NSMangaedObject``` subclass.
6+
7+
## Why the hell did you make this?
8+
When I started learning more about application architecture, I ran into things like Clean Architecture that insist that the models, business logic, and views should be far away from platform specific frameworks. Your view should have no concern over the implementation details of persistence. When I compared that to how things are usually done on iOS, I noticed a big difference.
9+
10+
After some time passed I came across Composable Architecture which is a Swift library and seemingly meant for iOS. I was really confused how anybody could take it seriously since all of the app state is value types and the Apple frameworks are object oriented. Finally I found somebody discussing CoreData and ComposableArchitecture on the Swift Forums and they seemed to be mapping NSManagedObjects to structs which seemed insane but clever. After reading that, I did my best to suppress my inner rage at the inefficiency of it all and got to work.
11+
12+
The result is this library which in some form is actually used in production for my app. Going forward, when given the choice, I will always use this library rather than the old way. NSManagedObjects can be tricky. Fetching any real number of them on the main queue freezes the UI.
13+
14+
To give some weight to this idea, here's a quote from the Q&A portion of [this](https://academy.realm.io/posts/andy-matuschak-controlling-complexity/) talk by Andy Matuschak:
15+
16+
"Q: How do dependencies work out? It seems like the greatest value of using values is in the model layer, yet that’s the layer at which you have the most dependencies across the rest of your app, which is probably in Objective-C.
17+
18+
Andy: In my experience, we had a CoreData stack, which is the opposite of isolation. Our strategy was putting a layer about the CoreData layer that would perform queries and return values. But where would we add functionality in the model layer? As far as using values in the view layer, we do a lot of that actually. We have a table view cell all the way down the stack that will render some icon and a label. The traditional thing to do would be to pass the ManagedObject for that content to the cell, but it doesn’t need that. There’s no reason to create this dependency between the cell and everything the model knows about, and so we make these lightweight little value types that the view needs. The owner of the view can populate that value type and give it to the view. We make these things called presenters that given some model can compute the view data. Then the thing which owns the presenter can pass the results into the view."
19+
20+
## Basic Usage
21+
22+
### Model Bridging
23+
There are two protocols that handle briding between the value type and managed models.
24+
25+
#### RepositoryManagedModel
26+
```swift
27+
@objc(RepoMovie)
28+
final class RepoMovie: NSManagedObject {
29+
@NSManaged var id: UUID
30+
@NSManaged var title: String
31+
@NSManaged var releaseDate: Date
32+
@NSManaged var boxOffice: NSDecimalNumber
33+
}
34+
35+
extension RepoMovie: RepositoryManagedModel {
36+
var asUnmanaged: Movie {
37+
return Movie(
38+
id: id,
39+
title: title,
40+
releaseDate: releaseDate,
41+
boxOffice: boxOffice as Decimal,
42+
objectID: objectID
43+
)
44+
}
45+
46+
func update(from unmanaged: Movie) {
47+
self.id = unmanaged.id
48+
self.title = unmanaged.title
49+
self.releaseDate = unmanaged.releaseDate
50+
self.boxOffice = unmanaged.boxOffice as NSDecimalNumber
51+
}
52+
53+
static func fetchRequest() -> NSFetchRequest<RepoMovie> {
54+
NSFetchRequest<RepoMovie>(entityName: "RepoMovie")
55+
}
56+
}
57+
```
58+
#### UnmanagedModel
59+
```swift
60+
public struct Movie {
61+
public let id: UUID
62+
public var title: String = ""
63+
public var releaseDate: Date
64+
public var boxOffice: Decimal = 0
65+
public var objectID: NSManagedObjectID?
66+
}
67+
68+
extension Movie: UnmanagedModel {
69+
public func asRepoManaged(in context: NSManagedObjectContext) -> RepoMovie {
70+
let object = RepoMovie(context: context)
71+
object.id = id
72+
object.title = title
73+
object.releaseDate = releaseDate
74+
object.boxOffice = boxOffice as NSDecimalNumber
75+
return object
76+
}
77+
}
78+
```
79+
80+
### CRUD
81+
```swift
82+
var movie = Movie(id: UUID(), title: "The Madagascar Penguins in a Christmas Caper", releaseDate: Date(), boxOffice: 100)
83+
_ = repository.create(movie).subscribe(on: self.userInitSerialQueue)
84+
.receive(on: mainQueue)
85+
.sink(
86+
receiveCompletion: { completion in
87+
switch completion {
88+
case .finished:
89+
os_log("Successfully created new movie")
90+
case .failure:
91+
fatalError("Failed to create new movie")
92+
}
93+
},
94+
receiveValue: { result in
95+
switch result {
96+
case .create(let resultMovie):
97+
os_log("Created movie with title - \(resultMovie.title)")
98+
default:
99+
fatalError("I asked for a create operation!")
100+
}
101+
}
102+
)
103+
```
104+
### Fetch
105+
```swift
106+
let fetchRequest = NSFetchRequest<RepoMovie>(entityName: "RepoMovie")
107+
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \RepoMovie.title, ascending: true)]
108+
fetchRequest.predicate = NSPredicate(value: true)
109+
let result: AnyPublisher<Success, Failure> = repository.fetch(fetchRequest)
110+
let cancellable = result.subscribe(on: userInitSerialQueue)
111+
.receive(on: mainQueue)
112+
.sink(receiveCompletion: { completion in
113+
switch completion {
114+
case .finished:
115+
os_log("Fetched a bunch of moview")
116+
default:
117+
fatalError("Failed to fetch all the movies!")
118+
}
119+
}, receiveValue: { value in
120+
os_log("Fetched \(value.items.count) movies")
121+
})
122+
```
123+
### Fetch Subscription
124+
Similar to a regular fetch:
125+
```swift
126+
...
127+
let result: AnyPublisher<Success, Failure> = repository.fetch(fetchRequest).subscription(repository)
128+
...
129+
cancellable.cancel()
130+
```
131+
132+
### Aggregate
133+
```swift
134+
let result: AnyPublisher<Success<Decimal>, Failure> = repository.sum(
135+
predicate: NSPredicate(value: true),
136+
entityDesc: RepoMovie.entity(),
137+
attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })!
138+
)
139+
_ = result.subscribe(on: backgroundQueue)
140+
.receive(on: mainQueue)
141+
.sink(receiveCompletion: { completion in
142+
switch completion {
143+
case .finished:
144+
os_log("Finished getting the sum all the movies' boxOffice")
145+
default:
146+
fatalError("Failed to get the sum")
147+
}
148+
}, receiveValue: { value in
149+
os_log("The sum of all movies' boxOffice is \(value.result.first!.values.first!)")
150+
})
151+
```
152+
153+
### Batch
154+
```swift
155+
let movies: [[String: Any]] = [
156+
["id": UUID(), "title": "A", "releaseDate": Date()],
157+
["id": UUID(), "title": "B", "releaseDate": Date()],
158+
["id": UUID(), "title": "C", "releaseDate": Date()],
159+
["id": UUID(), "title": "D", "releaseDate": Date()],
160+
["id": UUID(), "title": "E", "releaseDate": Date()]
161+
]
162+
let request = NSBatchInsertRequest(entityName: RepoMovie.entity().name!, objects: movies)
163+
_ = self.repository.insert(request)
164+
.subscribe(on: userInitSerialQueue)
165+
.receive(on: mainQueue)
166+
.sink(
167+
receiveCompletion: { completion in
168+
switch completion {
169+
case .finished:
170+
os_log("Finished inserting A LOT of movies")
171+
default:
172+
fatalError("Failed to insert a lot of movies")
173+
}
174+
},
175+
receiveValue: { value in
176+
switch value {
177+
case let .insert(_, result):
178+
switch result.resultType {
179+
case .count:
180+
if let count = result.result as? Int {
181+
os_log("Batch inserted \(count) movies!")
182+
}
183+
case .objectIDs:
184+
if let objectIDs = result.result as? [NSManagedObjectID] {
185+
os_log("Batch inserted \(objectIDs.count) movies!")
186+
}
187+
case .statusOnly:
188+
let resultIsSuccessful = result.result as? Bool ?? false
189+
os_log("Batch insert - isSuccessful = \(resultIsSuccessful)")
190+
}
191+
default:
192+
fatalError("I asked for a batch INSERT!")
193+
}
194+
}
195+
)
196+
```
197+
198+
## TODO
199+
- Add a subscription feature for aggregate functions
200+
201+
## Contributing
202+
I welcome any feedback or contributions. I'm not eager to mess with the API a lot but let's be honest, it could probably be better. As always more tests wouldn't hurt.

0 commit comments

Comments
 (0)