Skip to content

Instantly share code, notes, and snippets.

@ailinykh
Created June 16, 2023 07:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ailinykh/1f5bbd85ef7565cd967b11eabae2a60b to your computer and use it in GitHub Desktop.
Save ailinykh/1f5bbd85ef7565cd967b11eabae2a60b to your computer and use it in GitHub Desktop.
analytic
//
// Analytic.swift
// YouDrive
//
// Created by Anton Ilinykh on 27.06.2022.
//
import Foundation
enum Event: String {
case appLaunch = "application_launched"
}
typealias Payload = [String: String]
typealias Entity = (event: Event, payload: Payload)
protocol Analytic {
func handle(event: Event, payload: Payload)
}
protocol AnalyticAPI {
func send(events: [Entity], completion: @escaping (Bool) -> Void)
}
protocol AnalyticStorage {
func save(events: [Entity], completion: @escaping (Bool) -> Void)
func load(completion: @escaping (Result<[Entity], Error>) -> Void)
}
final class AnalyticHTTPAPI: AnalyticAPI {
let url: URL
let session: URLSession
init(url: URL, session: URLSession = .shared) {
self.url = url
self.session = session
}
func send(events: [Entity], completion: @escaping (Bool) -> Void) {
var request = URLRequest(url: url)
request.method = .post
session.dataTask(with: request) { data, response, error in
print(data, response, error)
}.resume()
}
}
//
// AnalyticCoreDataStorage.swift
// YouDrive
//
// Created by Anton Ilinykh on 27.06.2022.
//
import CoreData
import Foundation
final class AnalyticCoreDataStorage {
private let resourceName: String
private let queue = DispatchQueue(label: "com.example.AnalyticCoreDataStorage")
private lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls.last! //swiftlint:disable:this force_unwrapping
}()
private lazy var managedObjectModel: NSManagedObjectModel = {
guard
let modelURL = Bundle.main.url(
forResource: resourceName,
withExtension: "momd"
),
let model = NSManagedObjectModel(contentsOf: modelURL)
else {
Log.error("Could not create NSManagedObjectModel")
return NSManagedObjectModel()
}
return model
}()
private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let url = applicationDocumentsDirectory.appendingPathComponent(resourceName + ".sqlite")
do {
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
try coordinator.addPersistentStore(
ofType: NSSQLiteStoreType,
configurationName: nil,
at: url,
options: options
)
} catch {
Log.error("migration fails!\n\(error)")
}
return coordinator
}()
private lazy var persistentContainer = NSPersistentContainer(name: resourceName)
private lazy var context = persistentContainer.newBackgroundContext()
init(resourceName: String = "analytics") {
self.resourceName = resourceName
}
// MARK: private
private func loadPersistentContainer() {
loadPersistentStores(resetOnFail: true)
}
private func loadPersistentStores(resetOnFail: Bool = false) {
persistentContainer.loadPersistentStores { description, error in
if let error = error /*as NSError?*/ {
if resetOnFail {
Log.warning("loadPersistentStores error: \(error)")
Log.warning("try to reset")
self.destroyPersistentStore(description: description)
} else {
Log.error("loadPersistentStores reset error: \(error)")
}
}
}
}
private func destroyPersistentStore(description: NSPersistentStoreDescription) {
if let url = description.url {
do {
try persistentContainer.persistentStoreCoordinator.destroyPersistentStore(
at: url,
ofType: NSSQLiteStoreType,
options: nil
)
loadPersistentStores()
} catch {
Log.error("loadPersistentStores error: \(error)")
}
}
}
}
extension AnalyticCoreDataStorage: AnalyticStorage {
func save(events: [Entity], completion: @escaping (Bool) -> Void) {
guard !events.isEmpty else { return }
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let context = context
queue.async {
events.forEach { entity in
if let payload = try? encoder.encode(entity.payload) {
context.performAndWait {
let model = AnalyticsCoreDataModel(context: context)
model.id = UUID()
model.date = Date()
model.event = entity.event.rawValue
model.payload = payload
}
}
}
context.perform {
if context.hasChanges {
guard let coordinator = context.persistentStoreCoordinator,
!coordinator.persistentStores.isEmpty else {
Log.error("NSPersistentStoreCoordinator has no persistent stores")
completion(false)
return
}
do {
try context.save()
completion(true)
} catch {
Log.error("Unknown saving error!\n\(error)")
completion(false)
}
}
}
}
}
func load(completion: @escaping (Result<[Entity], Error>) -> Void) {
let decoder = JSONDecoder()
let request = NSFetchRequest<AnalyticsCoreDataModel>(entityName: AnalyticsCoreDataModel.className)
context.perform {
do {
let objects = try request.execute()
let entities: [Entity] = objects.compactMap {
guard
let event = Event(rawValue: $0.event),
let payload = try? decoder.decode(Payload.self, from: $0.payload)
else { return nil }
return Entity(event: event, payload: payload)
}
completion(.success(entities))
} catch {
Log.error("fetching error: \(error)")
completion(.failure(error))
}
}
}
}
@objc(AnalyticsCoreDataModel)
private final class AnalyticsCoreDataModel: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var date: Date
@NSManaged var event: String
@NSManaged var payload: Data
static var className: String {
String(describing: self)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment