Skip to content

Commit c312150

Browse files
authored
Tests and automatic context management (#5)
- Unit tests - Added optional automatic context management - Simplified `destroyAll()` method
1 parent a373f68 commit c312150

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1165
-114
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ DerivedData/
77
.swiftpm/config/registries.json
88
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
99
.netrc
10-
/.swiftpm
10+
/.swiftpm
11+
Gemfile.lock

Example App/Example App.xcodeproj/project.pbxproj

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,30 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
BE33D3972964F4F600736CA7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE33D3962964F4F600736CA7 /* AppDelegate.swift */; };
11+
BE4859D2297785A20087653D /* CoreDataPlus in Frameworks */ = {isa = PBXBuildFile; productRef = BE4859D1297785A20087653D /* CoreDataPlus */; };
1012
BEFA11F929491832001DE330 /* Example_AppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA11F829491832001DE330 /* Example_AppApp.swift */; };
1113
BEFA11FB29491832001DE330 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA11FA29491832001DE330 /* ContentView.swift */; };
1214
BEFA11FD29491832001DE330 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BEFA11FC29491832001DE330 /* Assets.xcassets */; };
1315
BEFA120029491832001DE330 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BEFA11FF29491832001DE330 /* Preview Assets.xcassets */; };
1416
BEFA120229491832001DE330 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA120129491832001DE330 /* Persistence.swift */; };
1517
BEFA120529491832001DE330 /* Example_App.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BEFA120329491832001DE330 /* Example_App.xcdatamodeld */; };
16-
BEFA1215294918B3001DE330 /* CoreDataPlus in Frameworks */ = {isa = PBXBuildFile; productRef = BEFA1214294918B3001DE330 /* CoreDataPlus */; };
1718
BEFA121929491916001DE330 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121629491915001DE330 /* Book.swift */; };
1819
BEFA121A29491916001DE330 /* BookDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121729491916001DE330 /* BookDetailView.swift */; };
1920
BEFA121B29491916001DE330 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121829491916001DE330 /* Author.swift */; };
2021
/* End PBXBuildFile section */
2122

2223
/* Begin PBXFileReference section */
24+
BE0E1E5529776C7F00038C68 /* Example-App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Example-App-Info.plist"; sourceTree = SOURCE_ROOT; };
25+
BE33D3962964F4F600736CA7 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
26+
BE4859D0297785980087653D /* CoreDataPlus */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CoreDataPlus; path = ..; sourceTree = "<group>"; };
2327
BEFA11F529491832001DE330 /* Example App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
2428
BEFA11F829491832001DE330 /* Example_AppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example_AppApp.swift; sourceTree = "<group>"; };
2529
BEFA11FA29491832001DE330 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
2630
BEFA11FC29491832001DE330 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2731
BEFA11FF29491832001DE330 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2832
BEFA120129491832001DE330 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
2933
BEFA120429491832001DE330 /* Example_App.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Example_App.xcdatamodel; sourceTree = "<group>"; };
30-
BEFA12122949189B001DE330 /* CoreDataPlus */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CoreDataPlus; path = ..; sourceTree = "<group>"; };
3134
BEFA121629491915001DE330 /* Book.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = "<group>"; };
3235
BEFA121729491916001DE330 /* BookDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookDetailView.swift; sourceTree = "<group>"; };
3336
BEFA121829491916001DE330 /* Author.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = "<group>"; };
@@ -38,17 +41,25 @@
3841
isa = PBXFrameworksBuildPhase;
3942
buildActionMask = 2147483647;
4043
files = (
41-
BEFA1215294918B3001DE330 /* CoreDataPlus in Frameworks */,
44+
BE4859D2297785A20087653D /* CoreDataPlus in Frameworks */,
4245
);
4346
runOnlyForDeploymentPostprocessing = 0;
4447
};
4548
/* End PBXFrameworksBuildPhase section */
4649

