Skip to content

Instantly share code, notes, and snippets.

@M-Miyazako
Last active February 6, 2020 05:42
Show Gist options
  • Save M-Miyazako/363ac3c575f27bddc81ff2da90136413 to your computer and use it in GitHub Desktop.
Save M-Miyazako/363ac3c575f27bddc81ff2da90136413 to your computer and use it in GitHub Desktop.
# Realmことはじめ 要Realm&RxSwift
import Foundation
/// データソースに関するエラー
enum DataSourceError: Error {
/// realmのエラー
case realmError(message: String)
/// キーチェーンのエラー
case keychainError(message: String)
}
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)
}
}
}
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
}
}
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
}
}
}
}
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"
}
}
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