Realm Swift
Realm Swiftは「iOSとmacOSアプリケーション向けのオブジェクトデータベース」として設計された、Swiftネイティブなデータベース解決策です。SQLiteの代替として開発され、オブジェクト指向の直感的なAPIを通じて、複雑なSQLクエリなしに高性能なデータ操作を実現します。リアクティブプログラミング、自動UI更新、スレッドセーフティ、暗号化機能を内蔵し、モバイルアプリケーション開発に特化した最適化により、iOS/macOSの高品質なアプリケーション開発をサポートします。
ライブラリ
Realm Swift
概要
Realm Swiftは「iOSとmacOSアプリケーション向けのオブジェクトデータベース」として設計された、Swiftネイティブなデータベース解決策です。SQLiteの代替として開発され、オブジェクト指向の直感的なAPIを通じて、複雑なSQLクエリなしに高性能なデータ操作を実現します。リアクティブプログラミング、自動UI更新、スレッドセーフティ、暗号化機能を内蔵し、モバイルアプリケーション開発に特化した最適化により、iOS/macOSの高品質なアプリケーション開発をサポートします。
詳細
Realm Swift 2025年版は、Swift 6とSwiftUIとの完全統合により、現代的なiOS/macOSアプリケーション開発に最適化されています。自動スキーママイグレーション、リアルタイムデータ同期、オフライン・ファースト設計により、ユーザーエクスペリエンスを最優先とした堅牢なデータ層を提供します。Core DataやSQLiteと比較して、格段にシンプルなAPIと高い開発効率を実現しつつ、エンタープライズレベルのセキュリティ(AES-256暗号化)と性能を両立します。@Observable、@State、async/awaitとの自然な統合により、SwiftUIでのリアクティブなデータバインディングが簡単に実装できます。
主な特徴
- オブジェクト指向API: Swiftのオブジェクトとしてデータベースアクセス
- リアクティブUI: 自動的なUI更新とデータバインディング
- 高性能: SQLiteよりも高速なデータアクセス
- スレッドセーフ: マルチスレッド環境での安全なデータ操作
- 暗号化: AES-256による包括的データ保護
- SwiftUI統合: ネイティブなSwiftUIサポート
メリット・デメリット
メリット
- SQLクエリ不要で直感的なオブジェクト指向データ操作
- 自動的なUI更新による開発効率の大幅向上
- SQLiteやCore Dataを凌駕する高いパフォーマンス
- スレッドセーフな設計で安全なマルチスレッドプログラミング
- エンタープライズグレードの暗号化とセキュリティ機能
- SwiftUIとの自然な統合とリアクティブプログラミング対応
デメリット
- iOS/macOS/watchOSに限定されたプラットフォーム固有性
- 大規模データセットでのメモリ使用量増加の可能性
- SQLの豊富な機能と比較したクエリ機能の制限
- 他プラットフォームへの移植困難性
- SQLiteほどの普及度ではなく、専門知識が必要
- プロダクション環境でのバックアップとレプリケーション機能の限界
参考ページ
書き方の例
セットアップ
// Package.swift
// dependencies: [
// .package(url: "https://github.com/realm/realm-swift.git", from: "10.45.0")
// ]
// CocoaPods の場合
// pod 'RealmSwift', '~> 10.45.0'
import RealmSwift
import SwiftUI
// Info.plist または設定で暗号化キーを管理
// この例は開発用です。プロダクションでは適切なキーストア管理を行ってください
import Foundation
import Security
class RealmEncryptionKeyManager {
private static let keyIdentifier = "RealmEncryptionKey"
static func getOrCreateEncryptionKey() -> Data {
if let existingKey = getEncryptionKeyFromKeychain() {
return existingKey
}
let newKey = generateRandomKey()
saveEncryptionKeyToKeychain(newKey)
return newKey
}
private static func generateRandomKey() -> Data {
var keyData = Data(count: 64) // 512-bit key for AES-256
let result = keyData.withUnsafeMutableBytes { mutableBytes in
SecRandomCopyBytes(kSecRandomDefault, 64, mutableBytes.bindMemory(to: UInt8.self).baseAddress!)
}
guard result == errSecSuccess else {
fatalError("Failed to generate random key")
}
return keyData
}
private static func saveEncryptionKeyToKeychain(_ key: Data) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keyIdentifier,
kSecValueData as String: key
]
SecItemAdd(query as CFDictionary, nil)
}
private static func getEncryptionKeyFromKeychain() -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keyIdentifier,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess {
return dataTypeRef as? Data
}
return nil
}
}
基本的なモデル定義
import RealmSwift
import Foundation
// 基本ユーザーモデル
class User: Object, ObjectKeyIdentifiable {
@Persisted var id: ObjectId = ObjectId.generate()
@Persisted var name: String = ""
@Persisted var email: String = ""
@Persisted var age: Int = 0
@Persisted var isActive: Bool = true
@Persisted var createdAt: Date = Date()
@Persisted var updatedAt: Date = Date()
@Persisted var tags: List<String> = List<String>()
@Persisted var metadata: Map<String, AnyRealmValue> = Map<String, AnyRealmValue>()
// リレーション(逆方向)
@Persisted(originProperty: "author") var posts: LinkingObjects<Post>
@Persisted var profile: Profile?
// Primary Key
override static func primaryKey() -> String? {
return "id"
}
// インデックス設定
override static func indexedProperties() -> [String] {
return ["email", "isActive", "createdAt"]
}
// 計算プロパティ
var fullDisplayName: String {
return name.isEmpty ? "Anonymous User" : name
}
var isAdult: Bool {
return age >= 18
}
var postCount: Int {
return posts.count
}
// オブジェクトライフサイクル
override func willSave() {
super.willSave()
updatedAt = Date()
}
}
// プロフィール情報(埋め込み型)
class Profile: EmbeddedObject {
@Persisted var bio: String = ""
@Persisted var website: String = ""
@Persisted var location: String = ""
@Persisted var avatarURL: String = ""
@Persisted var socialLinks: Map<String, String> = Map<String, String>()
@Persisted var skills: List<String> = List<String>()
@Persisted var experienceYears: Int = 0
@Persisted var isPublic: Bool = true
var hasCompleteProfile: Bool {
return !bio.isEmpty && !location.isEmpty && skills.count > 0
}
}
// 投稿モデル
class Post: Object, ObjectKeyIdentifiable {
@Persisted var id: ObjectId = ObjectId.generate()
@Persisted var title: String = ""
@Persisted var content: String = ""
@Persisted var excerpt: String = ""
@Persisted var isPublished: Bool = false
@Persisted var publishedAt: Date?
@Persisted var createdAt: Date = Date()
@Persisted var updatedAt: Date = Date()
@Persisted var viewCount: Int = 0
@Persisted var tags: List<String> = List<String>()
@Persisted var featuredImageURL: String = ""
// リレーション
@Persisted var author: User?
@Persisted var category: Category?
@Persisted var comments: List<Comment> = List<Comment>()
override static func primaryKey() -> String? {
return "id"
}
override static func indexedProperties() -> [String] {
return ["isPublished", "publishedAt", "createdAt", "author"]
}
var isPublic: Bool {
return isPublished && publishedAt != nil
}
var readingTimeMinutes: Int {
let wordsPerMinute = 200
let wordCount = content.split(separator: " ").count
return max(1, wordCount / wordsPerMinute)
}
override func willSave() {
super.willSave()
updatedAt = Date()
// 抜粋の自動生成
if excerpt.isEmpty && !content.isEmpty {
excerpt = String(content.prefix(200))
}
// 公開日時の設定
if isPublished && publishedAt == nil {
publishedAt = Date()
}
}
}
// カテゴリモデル
class Category: Object, ObjectKeyIdentifiable {
@Persisted var id: ObjectId = ObjectId.generate()
@Persisted var name: String = ""
@Persisted var description: String = ""
@Persisted var color: String = "#007AFF"
@Persisted var isActive: Bool = true
@Persisted var sortOrder: Int = 0
@Persisted var createdAt: Date = Date()
@Persisted(originProperty: "category") var posts: LinkingObjects<Post>
override static func primaryKey() -> String? {
return "id"
}
override static func indexedProperties() -> [String] {
return ["name", "isActive", "sortOrder"]
}
var postCount: Int {
return posts.filter("isPublished == true").count
}
}
// コメントモデル
class Comment: EmbeddedObject {
@Persisted var id: ObjectId = ObjectId.generate()
@Persisted var content: String = ""
@Persisted var authorName: String = ""
@Persisted var authorEmail: String = ""
@Persisted var isApproved: Bool = false
@Persisted var isSpam: Bool = false
@Persisted var createdAt: Date = Date()
@Persisted var ipAddress: String = ""
@Persisted var userAgent: String = ""
var isValid: Bool {
return !content.isEmpty && !authorName.isEmpty && content.count >= 5
}
}
基本的なCRUD操作
import RealmSwift
class UserRepository: ObservableObject {
private let realm: Realm
init() throws {
// 暗号化設定
let encryptionKey = RealmEncryptionKeyManager.getOrCreateEncryptionKey()
let config = Realm.Configuration(
encryptionKey: encryptionKey,
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
// マイグレーション処理
if oldSchemaVersion < 1 {
// 必要に応じてマイグレーション処理を実装
}
}
)
self.realm = try Realm(configuration: config)
}
// ユーザー作成
func createUser(name: String, email: String, age: Int) async throws -> User {
let user = User()
user.name = name
user.email = email
user.age = age
user.createdAt = Date()
return try await MainActor.run {
try realm.write {
realm.add(user)
return user
}
}
}
// ユーザー取得
func getUser(by id: ObjectId) -> User? {
return realm.object(ofType: User.self, forPrimaryKey: id)
}
// 全ユーザー取得
func getAllUsers() -> Results<User> {
return realm.objects(User.self).sorted(byKeyPath: "createdAt", ascending: false)
}
// アクティブユーザー取得
func getActiveUsers() -> Results<User> {
return realm.objects(User.self).filter("isActive == true").sorted(byKeyPath: "name")
}
// 名前検索
func searchUsers(by namePattern: String) -> Results<User> {
return realm.objects(User.self)
.filter("name CONTAINS[c] %@ OR email CONTAINS[c] %@", namePattern, namePattern)
.sorted(byKeyPath: "name")
}
// 年齢範囲フィルター
func getUsersByAgeRange(min: Int, max: Int) -> Results<User> {
return realm.objects(User.self)
.filter("age >= %@ AND age <= %@", min, max)
.sorted(byKeyPath: "age")
}
// ユーザー更新
func updateUser(_ user: User, name: String, email: String, age: Int) async throws {
try await MainActor.run {
try realm.write {
user.name = name
user.email = email
user.age = age
user.updatedAt = Date()
}
}
}
// プロフィール更新
func updateUserProfile(_ user: User, profile: Profile) async throws {
try await MainActor.run {
try realm.write {
user.profile = profile
user.updatedAt = Date()
}
}
}
// ユーザー削除
func deleteUser(_ user: User) async throws {
try await MainActor.run {
try realm.write {
// 関連する投稿も削除(カスケード削除)
realm.delete(user.posts)
realm.delete(user)
}
}
}
// タグ追加
func addTagToUser(_ user: User, tag: String) async throws {
try await MainActor.run {
try realm.write {
if !user.tags.contains(tag) {
user.tags.append(tag)
}
}
}
}
// メタデータ設定
func setUserMetadata(_ user: User, key: String, value: AnyRealmValue) async throws {
try await MainActor.run {
try realm.write {
user.metadata[key] = value
}
}
}
// ユーザー統計
func getUserStatistics() -> (total: Int, active: Int, averageAge: Double) {
let allUsers = realm.objects(User.self)
let activeUsers = allUsers.filter("isActive == true")
let averageAge = allUsers.average(ofProperty: "age") ?? 0.0
return (
total: allUsers.count,
active: activeUsers.count,
averageAge: averageAge
)
}
}
// 使用例
class ContentView: View {
@StateObject private var userRepository = try! UserRepository()
@State private var users: Results<User>?
@State private var newUserName = ""
@State private var newUserEmail = ""
@State private var newUserAge = 25
@State private var searchText = ""
var body: some View {
NavigationView {
VStack {
// ユーザー作成フォーム
VStack(spacing: 10) {
TextField("Name", text: $newUserName)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Email", text: $newUserEmail)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
HStack {
Text("Age: \(newUserAge)")
Slider(value: Binding(
get: { Double(newUserAge) },
set: { newUserAge = Int($0) }
), in: 0...100, step: 1)
}
Button("Create User") {
Task {
try await createUser()
}
}
.disabled(newUserName.isEmpty || newUserEmail.isEmpty)
}
.padding()
// 検索バー
TextField("Search users...", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
// ユーザーリスト
if let users = users {
List(users, id: \.id) { user in
UserRowView(user: user, repository: userRepository)
}
}
Spacer()
}
.navigationTitle("Realm Swift Demo")
.onAppear {
loadUsers()
}
.onChange(of: searchText) { _, newValue in
searchUsers()
}
}
}
private func createUser() async throws {
let _ = try await userRepository.createUser(
name: newUserName,
email: newUserEmail,
age: newUserAge
)
// フォームリセット
newUserName = ""
newUserEmail = ""
newUserAge = 25
// リスト更新
loadUsers()
}
private func loadUsers() {
users = userRepository.getAllUsers()
}
private func searchUsers() {
if searchText.isEmpty {
users = userRepository.getAllUsers()
} else {
users = userRepository.searchUsers(by: searchText)
}
}
}
複雑なクエリとリレーション
class PostRepository: ObservableObject {
private let realm: Realm
init() throws {
let encryptionKey = RealmEncryptionKeyManager.getOrCreateEncryptionKey()
let config = Realm.Configuration(encryptionKey: encryptionKey)
self.realm = try Realm(configuration: config)
}
// 投稿作成
func createPost(title: String, content: String, author: User, category: Category?) async throws -> Post {
let post = Post()
post.title = title
post.content = content
post.author = author
post.category = category
return try await MainActor.run {
try realm.write {
realm.add(post)
return post
}
}
}
// 公開投稿取得
func getPublishedPosts() -> Results<Post> {
return realm.objects(Post.self)
.filter("isPublished == true")
.sorted(byKeyPath: "publishedAt", ascending: false)
}
// ユーザー別投稿
func getPostsByUser(_ user: User) -> Results<Post> {
return realm.objects(Post.self)
.filter("author == %@", user)
.sorted(byKeyPath: "createdAt", ascending: false)
}
// カテゴリ別投稿
func getPostsByCategory(_ category: Category) -> Results<Post> {
return realm.objects(Post.self)
.filter("category == %@ AND isPublished == true", category)
.sorted(byKeyPath: "publishedAt", ascending: false)
}
// 複雑な検索クエリ
func searchPosts(
titlePattern: String? = nil,
author: User? = nil,
category: Category? = nil,
publishedOnly: Bool = false,
tags: [String] = [],
dateRange: ClosedRange<Date>? = nil
) -> Results<Post> {
var predicates: [NSPredicate] = []
// タイトル検索
if let titlePattern = titlePattern, !titlePattern.isEmpty {
predicates.append(NSPredicate(format: "title CONTAINS[c] %@ OR content CONTAINS[c] %@", titlePattern, titlePattern))
}
// 著者フィルター
if let author = author {
predicates.append(NSPredicate(format: "author == %@", author))
}
// カテゴリフィルター
if let category = category {
predicates.append(NSPredicate(format: "category == %@", category))
}
// 公開状態フィルター
if publishedOnly {
predicates.append(NSPredicate(format: "isPublished == true"))
}
// タグフィルター
if !tags.isEmpty {
let tagPredicates = tags.map { NSPredicate(format: "ANY tags == %@", $0) }
predicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: tagPredicates))
}
// 日付範囲フィルター
if let dateRange = dateRange {
predicates.append(NSPredicate(format: "createdAt >= %@ AND createdAt <= %@", dateRange.lowerBound as NSDate, dateRange.upperBound as NSDate))
}
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
return realm.objects(Post.self)
.filter(compoundPredicate)
.sorted(byKeyPath: "createdAt", ascending: false)
}
// 人気投稿(閲覧数順)
func getPopularPosts(limit: Int = 10) -> Results<Post> {
return realm.objects(Post.self)
.filter("isPublished == true")
.sorted(byKeyPath: "viewCount", ascending: false)
.prefix(limit)
}
// 最近の投稿統計
func getRecentPostStats(days: Int) -> (total: Int, published: Int, drafts: Int) {
let cutoffDate = Calendar.current.date(byAdding: .day, value: -days, to: Date()) ?? Date()
let recentPosts = realm.objects(Post.self).filter("createdAt >= %@", cutoffDate)
let publishedPosts = recentPosts.filter("isPublished == true")
let draftPosts = recentPosts.filter("isPublished == false")
return (
total: recentPosts.count,
published: publishedPosts.count,
drafts: draftPosts.count
)
}
// 投稿の閲覧数増加
func incrementViewCount(_ post: Post) async throws {
try await MainActor.run {
try realm.write {
post.viewCount += 1
}
}
}
// 投稿公開
func publishPost(_ post: Post) async throws {
try await MainActor.run {
try realm.write {
post.isPublished = true
post.publishedAt = Date()
}
}
}
// コメント追加
func addComment(to post: Post, content: String, authorName: String, authorEmail: String) async throws {
let comment = Comment()
comment.content = content
comment.authorName = authorName
comment.authorEmail = authorEmail
comment.createdAt = Date()
try await MainActor.run {
try realm.write {
post.comments.append(comment)
}
}
}
// ユーザー投稿統計
func getUserPostStatistics() -> [(user: User, postCount: Int, publishedCount: Int)] {
let users = realm.objects(User.self)
return users.compactMap { user in
let totalPosts = user.posts.count
let publishedPosts = user.posts.filter("isPublished == true").count
guard totalPosts > 0 else { return nil }
return (user: user, postCount: totalPosts, publishedCount: publishedPosts)
}.sorted { $0.postCount > $1.postCount }
}
}
// SwiftUIでのリアクティブ更新例
struct PostListView: View {
@ObservedResults(Post.self,
filter: NSPredicate(format: "isPublished == true"),
sortDescriptor: SortDescriptor(keyPath: "publishedAt", ascending: false)
) var posts
@StateObject private var postRepository = try! PostRepository()
@State private var selectedCategory: Category?
@State private var searchText = ""
var filteredPosts: Results<Post> {
if searchText.isEmpty && selectedCategory == nil {
return posts
}
return postRepository.searchPosts(
titlePattern: searchText.isEmpty ? nil : searchText,
category: selectedCategory,
publishedOnly: true
)
}
var body: some View {
NavigationView {
VStack {
// 検索とフィルター
VStack {
TextField("Search posts...", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
// カテゴリ選択 (簡略化)
Text("Category filter would go here")
}
.padding()
// 投稿リスト
List {
ForEach(filteredPosts, id: \.id) { post in
PostRowView(post: post)
.onTapGesture {
Task {
try await postRepository.incrementViewCount(post)
}
}
}
}
}
.navigationTitle("Posts")
}
}
}
struct PostRowView: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(post.title)
.font(.headline)
.lineLimit(2)
Text(post.excerpt)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(3)
HStack {
if let author = post.author {
Text("by \(author.name)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text("\(post.viewCount) views")
.font(.caption)
.foregroundColor(.secondary)
if let publishedAt = post.publishedAt {
Text(publishedAt, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
}
// タグ表示
if !post.tags.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Array(post.tags), id: \.self) { tag in
Text(tag)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(8)
}
}
.padding(.horizontal)
}
}
}
.padding(.vertical, 4)
}
}
非同期処理とリアクティブプログラミング
import RealmSwift
import Combine
// リアクティブなデータサービス
class RealtimeDataService: ObservableObject {
private let realm: Realm
private var notificationTokens: [NotificationToken] = []
@Published var users: Results<User>?
@Published var posts: Results<Post>?
@Published var userStats: (total: Int, active: Int, average: Double) = (0, 0, 0.0)
@Published var postStats: (total: Int, published: Int, drafts: Int) = (0, 0, 0)
init() throws {
let encryptionKey = RealmEncryptionKeyManager.getOrCreateEncryptionKey()
let config = Realm.Configuration(encryptionKey: encryptionKey)
self.realm = try Realm(configuration: config)
setupRealtimeObservers()
}
deinit {
notificationTokens.forEach { $0.invalidate() }
}
private func setupRealtimeObservers() {
// ユーザーのリアルタイム更新
let userResults = realm.objects(User.self).sorted(byKeyPath: "createdAt", ascending: false)
let userToken = userResults.observe { [weak self] changes in
switch changes {
case .initial(let results):
self?.users = results
self?.updateUserStats()
case .update(let results, _, _, _):
self?.users = results
self?.updateUserStats()
case .error(let error):
print("User observation error: \(error)")
}
}
notificationTokens.append(userToken)
// 投稿のリアルタイム更新
let postResults = realm.objects(Post.self).sorted(byKeyPath: "createdAt", ascending: false)
let postToken = postResults.observe { [weak self] changes in
switch changes {
case .initial(let results):
self?.posts = results
self?.updatePostStats()
case .update(let results, _, _, _):
self?.posts = results
self?.updatePostStats()
case .error(let error):
print("Post observation error: \(error)")
}
}
notificationTokens.append(postToken)
}
private func updateUserStats() {
guard let users = users else { return }
let activeUsers = users.filter("isActive == true")
let averageAge = users.average(ofProperty: "age") ?? 0.0
DispatchQueue.main.async {
self.userStats = (
total: users.count,
active: activeUsers.count,
average: averageAge
)
}
}
private func updatePostStats() {
guard let posts = posts else { return }
let publishedPosts = posts.filter("isPublished == true")
let draftPosts = posts.filter("isPublished == false")
DispatchQueue.main.async {
self.postStats = (
total: posts.count,
published: publishedPosts.count,
drafts: draftPosts.count
)
}
}
// 非同期データ操作
func performBatchUserUpdate(updates: [(User, String, String, Int)]) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
for (user, name, email, age) in updates {
group.addTask {
try await self.updateUserAsync(user, name: name, email: email, age: age)
}
}
try await group.waitForAll()
}
}
private func updateUserAsync(_ user: User, name: String, email: String, age: Int) async throws {
try await MainActor.run {
try realm.write {
user.name = name
user.email = email
user.age = age
user.updatedAt = Date()
}
}
}
// データクリーンアップ
func cleanupOldData(olderThanDays days: Int) async throws {
let cutoffDate = Calendar.current.date(byAdding: .day, value: -days, to: Date()) ?? Date()
try await MainActor.run {
try realm.write {
// 古い非公開投稿を削除
let oldDrafts = realm.objects(Post.self)
.filter("isPublished == false AND createdAt < %@", cutoffDate)
realm.delete(oldDrafts)
// 非アクティブユーザーの削除
let inactiveUsers = realm.objects(User.self)
.filter("isActive == false AND updatedAt < %@", cutoffDate)
realm.delete(inactiveUsers)
}
}
}
// エクスポート機能
func exportUserData() async throws -> [String: Any] {
let users = realm.objects(User.self)
let posts = realm.objects(Post.self)
let userData = users.map { user in
[
"id": user.id.stringValue,
"name": user.name,
"email": user.email,
"age": user.age,
"isActive": user.isActive,
"postCount": user.posts.count
]
}
let postData = posts.map { post in
[
"id": post.id.stringValue,
"title": post.title,
"isPublished": post.isPublished,
"viewCount": post.viewCount,
"authorName": post.author?.name ?? ""
]
}
return [
"users": userData,
"posts": postData,
"exportDate": Date().ISO8601String(),
"totalUsers": users.count,
"totalPosts": posts.count
]
}
}
// リアルタイムダッシュボードView
struct RealtimeDashboardView: View {
@StateObject private var dataService = try! RealtimeDataService()
@State private var isExporting = false
@State private var exportData: [String: Any]?
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// ユーザー統計
VStack(alignment: .leading, spacing: 10) {
Text("User Statistics")
.font(.headline)
HStack {
StatCard(title: "Total Users", value: "\(dataService.userStats.total)")
StatCard(title: "Active Users", value: "\(dataService.userStats.active)")
StatCard(title: "Avg Age", value: String(format: "%.1f", dataService.userStats.average))
}
}
// 投稿統計
VStack(alignment: .leading, spacing: 10) {
Text("Post Statistics")
.font(.headline)
HStack {
StatCard(title: "Total Posts", value: "\(dataService.postStats.total)")
StatCard(title: "Published", value: "\(dataService.postStats.published)")
StatCard(title: "Drafts", value: "\(dataService.postStats.drafts)")
}
}
// エクスポートボタン
Button("Export Data") {
Task {
isExporting = true
do {
exportData = try await dataService.exportUserData()
print("Export completed: \(exportData?.keys.joined(separator: ", ") ?? "Unknown")")
} catch {
print("Export failed: \(error)")
}
isExporting = false
}
}
.disabled(isExporting)
// クリーンアップボタン
Button("Cleanup Old Data") {
Task {
try await dataService.cleanupOldData(olderThanDays: 30)
}
}
.foregroundColor(.red)
Spacer()
}
.padding()
}
.navigationTitle("Dashboard")
.refreshable {
// プルトゥリフレッシュは自動で更新されるため特に処理不要
}
}
}
}
struct StatCard: View {
let title: String
let value: String
var body: some View {
VStack {
Text(value)
.font(.title2)
.fontWeight(.bold)
Text(title)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
}
エラーハンドリングとセキュリティ
import RealmSwift
// カスタムエラー定義
enum RealmError: Error, LocalizedError {
case initializationFailed(String)
case encryptionKeyError
case writeTransactionFailed(String)
case objectNotFound(String)
case validationFailed(String)
case permissionDenied
case databaseCorrupted
var errorDescription: String? {
switch self {
case .initializationFailed(let message):
return "Database initialization failed: \(message)"
case .encryptionKeyError:
return "Encryption key generation or retrieval failed"
case .writeTransactionFailed(let message):
return "Write transaction failed: \(message)"
case .objectNotFound(let message):
return "Object not found: \(message)"
case .validationFailed(let message):
return "Validation failed: \(message)"
case .permissionDenied:
return "Permission denied for database operation"
case .databaseCorrupted:
return "Database file is corrupted"
}
}
}
// 安全なRealm操作クラス
class SecureRealmManager: ObservableObject {
private let realm: Realm
private let encryptionKey: Data
init() throws {
do {
self.encryptionKey = RealmEncryptionKeyManager.getOrCreateEncryptionKey()
let config = Realm.Configuration(
encryptionKey: encryptionKey,
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
// 安全なマイグレーション処理
Self.performSafeMigration(migration: migration, from: oldSchemaVersion)
},
shouldCompactOnLaunch: { totalBytes, usedBytes in
// 10MB以上かつ使用率50%以下の場合にコンパクト実行
let tenMB = 10 * 1024 * 1024
return (totalBytes > tenMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5
}
)
self.realm = try Realm(configuration: config)
} catch {
throw RealmError.initializationFailed(error.localizedDescription)
}
}
private static func performSafeMigration(migration: Migration, from oldSchemaVersion: UInt64) {
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: User.className()) { oldObject, newObject in
newObject?["updatedAt"] = Date()
}
}
if oldSchemaVersion < 2 {
migration.enumerateObjects(ofType: Post.className()) { oldObject, newObject in
newObject?["excerpt"] = ""
newObject?["viewCount"] = 0
}
}
}
// 安全なオブジェクト作成
func createObjectSafely<T: Object>(_ type: T.Type, configuration: (T) throws -> Void) async throws -> T {
let object = type.init()
do {
try configuration(object)
try validateObject(object)
return try await MainActor.run {
try realm.write {
realm.add(object)
return object
}
}
} catch {
throw RealmError.writeTransactionFailed(error.localizedDescription)
}
}
// オブジェクト検証
private func validateObject<T: Object>(_ object: T) throws {
if let user = object as? User {
try validateUser(user)
} else if let post = object as? Post {
try validatePost(post)
}
}
private func validateUser(_ user: User) throws {
guard !user.name.isEmpty else {
throw RealmError.validationFailed("User name cannot be empty")
}
guard user.email.contains("@") && user.email.contains(".") else {
throw RealmError.validationFailed("Invalid email format")
}
guard user.age >= 0 && user.age <= 150 else {
throw RealmError.validationFailed("Age must be between 0 and 150")
}
// メール重複チェック
let existingUser = realm.objects(User.self).filter("email == %@", user.email).first
if let existing = existingUser, existing.id != user.id {
throw RealmError.validationFailed("Email already exists")
}
}
private func validatePost(_ post: Post) throws {
guard !post.title.isEmpty else {
throw RealmError.validationFailed("Post title cannot be empty")
}
guard post.content.count >= 10 else {
throw RealmError.validationFailed("Post content must be at least 10 characters")
}
guard post.author != nil else {
throw RealmError.validationFailed("Post must have an author")
}
}
// 安全な更新操作
func updateObjectSafely<T: Object>(_ object: T, updates: @escaping (T) throws -> Void) async throws {
do {
try await MainActor.run {
try realm.write {
try updates(object)
try validateObject(object)
}
}
} catch {
throw RealmError.writeTransactionFailed(error.localizedDescription)
}
}
// 安全な削除操作
func deleteObjectSafely<T: Object>(_ object: T) async throws {
try await MainActor.run {
do {
try realm.write {
realm.delete(object)
}
} catch {
throw RealmError.writeTransactionFailed(error.localizedDescription)
}
}
}
// バックアップ作成
func createBackup() async throws -> URL {
let backupURL = getBackupURL()
try await MainActor.run {
try realm.writeCopy(toFile: backupURL, encryptionKey: encryptionKey)
}
return backupURL
}
// バックアップから復元
func restoreFromBackup(backupURL: URL) async throws {
guard FileManager.default.fileExists(atPath: backupURL.path) else {
throw RealmError.objectNotFound("Backup file not found")
}
// 新しいRealm設定でバックアップから復元
let restoreConfig = Realm.Configuration(
fileURL: backupURL,
encryptionKey: encryptionKey,
readOnly: true
)
let backupRealm = try Realm(configuration: restoreConfig)
try await MainActor.run {
try realm.write {
// データクリア
realm.deleteAll()
// バックアップからデータ復元
let users = backupRealm.objects(User.self)
let posts = backupRealm.objects(Post.self)
let categories = backupRealm.objects(Category.self)
realm.add(users, update: .modified)
realm.add(categories, update: .modified)
realm.add(posts, update: .modified)
}
}
}
// データベース整合性チェック
func performIntegrityCheck() -> [String] {
var issues: [String] = []
// 孤立投稿チェック
let postsWithoutAuthors = realm.objects(Post.self).filter("author == nil")
if !postsWithoutAuthors.isEmpty {
issues.append("\(postsWithoutAuthors.count) posts without authors found")
}
// 重複メールチェック
let users = realm.objects(User.self)
let emails = users.map { $0.email }
let uniqueEmails = Set(emails)
if emails.count != uniqueEmails.count {
issues.append("Duplicate email addresses found")
}
// 無効データチェック
let invalidUsers = users.filter("age < 0 OR age > 150")
if !invalidUsers.isEmpty {
issues.append("\(invalidUsers.count) users with invalid age found")
}
return issues
}
// セキュアな削除(データの完全削除)
func secureDelete() async throws {
try await MainActor.run {
try realm.write {
realm.deleteAll()
}
}
// ファイルの物理削除
let realmURL = realm.configuration.fileURL!
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("note"),
realmURL.appendingPathExtension("management")
]
for url in realmURLs {
try? FileManager.default.removeItem(at: url)
}
}
private func getBackupURL() -> URL {
let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let timestamp = ISO8601DateFormatter().string(from: Date())
return documentsPath.appendingPathComponent("realm_backup_\(timestamp).realm")
}
}
// エラーハンドリング使用例
struct SecureDataView: View {
@StateObject private var realmManager = try! SecureRealmManager()
@State private var errorMessage: String?
@State private var showingError = false
@State private var isBackingUp = false
@State private var integrityIssues: [String] = []
var body: some View {
VStack {
Button("Create Safe User") {
Task {
await createUserSafely()
}
}
Button("Run Integrity Check") {
integrityIssues = realmManager.performIntegrityCheck()
}
Button("Create Backup") {
Task {
await createBackup()
}
}
.disabled(isBackingUp)
if !integrityIssues.isEmpty {
Text("Integrity Issues:")
.font(.headline)
.foregroundColor(.red)
ForEach(integrityIssues, id: \.self) { issue in
Text("• \(issue)")
.foregroundColor(.red)
}
}
}
.alert("Error", isPresented: $showingError) {
Button("OK") { }
} message: {
Text(errorMessage ?? "Unknown error")
}
}
private func createUserSafely() async {
do {
let user = try await realmManager.createObjectSafely(User.self) { user in
user.name = "John Doe"
user.email = "[email protected]"
user.age = 30
}
print("User created safely: \(user.name)")
} catch {
errorMessage = error.localizedDescription
showingError = true
}
}
private func createBackup() async {
isBackingUp = true
do {
let backupURL = try await realmManager.createBackup()
print("Backup created at: \(backupURL)")
} catch {
errorMessage = error.localizedDescription
showingError = true
}
isBackingUp = false
}
}