【断酒iOSアプリ制作】Realmでデータの永続化 (9日目)
断酒してから9日が経過しました!お酒のない生活にも少しずつ慣れ、空いた時間で新しいことに挑戦する余裕も生まれてきました。その一環として、アプリ開発をコツコツ進めております。健康志向が芽生えてきたこともあり、ジョギングを趣味として始めたのも最近の大きな変化です。普段は30分ほど、3km〜5kmを目安に走る程度ですが、ある日妙に調子が良く、40km近く走る暴挙に出た結果、膝を痛めるという大失態を犯しました。その後1ヶ月ほど苦しみましたが、ようやく膝も治り、再びジョギング生活が戻ってきました。
そんな中、新たな相棒としてランニングシューズを購入しました。つい先日まではAmazonで適当に買った3000円のシューズを履いていましたが、これが微妙に大きく、走りづらさを感じる原因だったようです。先日、 高尾山に登った帰り に新宿のL-Breathに立ち寄り、アシックスのシューズを購入することに。 驚いたのは、店舗に備わっている最新鋭のレーザースキャン。足のサイズを正確に計測し、自分に合った靴を即座に提案してくれました。結果、自分が想定していたよりも足のサイズが小さいことが発覚!一方で横幅が広いため、サイズ選びに苦戦していた理由がようやくわかりました。新しいシューズは「これだ!」と思える履き心地で、地面からの衝撃をしっかり吸収してくれそうです。これなら膝を痛める心配も少なそう。トレンドの厚底シューズの恩恵に、さっそく期待が高まります。
ここで改めて感じたのは、「適切なツール選び」の重要性です。シューズ一つでこれだけ快適さが変わるのなら、アプリ開発においても適切なツールを選ぶことで効率や完成度に大きな差が出るはずです。
さて、今回進めている断酒サポートアプリの開発でも、この「ツール選び」がポイントとなりました。iOSアプリ開発ではデータの永続化が欠かせませんが、どの方法を使うべきか迷うところです。候補として挙がるのは以下の通り:
- UserDefaults:小規模な設定データの保存向け
- Core Data:強力だが、学習コストが高い
- SQLite:柔軟だが、低レベルな操作が必要
- Realm:シンプルかつ直感的で高速
今回は、セットアップが容易で直感的に使える Realm を選びました。ここからは、このRealmを使ってアプリのデータを永続化する方法について紹介していきます。
Realmの概要
Realmはモバイル向けのデータベースで、以下の特徴があります:
- 高速:SQLiteベースのデータベースより高速な読み書きが可能。
- 簡単:Core Dataと比べてシンプルなAPI設計。
- スキーマの自動管理:手動でデータベースのスキーマを管理する必要がない。
- クロスプラットフォーム対応:iOS、Android、React Nativeなど複数のプラットフォームで使用可能。
プロジェクトにRealmを導入
CocoaPodsを使用したRealmのインストール
以下のようにPodfileを編集してRealmをインストールします。
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
use_modular_headers!
target 'RealmSampleApp' do
use_frameworks!
pod 'RealmSwift'
end
その後、以下のコマンドを実行します:
pod install
これでプロジェクトにRealmが導入されます。
UIの実装
以下のようなUIを準備します。
class ViewController: UIViewController {
// UIコンポーネント
let datePicker: UIDatePicker = {
let picker = UIDatePicker()
picker.datePickerMode = .date
picker.translatesAutoresizingMaskIntoConstraints = false
return picker
}()
let textView: UITextView = {
let textView = UITextView()
textView.layer.borderColor = UIColor.lightGray.cgColor
textView.layer.borderWidth = 1.0
textView.layer.cornerRadius = 5.0
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let saveButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("保存", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let loadButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("読み込み", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let resultLabel: UILabel = {
let label = UILabel()
label.text = "日記がここに表示されます"
label.numberOfLines = 0
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let idTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "削除したいIDを入力"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("削除", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
saveButton.addTarget(self, action: #selector(saveDiary), for: .touchUpInside)
loadButton.addTarget(self, action: #selector(loadDiary), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteDiary), for: .touchUpInside)
}
// UIのレイアウト設定
func setupUI() {
view.addSubview(datePicker)
view.addSubview(textView)
view.addSubview(saveButton)
view.addSubview(loadButton)
view.addSubview(resultLabel)
view.addSubview(idTextField)
view.addSubview(deleteButton)
NSLayoutConstraint.activate([
datePicker.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
datePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textView.topAnchor.constraint(equalTo: datePicker.bottomAnchor, constant: 20),
textView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textView.widthAnchor.constraint(equalToConstant: 300),
textView.heightAnchor.constraint(equalToConstant: 150),
saveButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20),
saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -70),
loadButton.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20),
loadButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 70),
resultLabel.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 30),
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.widthAnchor.constraint(equalToConstant: 300),
idTextField.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20),
idTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
idTextField.widthAnchor.constraint(equalToConstant: 300),
deleteButton.topAnchor.constraint(equalTo: idTextField.bottomAnchor, constant: 10),
deleteButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
// 日記を保存する処理
@objc func saveDiary() {
let realm = try! Realm()
let selectedDate = datePicker.date
// プライマリキーがYYYYMMDD形式になるように設定
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd" // 固定フォーマット
let id = dateFormatter.string(from: selectedDate)
print(id) // 例: 20241129
// データ作成または更新
let entry = DiaryEntry()
entry.id = id
entry.date = selectedDate
entry.content = textView.text
try! realm.write {
realm.add(entry, update: .modified)
}
resultLabel.text = "日記を保存しました!"
textView.text = ""
}
...
}
UI説明
このアプリのUIは、日記を保存、読み込み、削除するためのシンプルな構成になっています。以下、それぞれの要素と役割について説明します:
日付選択(UIDatePicker)
日記の対象日を指定するために使用します。デフォルトでは今日の日付が選択されています。
テキスト入力エリア(UITextView)
日記の内容を入力するためのエリアです。入力後、「保存」ボタンを押すことでRealmに保存されます。
保存ボタン(UIButton)
入力した日記をRealmに保存します。同じ日付がすでに保存されている場合は上書きされます。
読み込みボタン(UIButton)
選択した日付の日記をRealmから読み込み、結果を表示します。
結果表示ラベル(UILabel)
読み込まれた日記や処理の結果を表示します。日記が存在しない場合は「指定の日付に日記はありません」と表示されます。
削除用ID入力フィールド(UITextField)
削除したい日記のIDを手動で入力するためのフィールドです。
削除ボタン(UIButton)
入力されたIDの日記をRealmから削除します。該当する日記がない場合はエラーメッセージを表示します。
Realmを使ったデータ操作の実装例
以下は、日記アプリを例にしたデータ操作の実装コードです。
モデル定義
まず、DiaryEntryモデルを作成します。
import RealmSwift
class DiaryEntry: Object {
@Persisted(primaryKey: true) var id: String // プライマリキー
@Persisted var date: Date
@Persisted var content: String
}
UIのセットアップと機能実装
以下のコードを参考に、ViewControllerにUIと機能を実装します。
日記の保存
@objc func saveDiary() {
let realm = try! Realm()
let selectedDate = datePicker.date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
let id = dateFormatter.string(from: selectedDate)
let entry = DiaryEntry()
entry.id = id
entry.date = selectedDate
entry.content = textView.text
try! realm.write {
realm.add(entry, update: .modified)
}
resultLabel.text = "日記を保存しました!"
textView.text = ""
}
日記の読み込み
@objc func loadDiary() {
let realm = try! Realm()
let selectedDate = datePicker.date
let startOfDay = Calendar.current.startOfDay(for: selectedDate)
let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
let predicate = NSPredicate(format: "date >= %@ AND date < %@", startOfDay as NSDate, endOfDay as NSDate)
let entry = realm.objects(DiaryEntry.self).filter(predicate).first
if let entry = entry {
resultLabel.text = "日記: \(entry.content)"
idTextField.text = entry.id
} else {
resultLabel.text = "指定の日付に日記はありません"
}
}
日記の削除
@objc func deleteDiary() {
let realm = try! Realm()
guard let id = idTextField.text, !id.isEmpty else {
resultLabel.text = "IDを入力してください"
return
}
if let entry = realm.object(ofType: DiaryEntry.self, forPrimaryKey: id) {
try! realm.write {
realm.delete(entry)
}
resultLabel.text = "ID: \(id) の日記を削除しました"
idTextField.text = ""
} else {
resultLabel.text = "ID: \(id) の日記は存在しません"
}
}
マイグレーション方法
アプリのリリース後にデータモデルを変更した場合、マイグレーションが必要です。以下はマイグレーションの設定例です。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 2 {
// 必要なマイグレーション処理
}
}
)
Realm.Configuration.defaultConfiguration = config
return true
}
コードを試してみる
この記事で使用したコードは以下のGitHubリポジトリで公開しています。ぜひご活用ください。 GitHub - aragig/ios_sample_dansyu: 断酒カレンダーアプリ制作で使う、部品サンプルです
今回のサンプルアプリは、RealmSampleAppターゲットをビルドすると再現できます。
まとめ
今回のアプリでは、Realmを使用したデータの永続化を通じて、以下の機能を実現しました:
- 日記の保存:シンプルなフォームでデータの保存を実装。
- 日記の読み込み:日付でのクエリを使用したデータの取得。
- 日記の削除:プライマリキーを活用して特定データを削除。
これをベースに、グラフや統計機能を追加することで、さらに価値のあるアプリへと発展させることが可能です。