-
Notifications
You must be signed in to change notification settings - Fork 150
/
Copy pathCoreDataHelper.swift
476 lines (399 loc) · 19.8 KB
/
CoreDataHelper.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
//
// CoreDataHelper.swift
// OpenGpxTracker
//
// Created by Vincent on 9/4/19.
//
import UIKit
import CoreData
import CoreGPX
/// Core Data implementation. As all Core Data related logic is contained here, I considered it as a helper.
///
/// Implementation learnt / inspired
/// from 4 part series:
/// https://marcosantadev.com/coredata_crud_concurrency_swift_1/
///
class CoreDataHelper {
// MARK: IDs
// ids to keep track of object's sequence
/// for waypoints
var waypointId = Int64()
/// for trackpoints
var trackpointId = Int64()
/// id to seperate trackpoints in different tracksegements
var tracksegmentId = Int64()
var isContinued = false
var lastTracksegmentId = Int64()
// MARK: Other Declarations
/// app delegate.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// swiftlint:disable:previous force_cast
// arrays for handling retrieval of data when needed.
// recovered tracksegments
var tracksegments = [GPXTrackSegment]()
// recovered current segment
var currentSegment = GPXTrackSegment()
// recovered waypoints, inclusive of waypoints from previous file if file is loaded on recovery.
var waypoints = [GPXWaypoint]()
// last file name of the recovered file, if the recovered file was a continuation.
var lastFileName = String()
// MARK: Add to Core Data
/// Adds the last file name to Core Data
///
/// - Parameters:
/// - lastFileName: Last file name of the previously logged GPX file.
///
func add(toCoreData lastFileName: String, willContinueAfterSave willContinue: Bool) {
let childManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Creates the link between child and parent
childManagedObjectContext.parent = appDelegate.managedObjectContext
childManagedObjectContext.perform {
// swiftlint:disable:next force_cast
let root = NSEntityDescription.insertNewObject(forEntityName: "CDRoot", into: childManagedObjectContext) as! CDRoot
root.lastFileName = lastFileName
root.continuedAfterSave = willContinue
root.lastTrackSegmentId = self.tracksegmentId
do {
try childManagedObjectContext.save()
self.appDelegate.managedObjectContext.performAndWait {
do {
// Saves the data from the child to the main context to be stored properly
try self.appDelegate.managedObjectContext.save()
} catch {
print("Failure to save parent context when adding last file name: \(error)")
}
}
} catch {
print("Failure to save child context when adding last file name: \(error)")
}
}
}
/// Adds a trackpoint to Core Data
///
/// A track segment ID should also be provided, such that trackpoints would be seperated in their track segments when recovered.
/// - Parameters:
/// - trackpoint: the trackpoint meant to be added to Core Data
/// - Id: track segment ID that the trackpoint originally was in.
///
func add(toCoreData trackpoint: GPXTrackPoint, withTrackSegmentID Id: Int) {
let childManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Creates the link between child and parent
childManagedObjectContext.parent = appDelegate.managedObjectContext
childManagedObjectContext.perform {
print("Core Data Helper: Add trackpoint with id: \(self.trackpointId)")
// swiftlint:disable:next force_cast
let pt = NSEntityDescription.insertNewObject(forEntityName: "CDTrackpoint", into: childManagedObjectContext) as! CDTrackpoint
guard let elevation = trackpoint.elevation else { return }
guard let latitude = trackpoint.latitude else { return }
guard let longitude = trackpoint.longitude else { return }
pt.elevation = elevation
pt.latitude = latitude
pt.longitude = longitude
pt.time = trackpoint.time
pt.trackpointId = self.trackpointId
pt.trackSegmentId = Int64(Id)
// Serialization of trackpoint
do {
let serialized = try JSONEncoder().encode(trackpoint)
pt.serialized = serialized
} catch {
print("Core Data Helper: serialization error when adding trackpoint: \(error)")
}
self.trackpointId += 1
do {
try childManagedObjectContext.save()
self.appDelegate.managedObjectContext.performAndWait {
do {
// Saves the data from the child to the main context to be stored properly
try self.appDelegate.managedObjectContext.save()
} catch {
print("Failure to save parent context when adding trackpoint: \(error)")
}
}
} catch {
print("Failure to save child context when adding trackpoint: \(error)")
}
}
}
/// Adds a waypoint to Core Data
///
/// - Parameters:
/// - waypoint: the waypoint meant to be added to Core Data
///
func add(toCoreData waypoint: GPXWaypoint) {
let waypointChildManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Creates the link between child and parent
waypointChildManagedObjectContext.parent = appDelegate.managedObjectContext
waypointChildManagedObjectContext.perform {
print("Core Data Helper: Add waypoint with id: \(self.waypointId)")
// swiftlint:disable:next force_cast
let pt = NSEntityDescription.insertNewObject(forEntityName: "CDWaypoint", into: waypointChildManagedObjectContext) as! CDWaypoint
guard let latitude = waypoint.latitude else { return }
guard let longitude = waypoint.longitude else { return }
if let elevation = waypoint.elevation {
pt.elevation = elevation
} else {
pt.elevation = .greatestFiniteMagnitude
}
pt.name = waypoint.name
pt.desc = waypoint.desc
pt.latitude = latitude
pt.longitude = longitude
pt.time = waypoint.time
pt.waypointId = self.waypointId
// Serialization of trackpoint
do {
let serialized = try JSONEncoder().encode(waypoint)
pt.serialized = serialized
} catch {
print("Core Data Helper: serialization error when adding waypoint: \(error)")
}
self.waypointId += 1
do {
try waypointChildManagedObjectContext.save()
self.appDelegate.managedObjectContext.performAndWait {
do {
// Saves the data from the child to the main context to be stored properly
try self.appDelegate.managedObjectContext.save()
} catch {
print("Failure to save parent context when adding waypoint: \(error)")
}
}
} catch {
print("Failure to save parent context when adding waypoint: \(error)")
}
}
}
// MARK: Update Core Data
/// Updates a previously added waypoint to Core Data
///
/// The waypoint at the given index will be updated accordingly.
/// - Parameters:
/// - updatedWaypoint: the waypoint meant to replace a already added, Core Data waypoint.
/// - index: the waypoint that is meant to be replaced/updated to newer data.
///
func update(toCoreData updatedWaypoint: GPXWaypoint, from index: Int) {
let privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateManagedObjectContext.parent = appDelegate.managedObjectContext
// Creates a fetch request
let wptFetchRequest = NSFetchRequest<CDWaypoint>(entityName: "CDWaypoint")
let asynchronousWaypointFetchRequest = NSAsynchronousFetchRequest(fetchRequest: wptFetchRequest) { asynchronousFetchResult in
print("Core Data Helper: updating waypoint in Core Data")
// Retrieves an array of points from Core Data
guard let waypointResults = asynchronousFetchResult.finalResult else { return }
privateManagedObjectContext.perform {
let objectID = waypointResults[index].objectID
guard let pt = self.appDelegate.managedObjectContext.object(with: objectID) as? CDWaypoint else { return }
guard let latitude = updatedWaypoint.latitude else { return }
guard let longitude = updatedWaypoint.longitude else { return }
if let elevation = updatedWaypoint.elevation {
pt.elevation = elevation
} else {
pt.elevation = .greatestFiniteMagnitude
}
pt.name = updatedWaypoint.name
pt.desc = updatedWaypoint.desc
pt.latitude = latitude
pt.longitude = longitude
do {
try privateManagedObjectContext.save()
self.appDelegate.managedObjectContext.performAndWait {
do {
// Saves the changes from the child to the main context to be applied properly
try self.appDelegate.managedObjectContext.save()
} catch {
print("Failure to update and save waypoint to parent context: \(error)")
}
}
} catch {
print("Failure to update and save waypoint to context at child context: \(error)")
}
}
}
do {
try privateManagedObjectContext.execute(asynchronousWaypointFetchRequest)
} catch {
print("NSAsynchronousFetchRequest (for finding updatable waypoint) error: \(error)")
}
}
// MARK: Retrieval From Core Data
/// Retrieves everything from Core Data
///
/// Currently, it retrieves CDTrackpoint, CDWaypoint and CDRoot,
/// to process from those Core Data types to CoreGPX types such as GPXTrackPoint, GPXWaypoint, etc.
///
/// It will also call on crashFileRecovery() method to continue the next procudure.
///
func retrieveFromCoreData() {
let privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateManagedObjectContext.parent = appDelegate.managedObjectContext
do {
// Note: it appears that the actual object context execution happens after all of this, probably due to its async nature.
try privateManagedObjectContext.execute(rootFetchRequest())
try privateManagedObjectContext.execute(trackPointFetchRequest())
try privateManagedObjectContext.execute(waypointFetchRequest())
} catch let error {
print("NSAsynchronousFetchRequest (fetch request for recovery) error: \(error)")
}
}
// MARK: Delete from Core Data
/// Delete Waypoint from index
///
/// - Parameters:
/// - index: index of the waypoint that is meant to be deleted.
///
func deleteWaypoint(fromCoreDataAt index: Int) {
let privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateManagedObjectContext.parent = appDelegate.managedObjectContext
// Creates a fetch request
let wptFetchRequest = NSFetchRequest<CDWaypoint>(entityName: "CDWaypoint")
wptFetchRequest.includesPropertyValues = false
let asynchronousWaypointFetchRequest = NSAsynchronousFetchRequest(fetchRequest: wptFetchRequest) { asynchronousFetchResult in
print("Core Data Helper: delete waypoint from Core Data at index: \(index)")
// Retrieves an array of points from Core Data
guard let waypointResults = asynchronousFetchResult.finalResult else { return }
privateManagedObjectContext.delete(waypointResults[index])
do {
try privateManagedObjectContext.save()
self.appDelegate.managedObjectContext.performAndWait {
do {
// Saves the changes from the child to the main context to be applied properly
try self.appDelegate.managedObjectContext.save()
} catch {
print("Failure to save context (when deleting waypoint): \(error)")
}
}
} catch {
print("Failure to save context at child context (when deleting waypoint): \(error)")
}
}
do {
try privateManagedObjectContext.execute(asynchronousWaypointFetchRequest)
} catch let error {
print("NSAsynchronousFetchRequest (for finding deletable waypoint) error: \(error)")
}
}
/// Delete all objects of entity given as parameter in Core Data.
func coreDataDeleteAll<T: NSManagedObject>(of type: T.Type) {
print("Core Data Helper: Batch Delete \(T.self) from Core Data")
if #available(iOS 10.0, *) {
modernBatchDelete(of: T.self)
} else { // for pre iOS 9 (less efficient, load in memory before removal)
legacyBatchDelete(of: T.self)
}
}
// MARK: Handles recovered data
/// Prompts user on what to do with recovered data
///
/// Adds all the 'recovered' content retrieved earlier to newly initialized `GPXRoot`.
/// Deletes and clears core data stuff after user decision is made.
///
/// Currently, there are three user decisions allowed:
/// - To continue last session, which loads the recovered data including previous file data (if applicable) on the map.
/// - To save recovered data silently in background and start a fresh new session immediately.
/// - To delete and ignore recovered data, to start a fresh new session instead.
///
func crashFileRecovery() {
DispatchQueue.global().async {
// checks if trackpoint and waypoint are available
if self.currentSegment.points.count > 0 || self.waypoints.count > 0 {
let root: GPXRoot
let track = GPXTrack()
// will load file if file was resumed before crash
if self.lastFileName != "" {
let gpx = GPXFileManager.URLForFilename(self.lastFileName)
let parsedRoot = GPXParser(withURL: gpx)?.parsedData()
root = parsedRoot ?? GPXRoot(creator: kGPXCreatorString)
} else {
root = GPXRoot(creator: kGPXCreatorString)
}
// generates a GPXRoot from recovered data
if self.isContinued && self.tracksegments.count >= (self.lastTracksegmentId + 1) {
// Check if there was a tracksegment
if root.tracks.last?.segments.count == 0 {
root.tracks.last?.add(trackSegment: GPXTrackSegment())
}
// if gpx is saved, but further trkpts are added after save, and crashed, trkpt are appended, not adding to new trkseg.
root.tracks.last?.segments[Int(self.lastTracksegmentId)].add(trackpoints: self.tracksegments.first!.points)
self.tracksegments.remove(at: 0)
} else {
track.segments = self.tracksegments
root.add(track: track)
}
root.waypoints = self.waypoints
// asks user on what to do with recovered data
DispatchQueue.main.sync {
NotificationCenter.default.post(name: .loadRecoveredFile, object: nil,
userInfo: ["recoveredRoot": root, "fileName": self.lastFileName])
let toastMessage = NSLocalizedString("LAST_SESSION_LOADED",
comment: "the filename displayed after the text") + " \n" + self.lastFileName + ".gpx"
Toast.regular(toastMessage, position: .top)
}
} else {
// no recovery file will be generated if nothing is recovered (or did not crash).
}
}
}
/// saves recovered data to a gpx file, silently, without loading on map.
func saveFile(from gpx: GPXRoot, andIfAvailable lastfileName: String) {
// date format same as usual.
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MMM-yyyy-HHmm"
var fileName = String()
if lastfileName != "" {
fileName = lastfileName
} else if let lastTrkptDate = gpx.tracks.last?.segments.last?.points.last?.time {
fileName = dateFormatter.string(from: lastTrkptDate)
} else {
// File name's date will be as of recovery time, not of crash time.
fileName = dateFormatter.string(from: Date())
}
let recoveredFileName = "recovery-\(fileName)"
let gpxString = gpx.gpx()
// Save the recovered file.
GPXFileManager.save(recoveredFileName, gpxContents: gpxString)
print("File \(recoveredFileName) was recovered from previous session, prior to unexpected crash/exit")
// clear aft save.
self.clearAll()
self.coreDataDeleteAll(of: CDRoot.self)
}
// MARK: Reset & Clear
/// Resets trackpoints and waypoints Id
///
/// the Id is to ensure that when retrieving the entities, the order remains.
/// This is important to ensure that the resulting recovery file has the correct order.
func resetIds() {
self.trackpointId = 0
self.waypointId = 0
self.tracksegmentId = 0
}
/// Clear all arrays and current segment after recovery.
func clearObjects() {
self.tracksegments = []
self.waypoints = []
self.currentSegment = GPXTrackSegment()
}
func clearAllExceptWaypoints() {
// once file recovery is completed, Core Data stored items are deleted.
self.coreDataDeleteAll(of: CDTrackpoint.self)
// once file recovery is completed, arrays are cleared.
self.tracksegments = []
// current segment should be 'reset' as well
self.currentSegment = GPXTrackSegment()
// reset order sorting ids
self.trackpointId = 0
self.tracksegmentId = 0
}
/// clears all
func clearAll() {
// once file recovery is completed, Core Data stored items are deleted.
self.coreDataDeleteAll(of: CDTrackpoint.self)
self.coreDataDeleteAll(of: CDWaypoint.self)
// once file recovery is completed, arrays are cleared.
self.clearObjects()
// current segment should be 'reset' as well
self.currentSegment = GPXTrackSegment()
// reset order sorting ids
self.resetIds()
}
}