Skip to content

Commit

Permalink
Tests and automatic context management (#5)
Browse files Browse the repository at this point in the history
- Unit tests
- Added optional automatic context management
- Simplified `destroyAll()` method
  • Loading branch information
spnkr authored Jan 18, 2023
1 parent a373f68 commit c312150
Show file tree
Hide file tree
Showing 49 changed files with 1,165 additions and 114 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/.swiftpm
/.swiftpm
Gemfile.lock
36 changes: 22 additions & 14 deletions Example App/Example App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,30 @@
objects = {

/* Begin PBXBuildFile section */
BE33D3972964F4F600736CA7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE33D3962964F4F600736CA7 /* AppDelegate.swift */; };
BE4859D2297785A20087653D /* CoreDataPlus in Frameworks */ = {isa = PBXBuildFile; productRef = BE4859D1297785A20087653D /* CoreDataPlus */; };
BEFA11F929491832001DE330 /* Example_AppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA11F829491832001DE330 /* Example_AppApp.swift */; };
BEFA11FB29491832001DE330 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA11FA29491832001DE330 /* ContentView.swift */; };
BEFA11FD29491832001DE330 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BEFA11FC29491832001DE330 /* Assets.xcassets */; };
BEFA120029491832001DE330 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BEFA11FF29491832001DE330 /* Preview Assets.xcassets */; };
BEFA120229491832001DE330 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA120129491832001DE330 /* Persistence.swift */; };
BEFA120529491832001DE330 /* Example_App.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BEFA120329491832001DE330 /* Example_App.xcdatamodeld */; };
BEFA1215294918B3001DE330 /* CoreDataPlus in Frameworks */ = {isa = PBXBuildFile; productRef = BEFA1214294918B3001DE330 /* CoreDataPlus */; };
BEFA121929491916001DE330 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121629491915001DE330 /* Book.swift */; };
BEFA121A29491916001DE330 /* BookDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121729491916001DE330 /* BookDetailView.swift */; };
BEFA121B29491916001DE330 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEFA121829491916001DE330 /* Author.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
BE0E1E5529776C7F00038C68 /* Example-App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Example-App-Info.plist"; sourceTree = SOURCE_ROOT; };
BE33D3962964F4F600736CA7 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BE4859D0297785980087653D /* CoreDataPlus */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CoreDataPlus; path = ..; sourceTree = "<group>"; };
BEFA11F529491832001DE330 /* Example App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
BEFA11F829491832001DE330 /* Example_AppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example_AppApp.swift; sourceTree = "<group>"; };
BEFA11FA29491832001DE330 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
BEFA11FC29491832001DE330 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BEFA11FF29491832001DE330 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
BEFA120129491832001DE330 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
BEFA120429491832001DE330 /* Example_App.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Example_App.xcdatamodel; sourceTree = "<group>"; };
BEFA12122949189B001DE330 /* CoreDataPlus */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CoreDataPlus; path = ..; sourceTree = "<group>"; };
BEFA121629491915001DE330 /* Book.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = "<group>"; };
BEFA121729491916001DE330 /* BookDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookDetailView.swift; sourceTree = "<group>"; };
BEFA121829491916001DE330 /* Author.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = "<group>"; };
Expand All @@ -38,17 +41,25 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
BEFA1215294918B3001DE330 /* CoreDataPlus in Frameworks */,
BE4859D2297785A20087653D /* CoreDataPlus in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
BE4859CF297785980087653D /* Packages */ = {
isa = PBXGroup;
children = (
BE4859D0297785980087653D /* CoreDataPlus */,
);
name = Packages;
sourceTree = "<group>";
};
BEFA11EC29491832001DE330 = {
isa = PBXGroup;
children = (
BEFA12112949189B001DE330 /* Packages */,
BE4859CF297785980087653D /* Packages */,
BEFA11F729491832001DE330 /* Example App */,
BEFA11F629491832001DE330 /* Products */,
BEFA1213294918B3001DE330 /* Frameworks */,
Expand All @@ -66,9 +77,11 @@
BEFA11F729491832001DE330 /* Example App */ = {
isa = PBXGroup;
children = (
BE0E1E5529776C7F00038C68 /* Example-App-Info.plist */,
BEFA121829491916001DE330 /* Author.swift */,
BEFA121629491915001DE330 /* Book.swift */,
BEFA121729491916001DE330 /* BookDetailView.swift */,
BE33D3962964F4F600736CA7 /* AppDelegate.swift */,
BEFA11F829491832001DE330 /* Example_AppApp.swift */,
BEFA11FA29491832001DE330 /* ContentView.swift */,
BEFA11FC29491832001DE330 /* Assets.xcassets */,
Expand All @@ -87,14 +100,6 @@
path = "Preview Content";
sourceTree = "<group>";
};
BEFA12112949189B001DE330 /* Packages */ = {
isa = PBXGroup;
children = (
BEFA12122949189B001DE330 /* CoreDataPlus */,
);
name = Packages;
sourceTree = "<group>";
};
BEFA1213294918B3001DE330 /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand All @@ -119,7 +124,7 @@
);
name = "Example App";
packageProductDependencies = (
BEFA1214294918B3001DE330 /* CoreDataPlus */,
BE4859D1297785A20087653D /* CoreDataPlus */,
);
productName = "Example App";
productReference = BEFA11F529491832001DE330 /* Example App.app */;
Expand Down Expand Up @@ -175,6 +180,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BE33D3972964F4F600736CA7 /* AppDelegate.swift in Sources */,
BEFA120529491832001DE330 /* Example_App.xcdatamodeld in Sources */,
BEFA120229491832001DE330 /* Persistence.swift in Sources */,
BEFA11FB29491832001DE330 /* ContentView.swift in Sources */,
Expand Down Expand Up @@ -313,6 +319,7 @@
DEVELOPMENT_TEAM = 63P2N3D9XL;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Example-App-Info.plist";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -342,6 +349,7 @@
DEVELOPMENT_TEAM = 63P2N3D9XL;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Example-App-Info.plist";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -384,7 +392,7 @@
/* End XCConfigurationList section */

/* Begin XCSwiftPackageProductDependency section */
BEFA1214294918B3001DE330 /* CoreDataPlus */ = {
BE4859D1297785A20087653D /* CoreDataPlus */ = {
isa = XCSwiftPackageProductDependency;
productName = CoreDataPlus;
};
Expand Down
21 changes: 21 additions & 0 deletions Example App/Example App/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import UIKit
import CoreDataPlus
import CoreData

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let viewContext = PersistenceController.shared.container.viewContext

let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy

CoreDataPlus.setup( viewContext: viewContext,
backgroundContext: backgroundContext,
logHandler: { message in print("🌎🌧 log: \(message)") }
)

return true
}
}
15 changes: 11 additions & 4 deletions Example App/Example App/BookDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import Foundation
import SwiftUI
import CoreData
import CoreDataPlus

struct BookDetailView: View {
Expand Down Expand Up @@ -39,7 +38,9 @@ struct BookDetailView: View {
"Lydia Millet"
]

book.addToAuthors(Author.findOrCreate(column: "name", value: names.randomElement()!, context: viewContext))
let author = Author.findOrCreate(column: "name", value: names.randomElement()!, context: viewContext)

book.addToAuthors(author)
})
.buttonStyle(.bordered)

