Skip to content

Instantly share code, notes, and snippets.

@SergLam
Last active January 11, 2021 22:21
Show Gist options
  • Save SergLam/0935882e12e77d12ef1f10403124952a to your computer and use it in GitHub Desktop.
Save SergLam/0935882e12e77d12ef1f10403124952a to your computer and use it in GitHub Desktop.
Realm DataManager - generic operations + migration setup + thread-safe(!!!) read-write async operations
@UIApplicationMain
final class AppDelegate: UIResponder {
override init() {
RealmDAO.shared.configureMigration()
super.init()
}
}
// MARK: - UIApplicationDelegate
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}
import Foundation
import RealmSwift
protocol DatabaseCommandRepo {
var config: Realm.Configuration { get set }
var commandQueue: DispatchQueue { get set }
}
import Foundation
enum DatabaseError: Error, LocalizedError {
case databaseAlreadyInWriteTransaction
var description: String {
switch self {
case .databaseAlreadyInWriteTransaction:
return "Database already in a write transaction."
}
}
var errorDescription: String? {
return self.description
}
}
import Foundation
import RealmSwift
protocol DatabaseQueryRepo {
var config: Realm.Configuration { get set }
var queryQueue: DispatchQueue { get set }
}
import Foundation
import RealmSwift
final class MyUserDatabaseCommandRepo: DatabaseCommandRepo {
var config: Realm.Configuration
var commandQueue: DispatchQueue
init(config: Realm.Configuration, queue: DispatchQueue) {
self.config = config
self.commandQueue = queue
}
func updateCurrentUser(_ newUser: MyUserJSON, completion: @escaping VoidClosure) {
commandQueue.async(flags: .barrier) {
autoreleasepool{
do {
try RealmDAO.shared.realm.safeWrite {
RealmDAO.shared.myUserQueryRepo.getCurrentUser { userObj in
switch userObj {
case .none:
ErrorLoggerService.logWithTrace("Current user is nil")
completion()
case .some(let user):
user.update(user: newUser)
RealmDAO.shared.writeUnsafe(value: [user], policy: .all, onCompletion: {
completion()
})
}
}
}
} catch {
ErrorLoggerService.logWithTrace(error: error)
completion()
}
}
}
}
func updateCurrentUserShippingAddress(_ address: ShippingAddressJSON, completion: @escaping VoidClosure) {
commandQueue.async(flags: .barrier) { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.config)
let userObj = realm.objects(MyUser.self).first
switch userObj {
case .none:
ErrorLoggerService.logWithTrace("Current user is nil")
completion()
case .some(let user):
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in
guard let user = userRef else { return }
do {
try realm.safeWrite {
if let addressDao = user.shippingAddreses.first {
addressDao.update(address: address)
completion()
} else {
let new = ShippingAddressDAO(address: address)
user.shippingAddreses.append(new)
completion()
}
}
} catch {
let message: String = error.localizedDescription
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
}
func updateCurrentUserEmailAddress(_ email: String,
completion: @escaping VoidClosure) {
commandQueue.async(flags: .barrier) { [weak self] in
autoreleasepool{
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.config)
let userObj = realm.objects(MyUser.self).first
switch userObj {
case .none:
ErrorLoggerService.logWithTrace("Current user is nil")
completion()
case .some(let user):
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in
guard let user = userRef else { return }
do {
try realm.safeWrite {
user.email = email
completion()
}
} catch {
let message: String = error.localizedDescription
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
}
}
func updateCurrentUserDateOfBirth(_ date: Date, completion: @escaping VoidClosure) {
commandQueue.async(flags: .barrier) { [weak self] in
autoreleasepool{
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.config)
let userObj = realm.objects(MyUser.self).first
switch userObj {
case .none:
ErrorLoggerService.logWithTrace("Current user is nil")
completion()
case .some(let user):
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in
guard let user = userRef else { return }
do {
try realm.safeWrite {
user.dateOfBirth = date
completion()
}
} catch {
let message: String = error.localizedDescription
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
}
}
}
import Foundation
import RealmSwift
final class MyUserDatabaseQueryRepo: DatabaseQueryRepo {
var config: Realm.Configuration
var queryQueue: DispatchQueue
init(config: Realm.Configuration, queue: DispatchQueue) {
self.queryQueue = queue
self.config = config
}
func getCurrentUser(completion: @escaping TypeClosure<MyUser?>,
targetQueue: DispatchQueue = .main) {
queryQueue.async { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.config)
guard let user = realm.objects(MyUser.self).first else {
completion(nil)
return
}
let wrappedObj = ThreadSafeReference(to: user)
targetQueue.async {
autoreleasepool {
do {
let realm = try Realm(configuration: RealmDAO.shared.realm.configuration)
guard let obj = realm.resolve(wrappedObj) else {
completion(nil)
return // obj was deleted
}
completion(obj)
} catch {
ErrorLoggerService.logWithTrace(error.localizedDescription)
completion(nil)
}
}
}
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
}
}
import Foundation
final class PredicateBuilder {
static let shippingAddressDefaultFieldName = "isDefault"
// MARK: - Predicate creation methods
static func equalityPredicate(_ fieldName: String, _ value: Any) -> NSPredicate {
return NSPredicate(format: "\(fieldName) == %@", argumentArray: [value])
}
static func multipleEqualityPredicate(_ fieldsNames: [String], _ values: [Any]) -> NSPredicate {
assert(fieldsNames.count == values.count, "Field names and values count should be the same")
var predicateFormat = ""
for (index, name) in fieldsNames.enumerated() {
if index != fieldsNames.count - 1 {
predicateFormat.append("\(name) == %@ AND ")
} else {
predicateFormat.append("\(name) == %@")
}
}
return NSPredicate(format: predicateFormat, argumentArray: [values])
}
static func notEqualPredicate(_ fieldName: String, _ value: Any) -> NSPredicate {
return NSPredicate(format: "\(fieldName) != %@", argumentArray: [value])
}
static func buildPredicate(_ format: String, _ value: [Any]) -> NSPredicate {
return NSPredicate(format: format, argumentArray: value)
}
static func nilPredicate(_ fieldName: String) -> NSPredicate {
return NSPredicate(format: "\(fieldName) == nil")
}
static func notNilPredicate(_ fieldName: String) -> NSPredicate {
return NSPredicate(format: "\(fieldName) != nil")
}
}
import Foundation
import Realm
import RealmSwift
extension Realm {
/**
// https://github.com/realm/realm-cocoa/issues/4511
Terminating app due to uncaught exception 'RLMException', reason: 'The Realm is already in a write transaction'
*/
func safeWrite(_ block: (() throws -> Void)) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}
}
import Foundation
import Realm
import RealmSwift
extension Migration {
func hadProperty(onType typeName: String, property propertyName: String) -> Bool {
var hasPropery = false
self.enumerateObjects(ofType: typeName) { oldObject, _ in
hasPropery = oldObject?.objectSchema.properties.contains(where: { $0.name == propertyName }) ?? false
return
}
return hasPropery
}
func renamePropertyIfExists(onType typeName: String, from oldName: String, to newName: String) {
if hadProperty(onType: typeName, property: oldName) {
renameProperty(onType: typeName, from: oldName, to: newName)
}
}
}
import Foundation
import RealmSwift
final class RealmDAO {
static let shared = RealmDAO()
lazy var myUserQueryRepo: MyUserDatabaseQueryRepo = MyUserDatabaseQueryRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue)
lazy var myUserCommandRepo: MyUserDatabaseCommandRepo = MyUserDatabaseCommandRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue)
lazy var shippingAddressQueryRepo: ShippingAddressDatabaseQueryRepo = ShippingAddressDatabaseQueryRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue)
lazy var shippingAddressCommandRepo: ShippingAddressDatabaseCommandRepo = ShippingAddressDatabaseCommandRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue)
/**
Queue to read + write object synchroniously(reader-writer problem)
Always use it!
*/
private static let databaseOperationsQueue: DispatchQueue = DispatchQueue(label: "\(Environment.bundleId).realm-queue", qos: .userInteractive, attributes: [.concurrent], autoreleaseFrequency: .workItem, target: nil)
lazy var migrationExecutor: RealmDatabaseMigrationExecutor = RealmDatabaseMigrationExecutor()
lazy var realm: Realm = {
do {
return try Realm()
} catch {
let message: String = "Unable to create realm instance \(error)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}()
/**
Get this configuration each time when you need to perform CRUD operation
*/
private var configuration: Realm.Configuration {
return realm.configuration
}
var databaseFileURL: URL? {
return realm.configuration.fileURL
}
private init() {
}
/**
Domain specific operations should be here
*/
func performCommand(command: @escaping VoidClosure) {
RealmDAO.databaseOperationsQueue.async(flags: .barrier) {
command()
}
}
// MARK: - Base CRUD operations implemented with generics
/**
Use to write NEW UNMANAGED objects to database
*/
func writeUnsafe<T: Object>(value: [T],
policy: Realm.UpdatePolicy = .all,
onCompletion: @escaping VoidClosure) {
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in
guard let `self` = self else { return }
do {
let newRealm = try Realm(configuration: self.configuration)
let value = value.filter{ return $0.isInvalidated == false }
try newRealm.write {
newRealm.add(value, update: policy)
try newRealm.commitWrite()
onCompletion()
}
} catch {
let message: String = "\(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
onCompletion()
}
}
}
/**
Use to write in background for already existing objects in database
*/
func write<T: Object>(value: [T], completion: @escaping VoidClosure) {
let isManaged: Bool = value.first?.realm != nil
for obj in value {
if isManaged {
RealmDAO.shared.writeAsync(obj: obj, block: { realm, obj in
guard let objReference = obj else { return }
realm.add(objReference, update: .all)
completion()
})
} else {
RealmDAO.shared.writeUnsafe(value: value, onCompletion: {
completion()
})
}
}
}
func readAll<T: Object>(object: T.Type,
completion: @escaping ResultClosure<[T]>) {
RealmDAO.databaseOperationsQueue.async { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.configuration)
let readResults = realm.objects(T.self)
completion(.success(Array(readResults)))
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
completion(.failure(error))
preconditionFailure(message)
}
}
}
func readAllResult<T: Object>(object: T.Type,
completion: @escaping ResultClosure<RealmSwift.Results<T>>) {
RealmDAO.databaseOperationsQueue.async { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.configuration)
completion(.success(realm.objects(T.self)))
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
}
func deleteAll<T: Object>(_ class: T.Type, completion: @escaping VoidClosure) {
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.configuration)
let allObjects = realm.objects(T.self)
try realm.write {
realm.delete(allObjects)
completion()
}
} catch {
let message: String = error.localizedDescription
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
/**
Always pass predicate, not the actual objects
Exception - Can only delete an object from the Realm it belongs to.
Because of creating another Realm instance on the writing queue
*/
func delete<T: Object>(predicates: [NSPredicate],
objectType: T.Type,
completion: @escaping VoidResultClosure) {
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.configuration)
self.readAllResult(object: T.self, completion: { result in
do {
switch result {
case .success(let objects):
var objToDelete = objects
for predicate in predicates {
objToDelete = objects.filter(predicate)
}
try realm.write {
realm.delete(objToDelete)
}
case .failure(let error):
completion(.failure(error))
}
} catch {
ErrorLoggerService.logWithTrace(error.localizedDescription)
}
})
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
}
}
}
func update<T: Object>(value: [T], completion: @escaping VoidClosure) {
for obj in value {
RealmDAO.shared.writeAsync(obj: obj) { realm, objRef in
guard let safeObj = objRef else { return }
do {
try realm.write {
realm.add(safeObj, update: .modified)
completion()
}
} catch {
let message: String = error.localizedDescription
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
}
func deleteAllRecords(completion: @escaping VoidClosure) {
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in
guard let `self` = self else { return }
do {
let realm = try Realm(configuration: self.configuration)
try realm.write {
realm.deleteAll()
completion()
}
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
completion()
}
}
}
func isObjectExists<T: Object>(type: T.Type, primaryKey: String) -> Bool {
do {
let realm = try Realm(configuration: self.configuration)
return realm.object(ofType: type, forPrimaryKey: primaryKey) != nil
} catch {
let message: String = "Unable to create realm instance \(error.localizedDescription)"
ErrorLoggerService.logWithTrace(message)
preconditionFailure(message)
}
}
// MARK: - Private functions
// NOTE: Implementation from original documentation
// https://realm.io/docs/swift/latest/#passing-instances-across-threads
/**
Thread-safe writing to the Realm
*/
func writeAsync<T: ThreadConfined>(obj: T, errorHandler: @escaping ((_ error: Swift.Error) -> Void) = { _ in return }, block: @escaping ((Realm, T?) -> Void)) {
let wrappedObj = ThreadSafeReference(to: obj)
RealmDAO.databaseOperationsQueue.async(flags: .barrier) {
autoreleasepool {
do {
let realm = try Realm(configuration: RealmDAO.shared.realm.configuration)
guard let obj = realm.resolve(wrappedObj) else {
return // obj was deleted
}
try realm.safeWrite {
block(realm, obj)
}
} catch {
errorHandler(error)
}
}
}
}
}
// MARK: - Database migration functionality
extension RealmDAO {
func configureMigration() {
migrationExecutor.performMigrationIfNeeded()
}
private func getCurrentDatabaseVersion() -> UInt64 {
let configCheck = RealmDAO.shared.realm.configuration
guard let fileURL = configCheck.fileURL else {
let message: String = "Unable to get file url"
ErrorLoggerService.logWithTrace(message)
return UInt64(0)
}
do {
let lastSchemaVersion = try schemaVersionAtURL(fileURL)
return lastSchemaVersion
} catch {
// Realm file doesn't exist - realm initial setup
return UInt64(0)
}
}
private func deleteDatabase() {
let configuration = Realm.Configuration(deleteRealmIfMigrationNeeded: true)
Realm.Configuration.defaultConfiguration = configuration
}
}
import Foundation
import RealmSwift
final class RealmDatabaseMigrationExecutor: NSObject {
private let firstReleaseSchemaVersion: UInt64 = 1000
private let secondReleaseSchemaVersion: UInt64 = 1001
private var currentSchemaVersion: UInt64 {
guard let fileURL = Realm.Configuration.defaultConfiguration.fileURL else {
return UInt64(0)
}
do {
let lastSchemaVersion = try schemaVersionAtURL(fileURL)
return lastSchemaVersion
} catch {
// Realm file doesn't exist - realm initial setup
return UInt64(0)
}
}
deinit {
}
override init() {
super.init()
}
func performMigrationIfNeeded() {
let config = Realm.Configuration(schemaVersion: secondReleaseSchemaVersion, migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < self.secondReleaseSchemaVersion && self.currentSchemaVersion != 0 {
self.migrateToSecondSchemaVersion(migration: migration, oldSchema: oldSchemaVersion)
}
})
Realm.Configuration.defaultConfiguration = config
}
private func migrateToSecondSchemaVersion(migration: RealmSwift.Migration, oldSchema: UInt64) {
migration.enumerateObjects(ofType: MyUser.className()) { _, newObject in
newObject?["bio"] = ""
newObject?["coverImageURL"] = ""
newObject?["coverImageThumbURL"] = ""
newObject?["eventsCount"] = 0
newObject?["friendsCount"] = 0
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment