Skip to content

Commit 13d8074

Browse files
authored
Merge pull request #865 from Iterable/feature/fix-persistent-container-loading
[MOB-10311] fix persistent container loading
2 parents d5dd27a + 28a0365 commit 13d8074

10 files changed

+129
-115
lines changed

swift-sdk/Internal/DependencyContainerProtocol.swift

+3-8
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

+95-79
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,152 @@ 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
24+
let sharedManagedObjectModel: NSManagedObjectModel? = {
25+
let firstBundleURL: URL? = [Bundle.main, Bundle(for: PersistentContainer.self)].lazy.compactMap { bundle in
26+
ResourceHelper.url(
27+
forResource: PersistenceConst.dataModelFileName,
28+
withExtension: PersistenceConst.dataModelExtension,
29+
fromBundle: bundle
30+
)
31+
}.first
32+
33+
guard let url = firstBundleURL else {
34+
ITBError("Could not find \(PersistenceConst.dataModelFileName).\(PersistenceConst.dataModelExtension) in bundle")
35+
return nil
3236
}
33-
37+
ITBInfo("DB Bundle url: \(url)")
38+
return NSManagedObjectModel(contentsOf: url)
39+
}()
40+
41+
final class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
42+
3443
override func newBackgroundContext() -> NSManagedObjectContext {
3544
let backgroundContext = super.newBackgroundContext()
3645
backgroundContext.automaticallyMergesChangesFromParent = true
3746
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
3847
return backgroundContext
3948
}
4049

41-
private static func create() -> PersistentContainer? {
42-
guard let managedObjectModel = createManagedObjectModel() else {
43-
ITBError("Could not initialize managed object model")
44-
return nil
50+
init() {
51+
let name = PersistenceConst.dataModelFileName
52+
if let managedObjectModel = sharedManagedObjectModel {
53+
super.init(name: name, managedObjectModel: managedObjectModel)
54+
} else {
55+
super.init(name: name)
4556
}
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)
68-
}
69-
70-
private static func dataModelUrl(fromBundles bundles: [Bundle]) -> URL? {
71-
bundles.lazy.compactMap(dataModelUrl(fromBundle:)).first
72-
}
73-
74-
private static func dataModelUrl(fromBundle bundle: Bundle) -> URL? {
75-
ResourceHelper.url(forResource: PersistenceConst.dataModelFileName,
76-
withExtension: PersistenceConst.dataModelExtension,
77-
fromBundle: bundle)
57+
viewContext.automaticallyMergesChangesFromParent = true
58+
viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
7859
}
7960
}
8061

81-
struct CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
82-
init?(dateProvider: DateProviderProtocol = SystemDateProvider()) {
83-
guard let persistentContainer = PersistentContainer.initialize() else {
84-
return nil
85-
}
62+
final class CoreDataPersistenceContextProvider: IterablePersistenceContextProvider {
63+
init(
64+
dateProvider: DateProviderProtocol = SystemDateProvider(),
65+
persistentContainer: NSPersistentContainer = PersistentContainer()
66+
) {
8667
self.persistentContainer = persistentContainer
8768
self.dateProvider = dateProvider
8869
}
89-
70+
9071
func newBackgroundContext() -> IterablePersistenceContext {
72+
if !isStoreLoaded {
73+
isStoreLoaded = loadStore(into: persistentContainer)
74+
}
9175
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.newBackgroundContext(), dateProvider: dateProvider)
9276
}
93-
77+
9478
func mainQueueContext() -> IterablePersistenceContext {
79+
if !isStoreLoaded {
80+
isStoreLoaded = loadStore(into: persistentContainer)
81+
}
9582
return CoreDataPersistenceContext(managedObjectContext: persistentContainer.viewContext, dateProvider: dateProvider)
9683
}
97-
98-
private let persistentContainer: PersistentContainer
84+
85+
private let persistentContainer: NSPersistentContainer
9986
private let dateProvider: DateProviderProtocol
87+
private var isStoreLoaded = false
88+
89+
/// Loads the persistent container synchronously so we can easily capture loading errors.
90+
private func loadStore(into container: NSPersistentContainer) -> Bool {
91+
if let descriptor = container.persistentStoreDescriptions.first {
92+
descriptor.shouldAddStoreAsynchronously = false
93+
}
94+
95+
// This closure runs synchronously because of the settings above
96+
var loadError: (any Error)?
97+
container.loadPersistentStores { _, error in
98+
loadError = error
99+
}
100+
101+
if let error = loadError {
102+
ITBError("Failed to load Iterable's store. \(error.localizedDescription)")
103+
return false
104+
}
105+
return true
106+
}
100107
}
101108