Expand All @@ -48,8 +49,14 @@ struct BookDetailView: View {
})
.buttonStyle(.bordered)

Button("Add new fictional author", action: {
book.addToAuthors(Author.findOrCreate(column: "name", value: "Author \(Int.random(in: 1...1000))", context: viewContext))
Button("Add existing fictional author", action: {
let author = Author.searchFor(.empty(), context: viewContext).randomElement()!

if (((book.authors?.allObjects as? [Author])) ?? []).contains(author) {
print("\(author.name ?? "Author") is already linked to this book. Doing nothing.")
}

book.addToAuthors(author)
})
.buttonStyle(.bordered)
}
Expand Down
63 changes: 37 additions & 26 deletions Example App/Example App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct ContentView: View {

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

Spacer()
Button("Delete authors", action: {
Author.destroyAll(context: viewContext)
Author.destroyAll()
})
.foregroundColor(.red)

Button("Delete books", action: {
Book.destroyAll(context: viewContext)
CoreDataPlus.shared.backgroundContext?.perform {
Book.destroyAll(using: .background)
try! CoreDataPlus.shared.backgroundContext?.save()
}
})
.foregroundColor(.red)

Expand All @@ -101,49 +104,57 @@ struct ContentView: View {
}

}
Text("Select an item")
.navigationTitle("Books")
}

