Last active
February 6, 2020 05:42
-
-
Save M-Miyazako/363ac3c575f27bddc81ff2da90136413 to your computer and use it in GitHub Desktop.
# Realmことはじめ 要Realm&RxSwift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// データソースに関するエラー | |
enum DataSourceError: Error { | |
/// realmのエラー | |
case realmError(message: String) | |
/// キーチェーンのエラー | |
case keychainError(message: String) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// キーチェーンのリポジトリ | |
struct KeychainRepository { | |
/// 暗号化キーのサイズ(バイト) | |
static let keySizeInBytes = 64 | |
/// 暗号化キーのサイズ(ビット) | |
static var keySizeInBits: Int { | |
return keySizeInBytes * 8 | |
} | |
/// キーチェーンのIDを生成する | |
/// | |
/// - とりあえず Bundle ID + ".keys." + name とする | |
/// | |
/// - Parameter name: キーの名前 | |
/// - Returns: 生成したID | |
private static func generateIdentifier(with name: String) -> Data? { | |
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "keychain" | |
let identifier = "\(bundleIdentifier).keys.\(name)" | |
return identifier.data(using: .utf8, allowLossyConversion: false) | |
} | |
/// キーチェーンからキーを取得する | |
/// | |
/// - Parameter name: キーの名前 | |
/// - Returns: 取得したキー(取得できない場合はnil) | |
static func getKey(for name: String) -> Data? { | |
let query = [ | |
kSecClass: kSecClassKey, | |
kSecAttrApplicationTag: generateIdentifier(with: name) as AnyObject, | |
kSecAttrKeySizeInBits: keySizeInBits as AnyObject, | |
kSecReturnData: true as AnyObject | |
] as CFDictionary | |
var dataTypeRef: AnyObject? | |
let status = withUnsafeMutablePointer(to: &dataTypeRef) { SecItemCopyMatching(query, UnsafeMutablePointer($0)) } | |
if status == errSecSuccess { | |
return dataTypeRef as? Data | |
} | |
return nil | |
} | |
/// キーチェーンにキーを保存する | |
/// | |
/// - Parameters: | |
/// - name: キーの名前 | |
/// - data: キーのデータ | |
static func saveKey(for name: String, data: Data) throws { | |
let query = [ | |
kSecClass: kSecClassKey, | |
kSecAttrApplicationTag: generateIdentifier(with: name) as AnyObject, | |
kSecAttrKeySizeInBits: keySizeInBits as AnyObject, | |
kSecValueData: data as AnyObject, | |
kSecAttrSynchronizable: false as AnyObject | |
] as CFDictionary | |
let result = SecItemAdd(query, nil) | |
if result != errSecSuccess { | |
print("SecItemAdd() returns \(result)") | |
throw DataSourceError.keychainError(message: .keychainSaveError) | |
} | |
} | |
/// キーチェーンからキーを削除する | |
/// | |
/// - Parameter name: キーの名前 | |
static func deleteKey(for name: String) throws { | |
let query = [ | |
kSecClass: kSecClassKey, | |
kSecAttrApplicationTag: generateIdentifier(with: name) as AnyObject | |
] as CFDictionary | |
let result = SecItemDelete(query) | |
if result != errSecSuccess && result != errSecItemNotFound { | |
throw DataSourceError.keychainError(message: .keychainDeleteError) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
/// Realmの設定を構築する | |
struct RealmConfigurationBuilder { | |
/// 暗号化キーの名前 | |
static let encryptionKeyName = "realmEncryptionKey" | |
/// Realmの設定を構築する | |
/// | |
/// - Returns: Realmの設定 | |
static func build() throws -> Realm.Configuration { | |
let isProduction = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil | |
guard let buildMethod = buildMethodIfProduction[isProduction] else { | |
fatalError("Can't get product name") | |
} | |
return try buildMethod() | |
} | |
/// 環境別のbuildメソッド | |
private static let buildMethodIfProduction = [ | |
true: buildForProduction, | |
false: buildForTest | |
] | |
/// プロダクション用のRealmの設定を構築する | |
/// | |
/// - Note: とりあえず開発時もプロダクション用の設定 | |
/// - Returns: Realmの設定 | |
private static func buildForProduction() throws -> Realm.Configuration { | |
let key = try getOrGenerateKey() | |
#if DEBUG | |
// 開発時にRealmの中身を覗きたくなったときのために暗号化キーをコンソールに出力している | |
print("Realm encryption key: \(key.map { String(format: "%.2hhx", $0) }.joined())") | |
#endif | |
var config = Realm.Configuration() | |
config.fileURL = config.fileURL?.deletingLastPathComponent().appendingPathComponent("realm") | |
config.encryptionKey = key | |
config.schemaVersion = 2 | |
config.migrationBlock = { migration, oldVersion in | |
migrateToVersion2(from: oldVersion, with: migration) | |
} | |
config.shouldCompactOnLaunch = { totalBytes, usedBytes in | |
let oneHundredMB = 100 * 1_024 * 1_024 | |
return (totalBytes > oneHundredMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5 | |
} | |
return config | |
} | |
private static func migrateToVersion2(from oldVersion: UInt64, with migration: Migration) { | |
guard oldVersion < 2 else { | |
return | |
} | |
migration.renameProperty(onType: RealmCageUnitAnimal.className(), | |
from: "hoge", | |
to: "piyo") | |
} | |
/// テスト用のRealmの設定を構築する | |
/// | |
/// - Returns: Realmの設定 | |
private static func buildForTest() -> Realm.Configuration { | |
var config = Realm.Configuration() | |
config.fileURL = config.fileURL?.deletingLastPathComponent().appendingPathComponent("test.realm") | |
config.deleteRealmIfMigrationNeeded = true | |
return config | |
} | |
/// 暗号化キーをキーチェーンから取得し、なければ新しく生成する | |
/// | |
/// - Returns: 暗号化キー | |
private static func getOrGenerateKey() throws -> Data { | |
if let key = KeychainRepository.getKey(for: encryptionKeyName) { | |
return key | |
} | |
return try generateKey() | |
} | |
/// 暗号化キーを生成する | |
/// | |
/// - Returns: 暗号化キー | |
private static func generateKey() throws -> Data { | |
let size = KeychainRepository.keySizeInBytes | |
guard let key = NSMutableData(length: size) else { | |
throw DataSourceError.realmError(message: .realmConfigurationError) | |
} | |
let result = SecRandomCopyBytes( | |
kSecRandomDefault, | |
size, | |
key.mutableBytes.bindMemory(to: UTF8.self, capacity: size)) | |
if result != 0 { | |
print("SecRandomCopyBytes() returns \(result)") | |
throw DataSourceError.realmError(message: .realmConfigurationError) | |
} | |
let immutableKey = key as Data | |
try KeychainRepository.saveKey(for: encryptionKeyName, data: immutableKey) | |
return immutableKey | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
import RxSwift | |
/// Realmのデータベース | |
final class RealmDB { | |
/// Realmのインスタンス | |
internal private(set) var realm: Realm? | |
/// RealmDBのシングルトンインスタンス | |
static let shared = RealmDB() | |
/// マスタに更新があったことを流すストリーム | |
var masterUpdated: Observable<Void> { | |
return masterUpdatedSubject.asObservable().share(replay: 1) | |
} | |
/// マスタに更新があったことを流すためのSubject | |
private let masterUpdatedSubject = BehaviorSubject<Void>(value: ()) | |
/// Realmの通知トークン | |
private var notificationToken: NotificationToken? | |
/// initは隠蔽する | |
private init() { } | |
/// インスタンスが破棄されるときにRealmの通知を解除する | |
deinit { | |
notificationToken?.invalidate() | |
} | |
/// Realmの初期設定 | |
/// | |
/// - Parameter configuration: Realmの設定 | |
func initialize(configuration: Realm.Configuration) throws { | |
do { | |
realm = try Realm(configuration: configuration) | |
} catch let error { | |
print(error.localizedDescription) | |
try recreateRealmFile(configuration: configuration) | |
} | |
subscribeMasterUpdated() | |
} | |
/// Realmファイルの再作成 | |
/// | |
/// - Parameter configuration: Realmの設定 | |
private func recreateRealmFile(configuration: Realm.Configuration) throws { | |
do { | |
try deleteRealmFiles(configuration: configuration) | |
try KeychainRepository.deleteKey(for: RealmConfigurationBuilder.encryptionKeyName) | |
// 失敗したのは設定(暗号化キー)に問題があったからかもしれないので設定を作り直す | |
realm = try Realm(configuration: RealmConfigurationBuilder.build()) | |
} catch let error { | |
print(error.localizedDescription) | |
throw DataSourceError.realmError(message: .realmOpenError) | |
} | |
} | |
/// Realmファイルを削除する | |
/// | |
/// - Parameter configuration: Realmの設定 | |
private func deleteRealmFiles(configuration: Realm.Configuration) throws { | |
guard let url = configuration.fileURL else { | |
return | |
} | |
let urls = [ | |
url, | |
url.appendingPathExtension("lock"), | |
url.appendingPathExtension("note"), | |
url.appendingPathExtension("management") | |
] | |
try urls.forEach { | |
try FileManager.default.removeItem(at: $0) | |
} | |
} | |
/// マスタの変更を購読し、変更があればマスタの変更があったことをストリームに流す | |
private func subscribeMasterUpdated() { | |
notificationToken = RealmDB.shared.realm?.objects(MasterVersion.self) | |
.observe { [weak self] (changes: RealmCollectionChange) in | |
switch changes { | |
case .update: | |
self?.masterUpdatedSubject.onNext(()) | |
default: | |
break | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
final class RealmHoge: Object, RealmRepository { | |
typealias ModelType = RealmHoge | |
/// ID | |
@objc dynamic var id: String = "" | |
/// 名前 | |
@objc dynamic var name: String = "" | |
/// プライマリキーを返す | |
/// | |
/// - Returns: プライマリキー | |
override static func primaryKey() -> String? { | |
return "id" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
/// Realmを使用するリポジトリのプロトコル | |
protocol RealmRepository { | |
associatedtype ModelType: Object | |
/// idを生成する | |
/// | |
/// - Note: RealmはPKを一つしか持てないため、複数のPKで管理されているデータには一意となるようなIDを生成する必要がある | |
/// - 基本的にUUIDで良いと思われる | |
/// - Returns: id | |
func generateId() -> String | |
/// 全件取得する | |
/// | |
/// - Returns: すべてのデータ(見つからない場合は空の配列) | |
static func all() -> [ModelType] | |
/// 指定したidのデータを取得する | |
/// | |
/// - Parameter id: 取得したいデータのid | |
/// - Returns: 指定したidのデータ(見つからない場合はnil) | |
static func find(by id: String) -> ModelType? | |
/// データを追加する | |
/// | |
/// - Parameter model: 追加するデータ | |
/// - Throws: 追加に失敗した場合は例外が発生する | |
static func add(_ model: ModelType) throws | |
/// 複数のデータを追加する | |
/// | |
/// - Parameter models: 追加するデータ | |
/// - Throws: 追加に失敗した場合は例外が発生する | |
static func add(_ models: [ModelType]) throws | |
/// 更新処理ブロック | |
typealias UpdateBlock = (_ model: ModelType?) -> Void | |
/// データを更新する | |
/// | |
/// - Parameters: | |
/// - id: 更新対象データのid | |
/// - block: 更新処理ブロック | |
/// - Throws: 更新に失敗した場合は例外が発生する | |
static func update(by id: String, block: UpdateBlock) throws | |
/// 指定したidのデータを削除する | |
/// | |
/// - Parameter id: 削除したいデータのid | |
/// - Throws: 削除に失敗した場合は例外が発生する | |
static func delete(by id: String) throws | |
/// 全件削除する | |
/// - Throws: 削除に失敗した場合は例外が発生する | |
static func truncate() throws | |
} | |
extension RealmRepository { | |
/// generateIdのデフォルト実装 | |
/// | |
/// - Returns: id | |
func generateId() -> String { | |
return UUID().uuidString | |
} | |
/// allのデフォルト実装 | |
/// | |
/// - Returns: すべてのデータ(見つからない場合は空の配列) | |
static func all() -> [ModelType] { | |
return RealmDB.shared.realm?.objects(ModelType.self).map { $0 } ?? [] | |
} | |
/// find(by id:)のデフォルト実装 | |
/// | |
/// - Parameter id: 取得したいデータのid | |
/// - Returns: 指定したidのデータ(見つからない場合はnil) | |
static func find(by id: String) -> ModelType? { | |
return RealmDB.shared.realm?.object(ofType: ModelType.self, forPrimaryKey: id) | |
} | |
/// add(model:)のデフォルト実装 | |
/// | |
/// - Parameter model: 追加するデータ | |
/// - Throws: 追加に失敗した場合は例外が発生する | |
static func add(_ model: ModelType) throws { | |
try RealmDB.shared.realm?.write { | |
RealmDB.shared.realm?.add(model, update: .all) | |
} | |
} | |
/// add(models:)のデフォルト実装 | |
/// | |
/// - Parameter models: 追加するデータ | |
/// - Throws: 追加に失敗した場合は例外が発生する | |
static func add(_ models: [ModelType]) throws { | |
try RealmDB.shared.realm?.write { | |
RealmDB.shared.realm?.add(models, update: .all) | |
} | |
} | |
/// update(block:)のデフォルト実装 | |
/// | |
/// - Parameters: | |
/// - id: 更新対象データのid | |
/// - block: 更新処理ブロック | |
/// - Throws: 更新に失敗した場合は例外が発生する | |
static func update(by id: String, block: UpdateBlock) throws { | |
guard let realm = RealmDB.shared.realm else { | |
return | |
} | |
let target = Self.find(by: id) | |
try realm.write { | |
block(target) | |
} | |
} | |
/// delete(by id:)のデフォルト実装 | |
/// | |
/// - Parameter id: 削除したいデータのid | |
/// - Throws: 削除に失敗した場合は例外が発生する | |
static func delete(by id: String) throws { | |
guard let target = Self.find(by: id) else { | |
return | |
} | |
try RealmDB.shared.realm?.write { | |
RealmDB.shared.realm?.delete(target) | |
} | |
} | |
/// truncate()のデフォルト実装 | |
/// | |
/// - Throws: 削除に失敗した場合は例外が発生する | |
static func truncate() throws { | |
let all = Self.all() | |
try RealmDB.shared.realm?.write { | |
RealmDB.shared.realm?.delete(all) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment