markdown
はじめに (対象読者・この記事でわかること)
この記事は、SwiftとSwiftUIを使って動画再生アプリを開発したいが、どのようなローカルデータベースを使えば良いか悩んでいる方を対象としています。特に、動画のメタデータ(視聴履歴、ブックマーク、ダウンロード情報など)を効率的に管理したい方に最適です。
この記事を読むことで、Realm・CoreData・SQLiteの3つの主要なローカルDBの特徴と使い分けが理解でき、実際にSwiftアプリに組み込むための具体的な実装方法が身につきます。また、動画再生アプリ特有の課題(大容量データの扱い、オフライン対応など)への対処法も学べます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Swiftの基本的な文法(クラス、構造体、プロトコル) - SwiftUIの基礎(Viewプロトコル、Stateプロパティ) - iOSアプリ開発の基礎(Xcodeの使い方、シミュレータの実行)
動画再生アプリにおけるDBの重要性と選定基準
動画再生アプリでは、単に動画を再生するだけでなく、ユーザーの視聴履歴やブックマーク、ダウンロード状態などのデータを管理する必要があります。これらのデータを効率的に扱うためには、適切なローカルデータベースの選定が重要です。
選定に当たっては以下の点を考慮する必要があります:
- パフォーマンス: 動画のメタデータは通常大容量になりやすく、高速な読み書きが必須
- オフライン対応: ネットワーク非接続時でも動作する必要がある
- データの整合性: 視聴途中の情報など、失敗が許されないデータの扱い
- 開発効率: Swiftの生態系との親和性、学習コスト
3つの主要DBを徹底比較・実装してみた
それでは、実際に動画再生アプリに組み込んだ際の3つのDBを詳しく見ていきましょう。
Realm:直感的で高速なNoSQLデータベース
Realmは、MongoDBが提供するモバイル向けのNoSQLデータベースです。Swiftとの親和性が非常に高く、直感的なAPIで操作できます。
まず、Podfileに以下を追加してインストールします:
Rubypod 'RealmSwift'
次に、動画メタデータ用のモデルを定義します:
Swiftimport RealmSwift class VideoMetadata: Object { @Persisted(primaryKey: true) var id: String @Persisted var title: String @Persisted var duration: Double @Persisted var lastPlayedPosition: Double @Persisted var isBookmarked: Bool @Persisted var downloadStatus: String @Persisted var updatedAt: Date convenience init(id: String, title: String, duration: Double) { self.init() self.id = id self.title = title self.duration = duration self.lastPlayedPosition = 0 self.isBookmarked = false self.downloadStatus = "none" self.updatedAt = Date() } }
そして、実際にデータを保存・取得する処理は以下のように記述します:
Swiftclass VideoRepository { private let realm = try! Realm() func saveVideoMetadata(_ metadata: VideoMetadata) { try! realm.write { realm.add(metadata, update: .modified) } } func fetchBookmarkedVideos() -> Results<VideoMetadata> { return realm.objects(VideoMetadata.self) .filter("isBookmarked == true") .sorted(byKeyPath: "updatedAt", ascending: false) } }
Realmの最大の特徴は、リアクティブなデータ更新が可能な点です。SwiftUIと組み合わせることで、データの変更を自動的にUIに反映できます:
Swiftstruct BookmarkedVideosView: View { @ObservedResults(VideoMetadata.self, where: \.isBookmarked == true) var bookmarkedVideos var body: some View { List(bookmarkedVideos) { video in VideoRow(video: video) } } }
CoreData:Apple純正のオブジェクトグラフ管理
CoreDataは、Appleが提供するオブジェクト永続化フレームワークです。iOS/macOS間でのデータ同期に強く、大規模なデータ管理に適しています。
まず、.xcdatamodeldファイルを作成し、エンティティを定義します:
Entity: VideoMetadata
Attributes:
- id: String (Indexed)
- title: String
- duration: Double
- lastPlayedPosition: Double
- isBookmarked: Boolean
- downloadStatus: String
- updatedAt: Date
次に、NSManagedObjectのサブクラスを生成し、以下のように操作します:
Swiftclass VideoMetadata: NSManagedObject { @NSManaged var id: String @NSManaged var title: String @NSManaged var duration: Double @NSManaged var lastPlayedPosition: Double @NSManaged var isBookmarked: Bool @NSManaged var downloadStatus: String @NSManaged var updatedAt: Date } class CoreDataManager { private let persistentContainer: NSPersistentContainer init() { persistentContainer = NSPersistentContainer(name: "VideoDataModel") persistentContainer.loadPersistentStores { _, error in if let error = error { fatalError("CoreData load error: \(error)") } } } func saveVideoMetadata(id: String, title: String, duration: Double) { let context = persistentContainer.viewContext let metadata = VideoMetadata(context: context) metadata.id = id metadata.title = title metadata.duration = duration do { try context.save() } catch { print("Failed to save: \(error)") } } func fetchAllVideos() -> [VideoMetadata] { let request: NSFetchRequest<VideoMetadata> = VideoMetadata.fetchRequest() let sortDescriptor = NSSortDescriptor(key: "updatedAt", ascending: false) request.sortDescriptors = [sortDescriptor] do { return try persistentContainer.viewContext.fetch(request) } catch { print("Fetch error: \(error)") return [] } } }
CoreDataの強みは、iCloud同期が標準でサポートされている点です。複数デバイス間で視聴履歴を共有したい場合に最適です。
SQLite:軽量で柔軟なリレーショナルDB
SQLiteは、組み込み型のリレーショナルデータベースです。最も低レベルな制御が可能で、パフォーマンス重視の場合に選択されます。
SQLite.swiftを使用して実装します:
Swiftimport SQLite class SQLiteManager { private var db: Connection! private let videos = Table("videos") private let id = Expression<String>("id") private let title = Expression<String>("title") private let duration = Expression<Double>("duration") private let lastPlayedPosition = Expression<Double>("last_played_position") private let isBookmarked = Expression<Bool>("is_bookmarked") private let downloadStatus = Expression<String>("download_status") private let updatedAt = Expression<Date>("updated_at") init() { do { let path = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true ).first! db = try Connection("\(path)/videos.sqlite3") // テーブル作成 try db.run(videos.create(ifNotExists: true) { t in t.column(id, primaryKey: true) t.column(title) t.column(duration) t.column(lastPlayedPosition, defaultValue: 0) t.column(isBookmarked, defaultValue: false) t.column(downloadStatus, defaultValue: "none") t.column(updatedAt, defaultValue: Date()) }) } catch { print("SQLite initialization error: \(error)") } } func insertVideo(id: String, title: String, duration: Double) { do { let insert = videos.insert( self.id <- id, self.title <- title, self.duration <- duration ) try db.run(insert) } catch { print("Insert error: \(error)") } } func fetchBookmarkedVideos() -> [VideoMetadata] { var results: [VideoMetadata] = [] do { for video in try db.prepare(videos.filter(isBookmarked == true)) { let metadata = VideoMetadata( id: video[id], title: video[title], duration: video[duration], lastPlayedPosition: video[lastPlayedPosition], isBookmarked: video[isBookmarked], downloadStatus: video[downloadStatus], updatedAt: video[updatedAt] ) results.append(metadata) } } catch { print("Fetch error: \(error)") } return results } }
SQLiteの最大のメリットは、メモリ効率と実行速度です。特に、大量の動画メタデータを扱う場合、適切なインデックスを張ることで高速な検索が可能です。
ハマった点やエラー解決
実装中に遭遇した主な問題を3つ紹介します。
1. Realmでのマルチスレッドエラー Realmオブジェクトはスレッドセーフではありません。以下のようなエラーが発生しました:
Realm accessed from incorrect thread
解決策
各スレッドで新しいRealmインスタンスを作成し、オブジェクトをThreadSafeReferenceで渡します:
Swift// バックグラウンドでの処理 let realm = try! Realm() let video = realm.object(ofType: VideoMetadata.self, forPrimaryKey: videoId) let safeRef = ThreadSafeReference(to: video) DispatchQueue.main.async { guard let mainRealm = try? Realm(), let mainVideo = mainRealm.resolve(safeRef) else { return } // メインスレッドで処理 }
2. CoreDataでの大量データ挿入時のパフォーマンス問題 1000件以上の動画データを一括挿入すると、非常に時間がかかりました。
解決策
バッチ処理とautoreleasepoolを使用します:
Swiftfunc batchInsertVideos(_ videos: [VideoData]) { let context = persistentContainer.newBackgroundContext() context.undoManager = nil // メモリ節約 for (index, videoData) in videos.enumerated() { autoreleasepool { let video = VideoMetadata(context: context) video.id = videoData.id video.title = videoData.title // ... 他のプロパティ設定 if index % 100 == 0 { try? context.save() context.reset() // メモリ解放 } } } try? context.save() }
3. SQLiteでの日本語検索の文字化け SQLiteでは、デフォルトでUTF-8以外の文字コードを扱うと文字化けが発生します。
解決策 接続時に文字コードを明示的に指定します:
Swiftdb = try Connection("\(path)/videos.sqlite3") db.busyTimeout = 5 try db.run("PRAGMA encoding = \"UTF-8\"")
まとめ
本記事では、Swiftで動画再生アプリを開発する際のローカルデータベース選定と、3つの主要なDB(Realm・CoreData・SQLite)の実装方法を解説しました。
- RealmはSwiftとの親和性が高く、リアクティブな更新が容易
- CoreDataはiCloud同期に強く、大規模データ管理に適している
- SQLiteは最も低レベルな制御が可能で、パフォーマンス重視の場合に最適
この記事を通して、各DBの特徴を理解し、自社の動画再生アプリに最適な選択ができるようになりました。今後は、各DBのマイグレーション戦略や、クラウド連携(Firebase、Supabase等)との組み合わせについても記事にする予定です。
参考資料
- Realm Swift Documentation
- Core Data Programming Guide - Apple Developer
- SQLite.swift Documentation
- iOS App Development with Swift - raywenderlich.com
