Skip to content

Commit 0cae0c6

Browse files
committed
Improving persistent store loading
1 parent 885d255 commit 0cae0c6

9 files changed

+108
-91
lines changed

swift-sdk/Internal/DependencyContainerProtocol.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protocol DependencyContainerProtocol: RedirectNetworkSessionProvider {
1717
var apnsTypeChecker: APNSTypeCheckerProtocol { get }
1818

1919
func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol
20-
func createPersistenceContextProvider() -> IterablePersistenceContextProvider?
20+
func createPersistenceContextProvider() -> IterablePersistenceContextProvider
2121
func createRequestHandler(apiKey: String,
2222
config: IterableConfig,
2323
endpoint: String,
@@ -83,12 +83,7 @@ extension DependencyContainerProtocol {
8383
dateProvider: dateProvider)
8484
lazy var offlineProcessor: OfflineRequestProcessor? = nil
8585
lazy var healthMonitor: HealthMonitor? = nil
86-
guard let persistenceContextProvider = createPersistenceContextProvider() else {
87-
return RequestHandler(onlineProcessor: onlineProcessor,
88-
offlineProcessor: nil,
89-
healthMonitor: nil,
90-
offlineMode: offlineMode)
91-
}
86+
let persistenceContextProvider = createPersistenceContextProvider()
9287
if offlineMode {
9388

9489
let healthMonitorDataProvider = createHealthMonitorDataProvider(persistenceContextProvider: persistenceContextProvider)
@@ -124,7 +119,7 @@ extension DependencyContainerProtocol {
124119
HealthMonitorDataProvider(maxTasks: 1000, persistenceContextProvider: persistenceContextProvider)
125120
}
126121

127-
func createPersistenceContextProvider() -> IterablePersistenceContextProvider? {
122+
func createPersistenceContextProvider() -> IterablePersistenceContextProvider {
128123
CoreDataPersistenceContextProvider(dateProvider: dateProvider)
129124
}
130125

swift-sdk/Internal/IterableCoreDataPersistence.swift

Lines changed: 98 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ enum PersistenceConst {
1212
enum Entity {
1313
enum Task {
1414
static let name = "IterableTaskManagedObject"
15-
15+
1616
enum Column {
1717
static let id = "id"
1818
static let scheduledAt = "scheduledAt"
@@ -21,143 +21,158 @@ enum PersistenceConst {
2121
}
2222
}
2323

24-
class PersistentContainer: NSPersistentContainer {
25-
static var shared: PersistentContainer?
26-
27-
static func initialize() -> PersistentContainer? {
28-
if shared == nil {
29-
shared = create()
30-
}
31-
return shared
32-
}
33-
24+
let sharedManagedObjectModel: NSManagedObjectModel? = {
25+
guard let url = PersistentContainer.dataModelUrl(fromBundles: [Bundle.main, Bundle(for: PersistentContainer.self)]) else {
26+
ITBError("Could not find \(PersistenceConst.dataModelFileName).\(PersistenceConst.dataModelExtension) in bundle")
27+
return nil
28+
}
29+
ITBInfo("DB Bundle url: \(url)")
30+
return NSManagedObjectModel(contentsOf: url)
31+
}()
32+
33+
final class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
34+
3435
override func newBackgroundContext() -> NSManagedObjectContext {
3536
let backgroundContext = super.newBackgroundContext()
3637
backgroundContext.automaticallyMergesChangesFromParent = true
3738
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
3839
return backgroundContext
3940
}
4041

41-
private static func create() -> PersistentContainer? {
42-
guard let managedObjectModel = createManagedObjectModel() else {
43-
ITBError("Could not initialize managed object model")
44-
return nil
42+
override init(
43+
name: String = PersistenceConst.dataModelFileName,
44+
managedObjectModel: NSManagedObjectModel? = sharedManagedObjectModel
45+
) {
46+
if let managedObjectModel {
47+
super.init(name: name, managedObjectModel: managedObjectModel)
48+
} else {
49+
super.init(name: name)
4550
}
46-
let container = PersistentContainer(name: PersistenceConst.dataModelFileName, managedObjectModel: managedObjectModel)
47-
container.loadPersistentStores { desc, error in
48-
if let error = error {
49-
ITBError("Unresolved error when creating PersistentContainer: \(error)")
50-
}
51-
52-
ITBInfo("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")")
53-
}
54-
55-
container.viewContext.automaticallyMergesChangesFromParent = true
56-
container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
57-
58-
return container
59-
}
60-
61-
private static func createManagedObjectModel() -> NSManagedObjectModel? {
62-
guard let url = dataModelUrl(fromBundles: [Bundle.main, Bundle(for: PersistentContainer.self)]) else {
63-
ITBError("Could not find \(PersistenceConst.dataModelFileName).\(PersistenceConst.dataModelExtension) in bundle")
64-
return nil
65-
}
66-
ITBInfo("DB Bundle url: \(url)")
67-
return NSManagedObjectModel(contentsOf: url)
51+
viewContext.automaticallyMergesChangesFromParent = true
52+
viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
6853
}
69-
70-
private static func dataModelUrl(fromBundles bundles: [Bundle]) -> URL? {
54+
55+
static func dataModelUrl(fromBundles bundles: [Bundle]) -> URL? {
7156
bundles.lazy.compactMap(dataModelUrl(fromBundle:)).first
7257
}
73-
58+
7459
private static func dataModelUrl(fromBundle bundle: Bundle) -> URL? {
75-
ResourceHelper.url(forResource: PersistenceConst.dataModelFileName,
76-
withExtension: PersistenceConst.dataModelExtension,
77-
fromBundle: bundle)
60+
ResourceHelper.url(
61+
forResource: PersistenceConst.dataModelFileName,
62+
withExtension: PersistenceConst.dataModelExtension,
63+
fromBundle: bundle
64+
)
7865
}
7966
}
8067

81-
struct CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
82-
init?(dateProvider: DateProviderProtocol = SystemDateProvider()) {
83-
guard let persistentContainer = PersistentContainer.initialize() else {
84-
return nil
85-
}
68+
final class CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
69+
init(
70+
dateProvider: DateProviderProtocol = SystemDateProvider(),
71+
persistentContainer: NSPersistentContainer = PersistentContainer()
72+
) {
8673
self.persistentContainer = persistentContainer
8774
self.dateProvider = dateProvider
8875
}
89-
76+
9077
func newBackgroundContext() -> IterablePersistenceContext {
78+
if !isStoreLoaded {
79+
isStoreLoaded = loadStore(into: persistentContainer)
80+
}
9181
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.newBackgroundContext(), dateProvider: dateProvider)
9282
}
93-
83+
9484
func mainQueueContext() -> IterablePersistenceContext {
85+
if !isStoreLoaded {
86+
isStoreLoaded = loadStore(into: persistentContainer)
87+
}
9588
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.viewContext, dateProvider: dateProvider)
9689
}
97-
98-
private let persistentContainer: PersistentContainer
90+
91+
private let persistentContainer: NSPersistentContainer
9992
private let dateProvider: DateProviderProtocol
93+
private var isStoreLoaded = false
94+
95+
/// Loads the persistent container synchronously so we can easily capture loading errors.
96+
private func loadStore(into container: NSPersistentContainer) -> Bool {
97+
if let descriptor = container.persistentStoreDescriptions.first {
98+
descriptor.shouldAddStoreAsynchronously = false
99+
}
100+
101+
// This closure runs synchronously because of the settings above
102+
var loadError: (any Error)?
103+
container.loadPersistentStores { _, error in
104+
loadError = error
105+
}
106+
107+
if let error = loadError {
108+
ITBError("Failed to load Iterable's store. \(error.localizedDescription)")
109+
return false
110+
}
111+
return true
112+
}
100113
}
101114

102115
struct CoreDataPersistenceContext: IterablePersistenceContext {
103116
init(managedObjectContext: NSManagedObjectContext, dateProvider: DateProviderProtocol) {
104117
self.managedObjectContext = managedObjectContext
105118
self.dateProvider = dateProvider
106119
}
107-
120+
108121
func create(task: IterableTask) throws -> IterableTask {
109122
guard let taskManagedObject = createTaskManagedObject() else {
110123
throw IterableDBError.general("Could not create task managed object")
111124
}
112-
125+
113126
PersistenceHelper.copy(from: task, to: taskManagedObject)
114127
taskManagedObject.createdAt = dateProvider.currentDate
115128
return PersistenceHelper.task(from: taskManagedObject)
116129
}
117-
130+
118131
func update(task: IterableTask) throws -> IterableTask {
119132
guard let taskManagedObject = try findTaskManagedObject(id: task.id) else {
120133
throw IterableDBError.general("Could not find task to update")
121134
}
122-
135+
123136
PersistenceHelper.copy(from: task, to: taskManagedObject)
124137
taskManagedObject.modifiedAt = dateProvider.currentDate
125138
return PersistenceHelper.task(from: taskManagedObject)
126139
}
127-
140+
128141
func delete(task: IterableTask) throws {
129142
try deleteTask(withId: task.id)
130143
}
131144

132145
func nextTask() throws -> IterableTask? {
133-
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(context: managedObjectContext,
134-
entity: PersistenceConst.Entity.Task.name,
135-
column: PersistenceConst.Entity.Task.Column.scheduledAt,
136-
ascending: true,
137-
limit: 1)
146+
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(
147+
context: managedObjectContext,
148+
entity: PersistenceConst.Entity.Task.name,
149+
column: PersistenceConst.Entity.Task.Column.scheduledAt,
150+
ascending: true,
151+
limit: 1
152+
)
138153
return taskManagedObjects.first.map(PersistenceHelper.task(from:))
139154
}
140-
155+
141156
func findTask(withId id: String) throws -> IterableTask? {
142157
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
143158
return nil
144159
}
145160
return PersistenceHelper.task(from: taskManagedObject)
146161
}
147-
162+
148163
func deleteTask(withId id: String) throws {
149164
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
150165
return
151166
}
152167
managedObjectContext.delete(taskManagedObject)
153168
}
154-
169+
155170
func findAllTasks() throws -> [IterableTask] {
156171
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
157-
172+
158173
return taskManagedObjects.map(PersistenceHelper.task(from:))
159174
}
160-
175+
161176
func deleteAllTasks() throws {
162177
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
163178
taskManagedObjects.forEach {
@@ -168,34 +183,41 @@ struct CoreDataPersistenceContext: IterablePersistenceContext {
168183
}
169184
}
170185
}
171-
186+
172187
func countTasks() throws -> Int {
173-
return try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
188+
try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
174189
}
175-
190+
176191
func save() throws {
192+
// Guard against Objective-C exceptions which cannot be caught in Swift.
193+
guard
194+
let coordinator = managedObjectContext.persistentStoreCoordinator,
195+
!coordinator.persistentStores.isEmpty
196+
else {
197+
throw NSError(domain: NSCocoaErrorDomain, code: NSPersistentStoreSaveError)
198+
}
177199
try managedObjectContext.save()
178200
}
179-
201+
180202
func perform(_ block: @escaping () -> Void) {
181203
managedObjectContext.perform(block)
182204
}
183-
205+
184206
func performAndWait(_ block: () -> Void) {
185207
managedObjectContext.performAndWait(block)
186208
}
187-
209+
188210
func performAndWait<T>(_ block: () throws -> T) throws -> T {
189211
try managedObjectContext.performAndWait(block)
190212
}
191-
213+
192214
private let managedObjectContext: NSManagedObjectContext
193215
private let dateProvider: DateProviderProtocol
194-
216+
195217
private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? {
196218
try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name, columnName: PersistenceConst.Entity.Task.Column.id, columnValue: id)
197219
}
198-
220+
199221
private func createTaskManagedObject() -> IterableTaskManagedObject? {
200222
CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
201223
}

tests/common/CommonExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class MockDependencyContainer: DependencyContainerProtocol {
129129
HealthMonitorDataProvider(maxTasks: maxTasks, persistenceContextProvider: persistenceContextProvider)
130130
}
131131

132-
func createPersistenceContextProvider() -> IterablePersistenceContextProvider? {
132+
func createPersistenceContextProvider() -> IterablePersistenceContextProvider {
133133
if let persistenceContextProvider = persistenceContextProvider {
134134
return persistenceContextProvider
135135
} else {

tests/offline-events-tests/HealthMonitorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ class HealthMonitorTests: XCTestCase {
226226
private let dateProvider = MockDateProvider()
227227

228228
private lazy var persistenceProvider: IterablePersistenceContextProvider = {
229-
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)!
229+
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)
230230
try! provider.mainQueueContext().deleteAllTasks()
231231
try! provider.mainQueueContext().save()
232232
return provider

tests/offline-events-tests/RequestHandlerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ class RequestHandlerTests: XCTestCase {
11701170
private let dateProvider = MockDateProvider()
11711171

11721172
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = {
1173-
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)!
1173+
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)
11741174
return provider
11751175
}()
11761176
}

tests/offline-events-tests/TaskProcessorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class TaskProcessorTests: XCTestCase {
253253
appPackageName: Bundle.main.appPackageName ?? "")
254254

255255
private lazy var persistenceProvider: IterablePersistenceContextProvider = {
256-
let provider = CoreDataPersistenceContextProvider()!
256+
let provider = CoreDataPersistenceContextProvider()
257257
try! provider.mainQueueContext().deleteAllTasks()
258258
try! provider.mainQueueContext().save()
259259
return provider

tests/offline-events-tests/TaskRunnerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ class TaskRunnerTests: XCTestCase {
411411
appPackageName: Bundle.main.appPackageName ?? "")
412412

413413
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = {
414-
let provider = CoreDataPersistenceContextProvider()!
414+
let provider = CoreDataPersistenceContextProvider()
415415
return provider
416416
}()
417417
}

tests/offline-events-tests/TaskSchedulerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class TaskSchedulerTests: XCTestCase {
114114
appPackageName: Bundle.main.appPackageName ?? "")
115115

116116
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = {
117-
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)!
117+
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)
118118
return provider
119119
}()
120120

tests/offline-events-tests/TasksCRUDTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class TasksCRUDTests: XCTestCase {
214214
private let dateProvider = MockDateProvider()
215215

216216
private lazy var persistenceProvider: IterablePersistenceContextProvider = {
217-
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)!
217+
let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)
218218
try! provider.mainQueueContext().deleteAllTasks()
219219
try! provider.mainQueueContext().save()
220220
return provider

0 commit comments

Comments
 (0)