102109
struct CoreDataPersistenceContext: IterablePersistenceContext {
103110
init(managedObjectContext: NSManagedObjectContext, dateProvider: DateProviderProtocol) {
104111
self.managedObjectContext = managedObjectContext
105112
self.dateProvider = dateProvider
106113
}
107-
114+
108115
func create(task: IterableTask) throws -> IterableTask {
109116
guard let taskManagedObject = createTaskManagedObject() else {
110117
throw IterableDBError.general("Could not create task managed object")
111118
}
112-
119+
113120
PersistenceHelper.copy(from: task, to: taskManagedObject)
114121
taskManagedObject.createdAt = dateProvider.currentDate
115122
return PersistenceHelper.task(from: taskManagedObject)
116123
}
117-
124+
118125
func update(task: IterableTask) throws -> IterableTask {
119126
guard let taskManagedObject = try findTaskManagedObject(id: task.id) else {
120127
throw IterableDBError.general("Could not find task to update")
121128
}
122-
129+
123130
PersistenceHelper.copy(from: task, to: taskManagedObject)
124131
taskManagedObject.modifiedAt = dateProvider.currentDate
125132
return PersistenceHelper.task(from: taskManagedObject)
126133
}
127-
134+
128135
func delete(task: IterableTask) throws {
129136
try deleteTask(withId: task.id)
130137
}
131138

132139
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)
140+
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(
141+
context: managedObjectContext,
142+
entity: PersistenceConst.Entity.Task.name,
143+
column: PersistenceConst.Entity.Task.Column.scheduledAt,
144+
ascending: true,
145+
limit: 1
146+
)
138147
return taskManagedObjects.first.map(PersistenceHelper.task(from:))
139148
}
140-
149+
141150
func findTask(withId id: String) throws -> IterableTask? {
142151
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
143152
return nil
144153
}
145154
return PersistenceHelper.task(from: taskManagedObject)
146155
}
147-
156+
148157
func deleteTask(withId id: String) throws {
149158
guard let taskManagedObject = try findTaskManagedObject(id: id) else {
150159
return
151160
}
152161
managedObjectContext.delete(taskManagedObject)
153162
}
154-
163+
155164
func findAllTasks() throws -> [IterableTask] {
156165
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
157-
166+
158167
return taskManagedObjects.map(PersistenceHelper.task(from:))
159168
}
160-
169+
161170
func deleteAllTasks() throws {
162171
let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
163172
taskManagedObjects.forEach {
@@ -168,34 +177,41 @@ struct CoreDataPersistenceContext: IterablePersistenceContext {
168177
}
169178
}
170179
}
171-
180+
172181
func countTasks() throws -> Int {
173-
return try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
182+
try CoreDataUtil.count(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
174183
}
175-
184+
176185
func save() throws {
186+
// Guard against Objective-C exceptions which cannot be caught in Swift.
187+
guard
188+
let coordinator = managedObjectContext.persistentStoreCoordinator,
189+
!coordinator.persistentStores.isEmpty
190+
else {
191+
throw NSError(domain: NSCocoaErrorDomain, code: NSPersistentStoreSaveError)
192+
}
177193
try managedObjectContext.save()
178194
}
179-
195+
180196
func perform(_ block: @escaping () -> Void) {
181197
managedObjectContext.perform(block)
182198
}
183-
199+
184200
func performAndWait(_ block: () -> Void) {
185201
managedObjectContext.performAndWait(block)
186202
}
187-
203+
188204
func performAndWait<T>(_ block: () throws -> T) throws -> T {
189205
try managedObjectContext.performAndWait(block)
190206
}
191-
207+
192208
private let managedObjectContext: NSManagedObjectContext
193209
private let dateProvider: DateProviderProtocol
194-
210+
195211
private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? {
196212
try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name, columnName: PersistenceConst.Entity.Task.Column.id, columnValue: id)
197213
}
198-
214+
199215
private func createTaskManagedObject() -> IterableTaskManagedObject? {
200216
CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name)
201217
}

tests/common/CommonExtensions.swift

+1-1
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/endpoint-tests/OfflineModeE2ETests.swift

+24-21
Original file line numberDiff line numberDiff line change
@@ -81,42 +81,45 @@ class OfflineModeEndpointTests: XCTestCase {
8181
"messageId": "msg_1",
8282
],
8383
]
84-
api.trackPushOpen(pushPayload,
85-
dataFields: ["data_field1": "value1"],
86-
onSuccess: { _ in
87-
expectation1.fulfill()
88-
}) { reason, _ in
84+
api.trackPushOpen(
85+
pushPayload,
86+
dataFields: ["data_field1": "value1"],
87+
onSuccess: { _ in
88+
expectation1.fulfill()
89+
}
90+
) { reason, _ in
8991
XCTFail(reason ?? "failed")
9092
}
91-
93+
9294
wait(for: [expectation1], timeout: 15)
9395
}
94-
96+
9597
func test04TrackEvent() throws {
9698
let expectation1 = expectation(description: #function)
9799
let localStorage = MockLocalStorage()
98100
localStorage.offlineMode = true
99-
let api = InternalIterableAPI.initializeForE2E(apiKey: Self.apiKey,
100-
localStorage: localStorage)
101+
let api = InternalIterableAPI.initializeForE2E(
102+
apiKey: Self.apiKey,
103+
localStorage: localStorage
104+
)
101105
api.email = "[email protected]"
102-
103-
api.track("event1",
104-
dataFields: ["data_field1": "value1"],
105-
onSuccess: { _ in
106-
expectation1.fulfill()
107-
}) { reason, _ in
106+
107+
api.track(
108+
"event1",
109+
dataFields: ["data_field1": "value1"],
110+
onSuccess: { _ in
111+
expectation1.fulfill()
112+
}
113+
) { reason, _ in
108114
XCTFail(reason ?? "failed")
109115
}
110-
116+
111117
wait(for: [expectation1], timeout: 15)
112118
}
113-
119+
114120
private static let apiKey = Environment.apiKey!
115121
private static let pushCampaignId = Environment.pushCampaignId!
116122
private static let pushTemplateId = Environment.pushTemplateId!
117123
private static let inAppCampaignId = Environment.inAppCampaignId!
118-
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = {
119-
let provider = CoreDataPersistenceContextProvider()!
120-
return provider
121-
}()
124+
private lazy var persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider()
122125
}

tests/offline-events-tests/HealthMonitorTests.swift

+1-1
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

0 commit comments

Comments
 (0)