.navigationViewStyle(.stack)
}

private func addTestData() {
// create some authors
// using the name field as the unique identifier
// TODO: add better docs on how to handle id/identifiable with core data (not library-specific)
let murakami = Author.findOrCreate(column: "name", value: "Haruki Murakami", context: viewContext)
let jk = Author.findOrCreate(column: "name", value: "J. K. Rowling", context: viewContext)

// using id as the unique identifier
let lydia = Author.findOrCreate(id: "1234", context: viewContext)
lydia.name = "Lydia Millet"

var authors = [murakami, jk, lydia]

for _ in 0..<10 {
let newItem = Book.findOrCreate(id: UUID().uuidString, context: viewContext)
newItem.title = "Harry Potter Vol. \(Int.random(in: 1...1000))"
let background = CoreDataPlus.shared.backgroundContext!
// TODO: document
background.perform {

// create some authors
// using the name field as the unique identifier
// TODO: add better docs on how to handle id/identifiable with core data (not library-specific)
let murakami = Author.findOrCreate(column: "name", value: "Haruki Murakami", using: .background)
let jk = Author.findOrCreate(column: "name", value: "J. K. Rowling", using: .background)

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

// add a 2nd author to some books
if Int.random(in: 1...100) > 50 {
var authors = [murakami, jk, lydia]

for _ in 0..<10 {
let newItem = Book.findOrCreate(id: UUID().uuidString, using: .background)
newItem.title = "Harry Potter Vol. \(Int.random(in: 1...1000))"

newItem.addToAuthors(authors.randomElement()!)

// add a 2nd author to some books
if Int.random(in: 1...100) > 50 {
newItem.addToAuthors(authors.randomElement()!)
}
}

try? background.save()
}
}
private func deleteAllBooks() {
Book.destroyAll(context: viewContext)
Book.destroyAll(using: .background)
}
private func deleteAllAuthors() {
Author.destroyAll(context: viewContext)
Author.destroyAll()
}
private func deleteAllBooksAndAuthors() {
Author.destroyAll(context: viewContext)
Book.destroyAll(context: viewContext)
Author.destroyAll()
Book.destroyAll()
}

private func addBook() {
let newBook = Book.findOrCreate(id: UUID().uuidString, context: viewContext)
let newBook = Book.findOrCreate(id: UUID().uuidString, using: .foreground)
newBook.title = "Book \(Int.random(in: 1...1000))"
}

Expand Down
13 changes: 13 additions & 0 deletions Example App/Example App/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ````

<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->

## Overview

<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->

## Topics

### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->

- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G72" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="21G320" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Author" representedClassName="Author" syncable="YES" codeGenerationType="class">
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="books" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Book" inverseName="authors" inverseEntity="Book"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="name"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Book" representedClassName="Book" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String"/>
<attribute name="title" attributeType="String" defaultValueString=""/>
<relationship name="authors" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Author" inverseName="books" inverseEntity="Author"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>
1 change: 1 addition & 0 deletions Example App/Example App/Example_AppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SwiftUI
struct Example_AppApp: App {
let persistenceController = PersistenceController.shared
@Environment(\.scenePhase) private var phase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
WindowGroup {
Expand Down
5 changes: 5 additions & 0 deletions Example App/Example-App-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "https://rubygems.org"

gem "rspec", "~> 3.1"
gem "rake"
gem 'tty-prompt'
gem 'tty-spinner'
Loading

0 comments on commit c312150

Please sign in to comment.