SettingsStore.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. //
  2. // SettingsStore.swift
  3. // LoopKit
  4. //
  5. // Created by Darin Krauss on 10/14/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import os.log
  9. import Foundation
  10. import CoreData
  11. import HealthKit
  12. public protocol SettingsStoreDelegate: AnyObject {
  13. /**
  14. Informs the delegate that the settings store has updated settings data.
  15. - Parameter settingsStore: The settings store that has updated settings data.
  16. */
  17. func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore)
  18. }
  19. public class SettingsStore {
  20. public weak var delegate: SettingsStoreDelegate?
  21. public private(set) var latestSettings: StoredSettings? {
  22. get {
  23. return lockedLatestSettings.value
  24. }
  25. set {
  26. lockedLatestSettings.value = newValue
  27. }
  28. }
  29. private let lockedLatestSettings: Locked<StoredSettings?>
  30. private let store: PersistenceController
  31. private let expireAfter: TimeInterval
  32. private let dataAccessQueue = DispatchQueue(label: "com.loopkit.SettingsStore.dataAccessQueue", qos: .utility)
  33. private let log = OSLog(category: "SettingsStore")
  34. public init(store: PersistenceController, expireAfter: TimeInterval) {
  35. self.store = store
  36. self.expireAfter = expireAfter
  37. self.lockedLatestSettings = Locked(nil)
  38. dataAccessQueue.sync {
  39. self.store.managedObjectContext.performAndWait {
  40. let storedRequest: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  41. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: false)]
  42. storedRequest.fetchLimit = 1
  43. do {
  44. let stored = try self.store.managedObjectContext.fetch(storedRequest)
  45. self.latestSettings = stored.first.flatMap { self.decodeSettings(fromData: $0.data) }
  46. } catch let error {
  47. self.log.error("Error fetching latest settings: %@", String(describing: error))
  48. return
  49. }
  50. }
  51. }
  52. }
  53. func storeSettings(_ settings: StoredSettings) async {
  54. await withCheckedContinuation { continuation in
  55. storeSettings(settings) { (_) in
  56. continuation.resume()
  57. }
  58. }
  59. }
  60. public func storeSettings(_ settings: StoredSettings, completion: @escaping (Error?) -> Void) {
  61. dataAccessQueue.async {
  62. var error: Error?
  63. if let data = self.encodeSettings(settings) {
  64. self.store.managedObjectContext.performAndWait {
  65. let object = SettingsObject(context: self.store.managedObjectContext)
  66. object.data = data
  67. object.date = settings.date
  68. error = self.store.save()
  69. }
  70. }
  71. self.latestSettings = settings
  72. self.purgeExpiredSettings()
  73. completion(error)
  74. }
  75. }
  76. public var expireDate: Date {
  77. return Date(timeIntervalSinceNow: -expireAfter)
  78. }
  79. private func purgeExpiredSettings() {
  80. guard let latestSettings = latestSettings else {
  81. return
  82. }
  83. guard expireDate < latestSettings.date else {
  84. return
  85. }
  86. purgeSettingsObjects(before: expireDate)
  87. }
  88. public func purgeSettings(before date: Date, completion: @escaping (Error?) -> Void) {
  89. dataAccessQueue.async {
  90. self.purgeSettingsObjects(before: date, completion: completion)
  91. }
  92. }
  93. private func purgeSettingsObjects(before date: Date, completion: ((Error?) -> Void)? = nil) {
  94. dispatchPrecondition(condition: .onQueue(dataAccessQueue))
  95. var purgeError: Error?
  96. store.managedObjectContext.performAndWait {
  97. do {
  98. let count = try self.store.managedObjectContext.purgeObjects(of: SettingsObject.self, matching: NSPredicate(format: "date < %@", date as NSDate))
  99. if count > 0 {
  100. self.log.default("Purged %d SettingsObjects", count)
  101. }
  102. } catch let error {
  103. self.log.error("Unable to purge SettingsObjects: %{public}@", String(describing: error))
  104. purgeError = error
  105. }
  106. }
  107. if let purgeError = purgeError {
  108. completion?(purgeError)
  109. return
  110. }
  111. delegate?.settingsStoreHasUpdatedSettingsData(self)
  112. completion?(nil)
  113. }
  114. private static var encoder: PropertyListEncoder = {
  115. let encoder = PropertyListEncoder()
  116. encoder.outputFormat = .binary
  117. return encoder
  118. }()
  119. private func encodeSettings(_ settings: StoredSettings) -> Data? {
  120. do {
  121. return try Self.encoder.encode(settings)
  122. } catch let error {
  123. self.log.error("Error encoding StoredSettings: %@", String(describing: error))
  124. return nil
  125. }
  126. }
  127. private static var decoder = PropertyListDecoder()
  128. private func decodeSettings(fromData data: Data) -> StoredSettings? {
  129. do {
  130. return try Self.decoder.decode(StoredSettings.self, from: data)
  131. } catch let error {
  132. self.log.error("Error decoding StoredSettings: %@", String(describing: error))
  133. return nil
  134. }
  135. }
  136. }
  137. extension SettingsStore {
  138. public struct QueryAnchor: Equatable, RawRepresentable {
  139. public typealias RawValue = [String: Any]
  140. internal var modificationCounter: Int64
  141. public init() {
  142. self.modificationCounter = 0
  143. }
  144. public init?(rawValue: RawValue) {
  145. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  146. return nil
  147. }
  148. self.modificationCounter = modificationCounter
  149. }
  150. public var rawValue: RawValue {
  151. var rawValue: RawValue = [:]
  152. rawValue["modificationCounter"] = modificationCounter
  153. return rawValue
  154. }
  155. }
  156. public enum SettingsQueryResult {
  157. case success(QueryAnchor, [StoredSettings])
  158. case failure(Error)
  159. }
  160. public func executeSettingsQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (SettingsQueryResult) -> Void) {
  161. dataAccessQueue.async {
  162. var queryAnchor = queryAnchor ?? QueryAnchor()
  163. var queryResult = [StoredSettingsData]()
  164. var queryError: Error?
  165. guard limit > 0 else {
  166. completion(.success(queryAnchor, []))
  167. return
  168. }
  169. self.store.managedObjectContext.performAndWait {
  170. let storedRequest: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  171. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  172. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  173. storedRequest.fetchLimit = limit
  174. do {
  175. let stored = try self.store.managedObjectContext.fetch(storedRequest)
  176. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  177. queryAnchor.modificationCounter = modificationCounter
  178. }
  179. queryResult.append(contentsOf: stored.compactMap { StoredSettingsData(date: $0.date, data: $0.data) })
  180. } catch let error {
  181. queryError = error
  182. return
  183. }
  184. }
  185. if let queryError = queryError {
  186. completion(.failure(queryError))
  187. return
  188. }
  189. // Decoding a large number of settings can be very CPU intensive and may take considerable wall clock time.
  190. // Do not block SettingsStore dataAccessQueue. Perform work and callback in global utility queue.
  191. DispatchQueue.global(qos: .utility).async {
  192. completion(.success(queryAnchor, queryResult.compactMap { self.decodeSettings(fromData: $0.data) }))
  193. }
  194. }
  195. }
  196. }
  197. public struct StoredSettingsData {
  198. public let date: Date
  199. public let data: Data
  200. public init(date: Date, data: Data) {
  201. self.date = date
  202. self.data = data
  203. }
  204. }
  205. public struct StoredSettings: Equatable {
  206. public let date: Date
  207. public var controllerTimeZone: TimeZone
  208. public let dosingEnabled: Bool
  209. public let glucoseTargetRangeSchedule: GlucoseRangeSchedule?
  210. public let preMealTargetRange: ClosedRange<HKQuantity>?
  211. public let workoutTargetRange: ClosedRange<HKQuantity>?
  212. public let overridePresets: [TemporaryScheduleOverridePreset]?
  213. public let scheduleOverride: TemporaryScheduleOverride?
  214. public let preMealOverride: TemporaryScheduleOverride?
  215. public let maximumBasalRatePerHour: Double?
  216. public let maximumBolus: Double?
  217. public let suspendThreshold: GlucoseThreshold?
  218. public let deviceToken: String?
  219. public let insulinType: InsulinType?
  220. public let defaultRapidActingModel: StoredInsulinModel?
  221. public let basalRateSchedule: BasalRateSchedule?
  222. public let insulinSensitivitySchedule: InsulinSensitivitySchedule?
  223. public let carbRatioSchedule: CarbRatioSchedule?
  224. public var notificationSettings: NotificationSettings?
  225. public let controllerDevice: ControllerDevice?
  226. public let cgmDevice: HKDevice?
  227. public let pumpDevice: HKDevice?
  228. // This is the user's display preference glucose unit. TODO: Rename?
  229. public let bloodGlucoseUnit: HKUnit?
  230. public let automaticDosingStrategy: AutomaticDosingStrategy
  231. public let syncIdentifier: UUID
  232. public init(date: Date = Date(),
  233. controllerTimeZone: TimeZone = TimeZone.current,
  234. dosingEnabled: Bool = false,
  235. glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  236. preMealTargetRange: ClosedRange<HKQuantity>? = nil,
  237. workoutTargetRange: ClosedRange<HKQuantity>? = nil,
  238. overridePresets: [TemporaryScheduleOverridePreset]? = nil,
  239. scheduleOverride: TemporaryScheduleOverride? = nil,
  240. preMealOverride: TemporaryScheduleOverride? = nil,
  241. maximumBasalRatePerHour: Double? = nil,
  242. maximumBolus: Double? = nil,
  243. suspendThreshold: GlucoseThreshold? = nil,
  244. deviceToken: String? = nil,
  245. insulinType: InsulinType? = nil,
  246. defaultRapidActingModel: StoredInsulinModel? = nil,
  247. basalRateSchedule: BasalRateSchedule? = nil,
  248. insulinSensitivitySchedule: InsulinSensitivitySchedule? = nil,
  249. carbRatioSchedule: CarbRatioSchedule? = nil,
  250. notificationSettings: NotificationSettings? = nil,
  251. controllerDevice: ControllerDevice? = nil,
  252. cgmDevice: HKDevice? = nil,
  253. pumpDevice: HKDevice? = nil,
  254. bloodGlucoseUnit: HKUnit? = nil,
  255. automaticDosingStrategy: AutomaticDosingStrategy = .tempBasalOnly,
  256. syncIdentifier: UUID = UUID()) {
  257. self.date = date
  258. self.controllerTimeZone = controllerTimeZone
  259. self.dosingEnabled = dosingEnabled
  260. self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
  261. self.preMealTargetRange = preMealTargetRange
  262. self.workoutTargetRange = workoutTargetRange
  263. self.overridePresets = overridePresets
  264. self.scheduleOverride = scheduleOverride
  265. self.preMealOverride = preMealOverride
  266. self.maximumBasalRatePerHour = maximumBasalRatePerHour
  267. self.maximumBolus = maximumBolus
  268. self.suspendThreshold = suspendThreshold
  269. self.deviceToken = deviceToken
  270. self.insulinType = insulinType
  271. self.defaultRapidActingModel = defaultRapidActingModel
  272. self.basalRateSchedule = basalRateSchedule
  273. self.insulinSensitivitySchedule = insulinSensitivitySchedule
  274. self.carbRatioSchedule = carbRatioSchedule
  275. self.notificationSettings = notificationSettings
  276. self.controllerDevice = controllerDevice
  277. self.cgmDevice = cgmDevice
  278. self.pumpDevice = pumpDevice
  279. self.bloodGlucoseUnit = bloodGlucoseUnit
  280. self.automaticDosingStrategy = automaticDosingStrategy
  281. self.syncIdentifier = syncIdentifier
  282. }
  283. }
  284. extension StoredSettings: Codable {
  285. fileprivate static let codingGlucoseUnit = HKUnit.milligramsPerDeciliter
  286. public init(from decoder: Decoder) throws {
  287. let container = try decoder.container(keyedBy: CodingKeys.self)
  288. let bloodGlucoseUnit = HKUnit(from: try container.decode(String.self, forKey: .bloodGlucoseUnit))
  289. self.init(date: try container.decode(Date.self, forKey: .date),
  290. controllerTimeZone: try container.decode(TimeZone.self, forKey: .controllerTimeZone),
  291. dosingEnabled: try container.decode(Bool.self, forKey: .dosingEnabled),
  292. glucoseTargetRangeSchedule: try container.decodeIfPresent(GlucoseRangeSchedule.self, forKey: .glucoseTargetRangeSchedule),
  293. preMealTargetRange: try container.decodeIfPresent(DoubleRange.self, forKey: .preMealTargetRange)?.quantityRange(for: bloodGlucoseUnit),
  294. workoutTargetRange: try container.decodeIfPresent(DoubleRange.self, forKey: .workoutTargetRange)?.quantityRange(for: bloodGlucoseUnit),
  295. overridePresets: try container.decodeIfPresent([TemporaryScheduleOverridePreset].self, forKey: .overridePresets),
  296. scheduleOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .scheduleOverride),
  297. preMealOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .preMealOverride),
  298. maximumBasalRatePerHour: try container.decodeIfPresent(Double.self, forKey: .maximumBasalRatePerHour),
  299. maximumBolus: try container.decodeIfPresent(Double.self, forKey: .maximumBolus),
  300. suspendThreshold: try container.decodeIfPresent(GlucoseThreshold.self, forKey: .suspendThreshold),
  301. deviceToken: try container.decodeIfPresent(String.self, forKey: .deviceToken),
  302. insulinType: try container.decodeIfPresent(InsulinType.self, forKey: .insulinType),
  303. defaultRapidActingModel: try container.decodeIfPresent(StoredInsulinModel.self, forKey: .defaultRapidActingModel),
  304. basalRateSchedule: try container.decodeIfPresent(BasalRateSchedule.self, forKey: .basalRateSchedule),
  305. insulinSensitivitySchedule: try container.decodeIfPresent(InsulinSensitivitySchedule.self, forKey: .insulinSensitivitySchedule),
  306. carbRatioSchedule: try container.decodeIfPresent(CarbRatioSchedule.self, forKey: .carbRatioSchedule),
  307. notificationSettings: try container.decodeIfPresent(NotificationSettings.self, forKey: .notificationSettings),
  308. controllerDevice: try container.decodeIfPresent(ControllerDevice.self, forKey: .controllerDevice),
  309. cgmDevice: try container.decodeIfPresent(CodableDevice.self, forKey: .cgmDevice)?.device,
  310. pumpDevice: try container.decodeIfPresent(CodableDevice.self, forKey: .pumpDevice)?.device,
  311. bloodGlucoseUnit: bloodGlucoseUnit,
  312. automaticDosingStrategy: try container.decodeIfPresent(AutomaticDosingStrategy.self, forKey: .automaticDosingStrategy) ?? .tempBasalOnly,
  313. syncIdentifier: try container.decode(UUID.self, forKey: .syncIdentifier))
  314. }
  315. public func encode(to encoder: Encoder) throws {
  316. let bloodGlucoseUnit = self.bloodGlucoseUnit ?? StoredSettings.codingGlucoseUnit
  317. var container = encoder.container(keyedBy: CodingKeys.self)
  318. try container.encode(date, forKey: .date)
  319. try container.encode(controllerTimeZone, forKey: .controllerTimeZone)
  320. try container.encode(dosingEnabled, forKey: .dosingEnabled)
  321. try container.encodeIfPresent(glucoseTargetRangeSchedule, forKey: .glucoseTargetRangeSchedule)
  322. try container.encodeIfPresent(preMealTargetRange?.doubleRange(for: bloodGlucoseUnit), forKey: .preMealTargetRange)
  323. try container.encodeIfPresent(workoutTargetRange?.doubleRange(for: bloodGlucoseUnit), forKey: .workoutTargetRange)
  324. try container.encodeIfPresent(overridePresets, forKey: .overridePresets)
  325. try container.encodeIfPresent(scheduleOverride, forKey: .scheduleOverride)
  326. try container.encodeIfPresent(preMealOverride, forKey: .preMealOverride)
  327. try container.encodeIfPresent(maximumBasalRatePerHour, forKey: .maximumBasalRatePerHour)
  328. try container.encodeIfPresent(maximumBolus, forKey: .maximumBolus)
  329. try container.encodeIfPresent(suspendThreshold, forKey: .suspendThreshold)
  330. try container.encodeIfPresent(insulinType, forKey: .insulinType)
  331. try container.encodeIfPresent(deviceToken, forKey: .deviceToken)
  332. try container.encodeIfPresent(defaultRapidActingModel, forKey: .defaultRapidActingModel)
  333. try container.encodeIfPresent(basalRateSchedule, forKey: .basalRateSchedule)
  334. try container.encodeIfPresent(insulinSensitivitySchedule, forKey: .insulinSensitivitySchedule)
  335. try container.encodeIfPresent(carbRatioSchedule, forKey: .carbRatioSchedule)
  336. try container.encodeIfPresent(notificationSettings, forKey: .notificationSettings)
  337. try container.encodeIfPresent(controllerDevice, forKey: .controllerDevice)
  338. try container.encodeIfPresent(cgmDevice.map { CodableDevice($0) }, forKey: .cgmDevice)
  339. try container.encodeIfPresent(pumpDevice.map { CodableDevice($0) }, forKey: .pumpDevice)
  340. try container.encode(bloodGlucoseUnit.unitString, forKey: .bloodGlucoseUnit)
  341. try container.encode(automaticDosingStrategy, forKey: .automaticDosingStrategy)
  342. try container.encode(syncIdentifier, forKey: .syncIdentifier)
  343. }
  344. public struct ControllerDevice: Codable, Equatable {
  345. public let name: String
  346. public let systemName: String
  347. public let systemVersion: String
  348. public let model: String
  349. public let modelIdentifier: String
  350. public init(name: String, systemName: String, systemVersion: String, model: String, modelIdentifier: String) {
  351. self.name = name
  352. self.systemName = systemName
  353. self.systemVersion = systemVersion
  354. self.model = model
  355. self.modelIdentifier = modelIdentifier
  356. }
  357. }
  358. private enum CodingKeys: String, CodingKey {
  359. case date
  360. case controllerTimeZone
  361. case dosingEnabled
  362. case glucoseTargetRangeSchedule
  363. case preMealTargetRange
  364. case workoutTargetRange
  365. case overridePresets
  366. case scheduleOverride
  367. case preMealOverride
  368. case maximumBasalRatePerHour
  369. case maximumBolus
  370. case suspendThreshold
  371. case deviceToken
  372. case insulinType
  373. case defaultRapidActingModel
  374. case basalRateSchedule
  375. case insulinSensitivitySchedule
  376. case carbRatioSchedule
  377. case notificationSettings
  378. case controllerDevice
  379. case cgmDevice
  380. case pumpDevice
  381. case bloodGlucoseUnit
  382. case automaticDosingStrategy
  383. case syncIdentifier
  384. }
  385. }
  386. // MARK: - Critical Event Log Export
  387. extension SettingsStore: CriticalEventLog {
  388. private var exportProgressUnitCountPerObject: Int64 { 11 }
  389. private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
  390. public var exportName: String { "Settings.json" }
  391. public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
  392. var result: Result<Int64, Error>?
  393. self.store.managedObjectContext.performAndWait {
  394. do {
  395. let request: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  396. request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
  397. let objectCount = try self.store.managedObjectContext.count(for: request)
  398. result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
  399. } catch let error {
  400. result = .failure(error)
  401. }
  402. }
  403. return result!
  404. }
  405. public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
  406. let encoder = JSONStreamEncoder(stream: stream)
  407. var modificationCounter: Int64 = 0
  408. var fetching = true
  409. var error: Error?
  410. while fetching && error == nil {
  411. self.store.managedObjectContext.performAndWait {
  412. do {
  413. guard !progress.isCancelled else {
  414. throw CriticalEventLogError.cancelled
  415. }
  416. let request: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  417. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
  418. self.exportDatePredicate(startDate: startDate, endDate: endDate)])
  419. request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  420. request.fetchLimit = self.exportFetchLimit
  421. let objects = try self.store.managedObjectContext.fetch(request)
  422. if objects.isEmpty {
  423. fetching = false
  424. return
  425. }
  426. try encoder.encode(objects)
  427. modificationCounter = objects.last!.modificationCounter
  428. progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
  429. } catch let fetchError {
  430. error = fetchError
  431. }
  432. }
  433. }
  434. if let closeError = encoder.close(), error == nil {
  435. error = closeError
  436. }
  437. return error
  438. }
  439. private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
  440. var predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  441. if let endDate = endDate {
  442. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "date < %@", endDate as NSDate)])
  443. }
  444. return predicate
  445. }
  446. }
  447. // MARK: - Core Data (Bulk) - TEST ONLY
  448. extension SettingsStore {
  449. public func addStoredSettings(settings: [StoredSettings], completion: @escaping (Error?) -> Void) {
  450. guard !settings.isEmpty else {
  451. completion(nil)
  452. return
  453. }
  454. dataAccessQueue.async {
  455. var error: Error?
  456. self.store.managedObjectContext.performAndWait {
  457. for setting in settings {
  458. guard let data = self.encodeSettings(setting) else {
  459. continue
  460. }
  461. let object = SettingsObject(context: self.store.managedObjectContext)
  462. object.data = data
  463. object.date = setting.date
  464. }
  465. error = self.store.save()
  466. }
  467. guard error == nil else {
  468. completion(error)
  469. return
  470. }
  471. self.log.info("Added %d SettingsObjects", settings.count)
  472. self.delegate?.settingsStoreHasUpdatedSettingsData(self)
  473. completion(nil)
  474. }
  475. }
  476. }