224 Core Data Best Practices PDF
224 Core Data Best Practices PDF
© 2018 Apple Inc. All rights reserved. Redistribution or public display not permitted without written permission from Apple.
•
Evolving containers
•
Managing growth
•
Potpourri
•
Evolving containers
•
Managing growth
•
Potpourri
•
Evolving containers
•
Managing growth
•
Potpourri
•
Evolving containers
•
Managing growth
•
Potpourri
•
Evolving containers
•
Managing growth
•
Potpourri
•
Evolving containers
•
Managing growth
•
Potpourri
Let’s Build an App
• On device
Object Graph
Object Graph Persistence
Core Data
Core Data Manages Persistence
Entity: Post
•
Entity: Comment
•
NSManagedObjectModel
Entity: Post
•
Entity: Comment
•
…
Core Data Manages Persistence
NSManagedObjectModel
Entity: Comment
•
…
Core Data Manages Persistence
NSPersistentStoreCoordinator NSManagedObjectModel
Entity: Comment
•
…
Core Data Manages Persistence
NSPersistentStoreCoordinator NSManagedObjectModel
Entity: Comment
•
…
NSManagedObjectContext
Core Data Manages Persistence
NSPersistentStoreCoordinator NSManagedObjectModel
Entity: Comment
•
…
NSManagedObjectContext
let storeURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Postings")
if success {
print("ready!")
}
let container = NSPersistentContainer(name: "Postings")
container.loadPersistentStores { (desc, err) in
if let err = err {
print("error loading store \(desc): \(err)")
return
}
print("ready!")
}
Core Data Manages Persistence
NSPersistentStoreCoordinator NSManagedObjectModel
Entity: Comment
•
…
NSManagedObjectContext
NSPersistentStoreCoordinator NSManagedObjectModel
Entity: Comment
•
…
NSManagedObjectContext
container.persistentStoreDescriptions[0].url = storeStorage.appendingPathComponent(storeName)
Navigation Controller
MyPostsViewController AllPostsViewController
MyPostViewController AllPostViewController
Generalizing Controllers
Navigation Controller
fetchRequest
PostsListViewController
detailObject
PostDetailViewController
Generalizing Controllers
- Managed object
- Fetch request
Generalizing Controllers
- Managed object
- Fetch request
• Managed object context
Generalizing Workers
- URL
- Property list
• Managed object context
Generalizing Controllers
• Storyboards
• Storyboards
• Code
• Batch size
Turning Fetch Requests Into List Views
• Batch size
extension Post {
@objc var day: String {
let components = Calendar.current.dateComponents([Calendar.Component.year,
Calendar.Component.month,
Calendar.Component.day],
from: self.timestamp!)
return "\(components.year!)-\(components.month!)-\(components.day!)"
}
}
Matching UIs to the Model
ChartViewController
40
30
20
10
May 10, 2018 May 17, 2018 May 24, 2018 May 31, 2018 June 7, 2018
Posts
Matching UIs to the Model
Complex fetch requests
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
fr.propertiesToGroupBy = ["day"]
fr.resultType = .dictionaryResultType
40
30
20
10
May 10, 2018 May 17, 2018 May 24, 2018 May 31, 2018 June 7, 2018
Posts
Matching UIs to the Model
Complex fetch requests
40
30
20
10
May 10, 2018 May 17, 2018 May 24, 2018 May 31, 2018 June 7, 2018
Posts
Matching UIs to the Model
ChartViewController
4000
3000
2000
1000
May 10, 2018 May 17, 2018 May 24, 2018 May 31, 2018 June 7, 2018
Posts
Denormalization
Matching UIs to the Model
ChartViewController
New entity
• PublishedPostCountPerDay
- day (Date)
- count (UInt)
Matching UIs to the Model
ChartViewController
New entity
Maintained as needed
• When publishing a post
context.performAndWait {
for case let post as Post in context.insertedObjects {
let countObj = PublishedPostCountPerDay(dayOf: post.timestamp, context: context)
countObj.count += 1
}
for case let post as Post in context.deletedObjects {
let countObj = PublishedPostCountPerDay(dayOf: post.timestamp, context: context)
countObj.count -= 1
}
}
}
•
Managing Growth
Nick Gillett
Managing Growth
Managing Growth
• Responsive scrolling
Managing Growth
• Responsive scrolling
• Customer Delight
Managing Growth
Managing Growth
Engineering Metrics
Managing Growth
Engineering Metrics
• Peak memory consumption
Managing Growth
Engineering Metrics
• Peak memory consumption
• Battery drain
Managing Growth
Engineering Metrics
• Peak memory consumption
• Battery drain
• CPU time
Managing Growth
Engineering Metrics
• Peak memory consumption
• Battery drain
• CPU time
• I/O
Consistent Experiences with Query Generations
Consistent Experiences with Query Generations
@available(iOS 11.0, *)
open class NSPersistentHistoryTransaction : NSObject, NSCopying {
open var changes: [NSPersistentHistoryChange]? { get }
//...
open func objectIDNotification() -> Notification
}
Filtering Updates with Persistent History Tracking
Filtering Updates with Persistent History Tracking
changedObjectID type
Post/p1 insert
Post/p2 insert
Post/p3 insert
Post/p4 insert
Post/p5 insert
let context = self.viewContext
let transaction: NSPersistentHistoryTransaction = //...
context.perform {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
let context = self.viewContext
let transaction: NSPersistentHistoryTransaction = //...
context.perform {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
Filtering Updates with Persistent History Tracking
changedObjectID type
Comment/p1 insert
Comment/p2 insert
Comment/p3 insert
Comment/p4 insert
Comment/p5 insert
var changes = [] as Array<NSPersistentHistoryChange>
//…
for transaction in transactions {
let filteredChanges = transaction.changes!.filter({ (change) -> Bool in
return Post.entity().name == change.changedObjectID.entity.name
})
changes.append(contentsOf: filteredChanges)
}
var changes = [] as Array<NSPersistentHistoryChange>
//…
for transaction in transactions {
let filteredChanges = transaction.changes!.filter({ (change) -> Bool in
return Post.entity().name == change.changedObjectID.entity.name
})
changes.append(contentsOf: filteredChanges)
}
extension Post {
@NSManaged public var image: Data?
@NSManaged public var title: String?
//...
}
extension Post {
@NSManaged public var image: Data?
@NSManaged public var title: String?
//...
}
var changes = [] as Array<NSPersistentHistoryChange>
//…
for transaction in transactions {
let filteredChanges = transaction.changes!.filter({ (change) -> Bool in
if let updatedProperties = change.updatedProperties {
return updatedProperties.contains(where: { (property) -> Bool in
return property.name == "image" ||
property.name == "title"
})
} else {
return false;
}
})
changes.append(contentsOf: filteredChanges)
}
var changes = [] as Array<NSPersistentHistoryChange>
//…
for transaction in transactions {
let filteredChanges = transaction.changes!.filter({ (change) -> Bool in
if let updatedProperties = change.updatedProperties {
return updatedProperties.contains(where: { (property) -> Bool in
return property.name == "image" ||
property.name == "title"
})
} else {
return false;
}
})
changes.append(contentsOf: filteredChanges)
}
•
1500 100%
1200
75%
900
Memory (MB)
50%
600
25%
300
0%
1000 100k 1M 10M
NSManagedObject.delete vs NSBatchDeleteRequest
1500 100%
1200
75%
900
Memory (MB)
50%
600
25%
300
0%
1000 100k 1M 10M
NSManagedObject.delete vs NSBatchDeleteRequest
1500 100%
1200
75%
900
Memory (MB)
50%
600
25%
300
0%
1000 100k 1M 10M
NSManagedObject.delete vs NSBatchDeleteRequest
1500 100%
1200
75%
900
Memory (MB)
50%
600
25%
300
0%
1000 100k 1M 10M
let context = container.viewContext
context.performAndWait {
let request = NSPersistentHistoryChangeRequest()
request.resultType = .transactionsAndChanges
do {
let result = try context.execute(request) as! NSPersistentHistoryResult
let transactions = result.result as! Array<NSPersistentHistoryTransaction>
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
} catch {
//...
}
}
let context = container.viewContext
context.performAndWait {
let request = NSPersistentHistoryChangeRequest()
request.resultType = .transactionsAndChanges
do {
let result = try context.execute(request) as! NSPersistentHistoryResult
let transactions = result.result as! Array<NSPersistentHistoryTransaction>
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
} catch {
//...
}
}
let context = container.viewContext
context.performAndWait {
let request = NSPersistentHistoryChangeRequest()
request.resultType = .transactionsAndChanges
do {
let result = try context.execute(request) as! NSPersistentHistoryResult
let transactions = result.result as! Array<NSPersistentHistoryTransaction>
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
} catch {
//...
}
}
•
• New—NSSecureUnarchiveFromDataTransformerName
• New—NSSecureUnarchiveFromDataTransformerName
Help Future You
• New—NSSecureUnarchiveFromDataTransformerName
• New—NSSecureUnarchiveFromDataTransformerName
sqlite> .expert
sqlite> SELECT 0, t0.Z_PK FROM ZPOST t0 ORDER BY t0.ZTIMESTAMP DESC;
CREATE INDEX ZPOST_idx_43b4963d ON ZPOST(ZTIMESTAMP DESC);
sqlite> .expert
sqlite> SELECT 0, t0.Z_PK FROM ZPOST t0 ORDER BY t0.ZTIMESTAMP DESC;
CREATE INDEX ZPOST_idx_43b4963d ON ZPOST(ZTIMESTAMP DESC);
sqlite> .expert
sqlite> SELECT 0, t0.Z_PK FROM ZPOST t0 ORDER BY t0.ZTIMESTAMP DESC;
CREATE INDEX ZPOST_idx_43b4963d ON ZPOST(ZTIMESTAMP DESC);
sqlite> .expert
sqlite> SELECT 0, t0.Z_PK FROM ZPOST t0 ORDER BY t0.ZTIMESTAMP DESC;
CREATE INDEX ZPOST_idx_43b4963d ON ZPOST(ZTIMESTAMP DESC);
• Verify assumptions
Testing With CoreData
• Verify assumptions
• Verify assumptions
self.measure {
do {
let context = self.newContainer().newBackgroundContext()
let frc = self.newFetchedResultsController(with: fetchRequest, context: context)
try frc.performFetch()
} catch {
XCTFail("performFetch threw: \(error)")
}
}
}
func testLibraryFetchPerformance() {
let fetchRequest: NSFetchRequest<Post> = Post.fetchRequest()
fetchRequest.fetchBatchSize = 200
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "timestamp", ascending: false) ]
self.measure {
do {
let context = self.newContainer().newBackgroundContext()
let frc = self.newFetchedResultsController(with: fetchRequest, context: context)
try frc.performFetch()
} catch {
XCTFail("performFetch threw: \(error)")
}
}
}
bugreport.apple.com
More Information
https://developer.apple.com/wwdc18/224