Appleから提供されている通知アプリリマインダーに似たアプリを個人的に自作してみました。
とはいえ本家にはだいぶ劣りますがシンプルさを活かして作成しています。
個人開発でリマインダーを自作してみた
今回作るのはシンプルさを売りにしたリマインダーです。組み込む機能はできるだけ少なく、誰でも直感的に使用方法がわかるようなアプリを意識して開発していきます。
必要な機能
- 通知を登録
- 通知を配信
- 登録された通知を削除
- 登録された通知を確認
作成するページ
- 通知登録ページ
- 通知リスト管理ページ
環境
- フレームワーク:SwiftUI
- データベース:Realm
今回は作り方がわかるように出来るだけ流れに沿って解説していきます。
実際に真似して作ってみてください。
開発の流れ
リマインダーアプリを開発する流れを先にまとめておきます。
- Realmデータベースクラスの作成
- 通知管理クラスの作成
- 登録ページの作成
- リスト表示ページの作成
ここではRealm(Realm Swiftライブラリ)の導入方法や使い方などは解説しませんので以下記事を参考にしてください。
【SwiftUI】Realm Swiftとは?導入方法とCRUD処理のやり方
Realmデータベースクラスの作成
リマインドしたい情報はデータベースの中に格納して保存していきます。これで現在登録している通知の把握と不要になった通知の削除が行えるようにします。
まずは保存するテーブル構造を定義します。
import UIKit
import RealmSwift
class Notification: Object ,ObjectKeyIdentifiable{
@Persisted(primaryKey: true) var id:UUID = UUID()
@Persisted var body:String = ""
@Persisted var date:Date = Date()
}
このファイルでやっていること
- Realm用のテーブル定義
- 削除できるようにプライマリーキーの指定
通知管理クラスの作成
続いて通知を実際に配信するためのクラスを作成します。SwiftではUNUserNotificationCenterクラスが通知を管理しています。
【SwiftUI】通知機能の実装方法!ローカル通知とリモート通知の違い
このクラスでは通知を設定するメソッドと通知を削除するメソッドを定義しておきます。
import UIKit
class NotificationRequestManager: NSObject {
func sendsendNotificationRequest(id:UUID,str:String,dateStr:String){
let content = UNMutableNotificationContent()
content.title = "Remind"
content.body = str
// "yyyy-MM-dd-H-m"形式で取得した文字列を配列に変換
let array = dateStr.split(separator: "-")
let dateComponent = DateComponents(year: Int(array[0]),
month: Int(array[1]),
day: Int(array[2]),
hour: Int(array[3]),
minute: Int(array[4]),
second: 0)
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponent, repeats: false)
let request = UNNotificationRequest(identifier: id.uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
func removeNotificationRequest(id:UUID){
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: [id.uuidString])
// 通知を確認用
// UNUserNotificationCenter.current().getPendingNotificationRequests { array in
// print(array)
// }
}
}
【Swift】DateComponentsとは?使用方法とDate型との違い
このファイルでやっていること
- 通知を設定するメソッドを定義
- 設定する日付は引数として受け取る
- DateComponentsへの変換
- 通知の識別子にテーブルIDを流用
- 通知を削除するメソッドを定義
登録ページの作成
続いて通知を登録するページを作ります。ここではカレンダーを中央に配置し、日付と時間をすぐに指定できるようにします。
あとは登録を実行するためのボタンを設置します。
import SwiftUI
import RealmSwift
struct EntryNotificationView: View {
// MARK: - Models
let manager = NotificationRequestManager()
@ObservedResults(Notification.self) var notification
// MARK: - Input
@State var date:Date = Date()
@State var text:String = ""
// MARK: - View
@State var isInput:Bool = true
@State var isAlert:Bool = false
// MARK: - method
func resetData(){
date = Date()
text = ""
}
var body: some View {
VStack{
Spacer()
// MARK: - DatePicker
DatePicker(selection: $date, label: {
Text("日付")
})
.environment(\.locale, Locale(identifier: "ja_JP"))
.environment(\.calendar, Calendar(identifier: .japanese))
.environment(\.timeZone, TimeZone(identifier: "Asia/Tokyo")!)
.datePickerStyle(.graphical)
// MARK: - DatePicker
// MARK: - TextField
TextField("通知内容", text: $text)
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(isInput ? Color("AccentColor") :.red ,lineWidth: 3)
)
.textFieldStyle(.roundedBorder)
.padding([.bottom,.trailing,.leading])
// MARK: - TextField
// MARK: - EntryButton
Button(action: {
if text != "" {
let notice = Notification()
notice.body = text
notice.date = date
let realm = try! Realm()
try! realm.write {
// 現在の日時より古い通知情報を削除
let result = realm.objects(Notification.self).where({$0.date < Date()})
print(realm.objects(Notification.self))
realm.delete(result)
// 追加処理
realm.add(notice)
// 通知セット
let currntItem = realm.objects(Notification.self).last!
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd-H-m"
let dateStr = df.string(from: currntItem.date)
manager.sendsendNotificationRequest(id: currntItem.id, str: currntItem.body, dateStr:dateStr)
}
isAlert = true
isInput = true
resetData()
}else{
isInput = false
}
}, label: {
Text("登録")
.fontWeight(.bold)
.padding()
.background(Color("AccentColor"))
.foregroundColor(.white)
.cornerRadius(5)
}).alert("Remindを登録しました。", isPresented: $isAlert, actions: {})
Spacer()
}
}
}
このファイルでやっていること
- 通知レコードを生成するための入力ボックスを定義
- 日付と時間はカレンダーから
- 通知内容はTextFieldから
- 登録ボタン押下でインサート処理
- 通知を削除するメソッドを定義
リスト表示ページの作成
2つ目の登録された通知をリストで表示するページを作成します。
import SwiftUI
import RealmSwift
struct ListNotificationView: View {
// MARK: - Models
let manager = NotificationRequestManager()
// 当日の日付よりも後にセットされている通知のみ表示かつ日付の新しい古い順にソート
@ObservedResults(Notification.self,where: {$0.date >= Date()},sortDescriptor:SortDescriptor(keyPath: "date", ascending: true)) var notification
// MARK: - View
@State var isAlert:Bool = false
var body: some View {
NavigationView{
VStack{
List{
ForEach(notification){ notice in
RowNotificationView(notice: notice)
}.onDelete(perform: { index in
manager.removeNotificationRequest(id:notification[index.first!].id)
$notification.remove(atOffsets: index)
isAlert = true
})
}.alert("Remindを削除しました。", isPresented: $isAlert, actions: {})
Spacer()
}.navigationTitle("RemindList")
}.navigationViewStyle(.stack)
}
}
struct ListNotificationView_Previews: PreviewProvider {
static var previews: some View {
ListNotificationView()
}
}
このファイルでやっていること
- 表示するデータは当日の日付より後のもののみをRealmDBから取得する
- 取得したデータをリスト表示
- 行は別ビューとして定義
リスト表示する1行1行は管理しやすいように別ビューに定義しておきます。
import SwiftUI
import RealmSwift
struct RowNotificationView: View {
func df() -> DateFormatter{
let df = DateFormatter()
df.calendar = Calendar(identifier: .gregorian)
df.locale = Locale(identifier: "ja_JP")
df.timeZone = TimeZone(identifier: "Asia/Tokyo")
df.dateFormat = "yyyy年\nMM月dd日\nH時mm分"
return df
}
@ObservedRealmObject var notice:Notification
var body: some View {
HStack{
Image(systemName: "checkmark.icloud.fill").foregroundColor(Color("AccentColor")).font(.system(size: 20))
Text(df().string(from: notice.date)).fontWeight(.bold).font(.system(size: 12)).multilineTextAlignment(.trailing)
Text(notice.body).fontWeight(.bold).font(.system(size: 20)).padding(.leading).lineLimit(1).foregroundColor(Color("AccentColor"))
}
}
}
最後にTabView構造体を使って作成した2つのページをタブページとして切り替えられるようにしておきます。
import SwiftUI
import RealmSwift
struct ContentView: View {
@State var selectedTag:Int = 1
var body: some View {
TabView(selection: $selectedTag) {
EntryNotificationView().tabItem({
Image(systemName: "icloud.and.arrow.up.fill")
}).tag(1)
ListNotificationView().tabItem({
Image(systemName: "list.bullet")
}).tag(2)
}
}
}
これでリマインドアプリが完成しました。
難しかったところ
- 通知登録時のDateComponentsへの変換
通知登録時に渡すDateComponentsは必要な情報のみでないと正常に動作しないようで、それを見つけるのに試行錯誤を繰り返しました。
またDate型がUTCでしか保持できないため日本時間でセットすることができず日本時間の文字列に変換して渡すことで解決することができました。
- Realmを使ったデータベース操作
Realmを使ったデータベース操作は今回が初めての試みだったのですが操作は意外と簡単で直感的に使用することができました。
それでもやはり不慣れな部分も多く、リストからスワイプて削除する流れは少し手間取りました。
感想
今回リマインダーを自作してiOSアプリにおける通知の実装方法とRealmを使ったデータベース操作の方法を学ぶことができました。
アプリ自体はとてもシンプルで実際に自分で使っていますが割と使いやすくて気に入っています。
趣味でやってる個人開発ですので至らぬ点や勘違いがあるかもしれませんが何かありましたら教えていただけると嬉しいです。
ご覧いただきありがとうございました。
\インストールはこちら/
シンプル通知アプリ-Remind