4750
/* Begin PBXGroup section */
51+
BE4859CF297785980087653D /* Packages */ = {
52+
isa = PBXGroup;
53+
children = (
54+
BE4859D0297785980087653D /* CoreDataPlus */,
55+
);
56+
name = Packages;
57+
sourceTree = "<group>";
58+
};
4859
BEFA11EC29491832001DE330 = {
4960
isa = PBXGroup;
5061
children = (
51-
BEFA12112949189B001DE330 /* Packages */,
62+
BE4859CF297785980087653D /* Packages */,
5263
BEFA11F729491832001DE330 /* Example App */,
5364
BEFA11F629491832001DE330 /* Products */,
5465
BEFA1213294918B3001DE330 /* Frameworks */,
@@ -66,9 +77,11 @@
6677
BEFA11F729491832001DE330 /* Example App */ = {
6778
isa = PBXGroup;
6879
children = (
80+
BE0E1E5529776C7F00038C68 /* Example-App-Info.plist */,
6981
BEFA121829491916001DE330 /* Author.swift */,
7082
BEFA121629491915001DE330 /* Book.swift */,
7183
BEFA121729491916001DE330 /* BookDetailView.swift */,
84+
BE33D3962964F4F600736CA7 /* AppDelegate.swift */,
7285
BEFA11F829491832001DE330 /* Example_AppApp.swift */,
7386
BEFA11FA29491832001DE330 /* ContentView.swift */,
7487
BEFA11FC29491832001DE330 /* Assets.xcassets */,
@@ -87,14 +100,6 @@
87100
path = "Preview Content";
88101
sourceTree = "<group>";
89102
};
90-
BEFA12112949189B001DE330 /* Packages */ = {
91-
isa = PBXGroup;
92-
children = (
93-
BEFA12122949189B001DE330 /* CoreDataPlus */,
94-
);
95-
name = Packages;
96-
sourceTree = "<group>";
97-
};
98103
BEFA1213294918B3001DE330 /* Frameworks */ = {
99104
isa = PBXGroup;
100105
children = (
@@ -119,7 +124,7 @@
119124
);
120125
name = "Example App";
121126
packageProductDependencies = (
122-
BEFA1214294918B3001DE330 /* CoreDataPlus */,
127+
BE4859D1297785A20087653D /* CoreDataPlus */,
123128
);
124129
productName = "Example App";
125130
productReference = BEFA11F529491832001DE330 /* Example App.app */;
@@ -175,6 +180,7 @@
175180
isa = PBXSourcesBuildPhase;
176181
buildActionMask = 2147483647;
177182
files = (
183+
BE33D3972964F4F600736CA7 /* AppDelegate.swift in Sources */,
178184
BEFA120529491832001DE330 /* Example_App.xcdatamodeld in Sources */,
179185
BEFA120229491832001DE330 /* Persistence.swift in Sources */,
180186
BEFA11FB29491832001DE330 /* ContentView.swift in Sources */,
@@ -313,6 +319,7 @@
313319
DEVELOPMENT_TEAM = 63P2N3D9XL;
314320
ENABLE_PREVIEWS = YES;
315321
GENERATE_INFOPLIST_FILE = YES;
322+
INFOPLIST_FILE = "Example-App-Info.plist";
316323
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
317324
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
318325
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -342,6 +349,7 @@
342349
DEVELOPMENT_TEAM = 63P2N3D9XL;
343350
ENABLE_PREVIEWS = YES;
344351
GENERATE_INFOPLIST_FILE = YES;
352+
INFOPLIST_FILE = "Example-App-Info.plist";
345353
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
346354
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
347355
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -384,7 +392,7 @@
384392
/* End XCConfigurationList section */
385393

386394
/* Begin XCSwiftPackageProductDependency section */
387-
BEFA1214294918B3001DE330 /* CoreDataPlus */ = {
395+
BE4859D1297785A20087653D /* CoreDataPlus */ = {
388396
isa = XCSwiftPackageProductDependency;
389397
productName = CoreDataPlus;
390398
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
import UIKit
3+
import CoreDataPlus
4+
import CoreData
5+
6+
class AppDelegate: NSObject, UIApplicationDelegate {
7+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
8+
let viewContext = PersistenceController.shared.container.viewContext
9+
10+
let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
11+
backgroundContext.automaticallyMergesChangesFromParent = true
12+
backgroundContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
13+
14+
CoreDataPlus.setup( viewContext: viewContext,
15+
backgroundContext: backgroundContext,
16+
logHandler: { message in print("🌎🌧 log: \(message)") }
17+
)
18+
19+
return true
20+
}
21+
}

Example App/Example App/BookDetailView.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import Foundation
44
import SwiftUI
5-
import CoreData
65
import CoreDataPlus
76

87
struct BookDetailView: View {
@@ -39,7 +38,9 @@ struct BookDetailView: View {
3938
"Lydia Millet"
4039
]
4140

42-
book.addToAuthors(Author.findOrCreate(column: "name", value: names.randomElement()!, context: viewContext))
41+
let author = Author.findOrCreate(column: "name", value: names.randomElement()!, context: viewContext)
42+
43+
book.addToAuthors(author)
4344
})
4445
.buttonStyle(.bordered)
4546

@@ -48,8 +49,14 @@ struct BookDetailView: View {
4849
})
4950
.buttonStyle(.bordered)
5051

51-
Button("Add new fictional author", action: {
52-
book.addToAuthors(Author.findOrCreate(column: "name", value: "Author \(Int.random(in: 1...1000))", context: viewContext))
52+
Button("Add existing fictional author", action: {
53+
let author = Author.searchFor(.empty(), context: viewContext).randomElement()!
54+
55+
if (((book.authors?.allObjects as? [Author])) ?? []).contains(author) {
56+
print("\(author.name ?? "Author") is already linked to this book. Doing nothing.")
57+
}
58+
59+
book.addToAuthors(author)
5360
})
5461
.buttonStyle(.bordered)
5562
}

Example App/Example App/ContentView.swift

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct ContentView: View {
7272

7373
ToolbarItemGroup(placement: .navigationBarLeading) {
7474
Button("New Book object with id = 100", action: {
75-
let book = Book.findOrCreate(id: "100", context: viewContext)
75+
let book = Book.findOrCreate(id: "100", using: .custom(nsManagedObjectContext: viewContext))
7676
book.title = "Book(id=100) last touched \(Date().formatted(date: .abbreviated, time: .complete))"
7777
})
7878
}
@@ -86,12 +86,15 @@ struct ContentView: View {
8686

8787
Spacer()
8888
Button("Delete authors", action: {
89-
Author.destroyAll(context: viewContext)
89+
Author.destroyAll()
9090
})
9191
.foregroundColor(.red)
9292

9393
Button("Delete books", action: {
94-
Book.destroyAll(context: viewContext)
94+
CoreDataPlus.shared.backgroundContext?.perform {
95+
Book.destroyAll(using: .background)
96+
try! CoreDataPlus.shared.backgroundContext?.save()
97+
}
9598
})
9699
.foregroundColor(.red)
97100

@@ -101,49 +104,57 @@ struct ContentView: View {
101104
}
102105

103106
}
104-
Text("Select an item")
107+
.navigationTitle("Books")
105108
}
109+
106110
.navigationViewStyle(.stack)
107111
}
108112

109113
private func addTestData() {
110-
// create some authors
111-
// using the name field as the unique identifier
112-
// TODO: add better docs on how to handle id/identifiable with core data (not library-specific)
113-
let murakami = Author.findOrCreate(column: "name", value: "Haruki Murakami", context: viewContext)
114-
let jk = Author.findOrCreate(column: "name", value: "J. K. Rowling", context: viewContext)
115-
116-
// using id as the unique identifier
117-
let lydia = Author.findOrCreate(id: "1234", context: viewContext)
118-
lydia.name = "Lydia Millet"
119114

120-
var authors = [murakami, jk, lydia]
121-
122-
for _ in 0..<10 {
123-
let newItem = Book.findOrCreate(id: UUID().uuidString, context: viewContext)
124-
newItem.title = "Harry Potter Vol. \(Int.random(in: 1...1000))"
115+
let background = CoreDataPlus.shared.backgroundContext!
116+
// TODO: document
117+
background.perform {
118+
119+
// create some authors
120+
// using the name field as the unique identifier
121+
// TODO: add better docs on how to handle id/identifiable with core data (not library-specific)
122+
let murakami = Author.findOrCreate(column: "name", value: "Haruki Murakami", using: .background)
123+
let jk = Author.findOrCreate(column: "name", value: "J. K. Rowling", using: .background)
125124

126-
newItem.addToAuthors(authors.randomElement()!)
125+
// using name as the unique identifier
126+
let lydia = Author.findOrCreate(column: "name", value: "Lydia Millet", using: .background)
127127

128-
// add a 2nd author to some books
129-
if Int.random(in: 1...100) > 50 {
128+
var authors = [murakami, jk, lydia]
129+
130+
for _ in 0..<10 {
131+
let newItem = Book.findOrCreate(id: UUID().uuidString, using: .background)
132+
newItem.title = "Harry Potter Vol. \(Int.random(in: 1...1000))"
133+
130134
newItem.addToAuthors(authors.randomElement()!)
135+
136+
// add a 2nd author to some books
137+
if Int.random(in: 1...100) > 50 {
138+
newItem.addToAuthors(authors.randomElement()!)
139+
}
131140
}
141+
142+
try? background.save()
132143
}
133144
}
134145
private func deleteAllBooks() {
135-
Book.destroyAll(context: viewContext)
146+
Book.destroyAll(using: .background)
136147
}
137148
private func deleteAllAuthors() {
138-
Author.destroyAll(context: viewContext)
149+
Author.destroyAll()
139150
}
140151
private func deleteAllBooksAndAuthors() {
141-
Author.destroyAll(context: viewContext)
142-
Book.destroyAll(context: viewContext)
152+
Author.destroyAll()
153+
Book.destroyAll()
143154
}
144155

145156
private func addBook() {
146-
let newBook = Book.findOrCreate(id: UUID().uuidString, context: viewContext)
157+
let newBook = Book.findOrCreate(id: UUID().uuidString, using: .foreground)
147158
newBook.title = "Book \(Int.random(in: 1...1000))"
148159
}
149160

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# ````
2+
3+
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
4+
5+
## Overview
6+
7+
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
8+
9+
## Topics
10+
11+
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
12+
13+
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G72" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
2+
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="21G320" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
33
<entity name="Author" representedClassName="Author" syncable="YES" codeGenerationType="class">
4-
<attribute name="id" optional="YES" attributeType="String"/>
54
<attribute name="name" attributeType="String" defaultValueString=""/>
65
<relationship name="books" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Book" inverseName="authors" inverseEntity="Book"/>
6+
<uniquenessConstraints>
7+
<uniquenessConstraint>
8+
<constraint value="name"/>
9+
</uniquenessConstraint>
10+
</uniquenessConstraints>
711
</entity>
812
<entity name="Book" representedClassName="Book" syncable="YES" codeGenerationType="class">
913
<attribute name="id" attributeType="String"/>
1014
<attribute name="title" attributeType="String" defaultValueString=""/>
1115
<relationship name="authors" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Author" inverseName="books" inverseEntity="Author"/>
16+
<uniquenessConstraints>
17+
<uniquenessConstraint>
18+
<constraint value="id"/>
19+
</uniquenessConstraint>
20+
</uniquenessConstraints>
1221
</entity>
1322
</model>

Example App/Example App/Example_AppApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SwiftUI
1111
struct Example_AppApp: App {
1212
let persistenceController = PersistenceController.shared
1313
@Environment(\.scenePhase) private var phase
14+
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
1415

1516
var body: some Scene {
1617
WindowGroup {

Example App/Example-App-Info.plist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict/>
5+
</plist>

Gemfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
source "https://rubygems.org"
2+
3+
gem "rspec", "~> 3.1"
4+
gem "rake"
5+
gem 'tty-prompt'
6+
gem 'tty-spinner'

0 commit comments

Comments
 (0)