3
3

【SwiftUI】リマインダーを自作!作成手順と開発方法

Last updated at Posted at 2022-10-24

Appleから提供されている通知アプリリマインダーに似たアプリを個人的に自作してみました。

とはいえ本家にはだいぶ劣りますがシンプルさを活かして作成しています。

892BB750-91EF-46EB-902D-79BE4FF2BF42.jpeg

F11F449D-11C6-4996-8098-FA90F111139F.jpeg

個人開発でリマインダーを自作してみた

今回作るのはシンプルさを売りにしたリマインダーです。組み込む機能はできるだけ少なく、誰でも直感的に使用方法がわかるようなアプリを意識して開発していきます。

シンプル通知アプリ-Remind

必要な機能

  • 通知を登録
  • 通知を配信
  • 登録された通知を削除
  • 登録された通知を確認

作成するページ

  • 通知登録ページ
  • 通知リスト管理ページ

環境

  • フレームワーク:SwiftUI
  • データベース:Realm

今回は作り方がわかるように出来るだけ流れに沿って解説していきます。
実際に真似して作ってみてください。

開発の流れ

リマインダーアプリを開発する流れを先にまとめておきます。

  1. Realmデータベースクラスの作成
  2. 通知管理クラスの作成
  3. 登録ページの作成
  4. リスト表示ページの作成

ここではRealm(Realm Swiftライブラリ)の導入方法や使い方などは解説しませんので以下記事を参考にしてください。

【SwiftUI】Realm Swiftとは?導入方法とCRUD処理のやり方

Realmデータベースクラスの作成

リマインドしたい情報はデータベースの中に格納して保存していきます。これで現在登録している通知の把握と不要になった通知の削除が行えるようにします。

まずは保存するテーブル構造を定義します。

Notification.swift
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】通知機能の実装方法!ローカル通知とリモート通知の違い

このクラスでは通知を設定するメソッド通知を削除するメソッドを定義しておきます。

NotificationRequestManager.swift
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を流用
  • 通知を削除するメソッドを定義

登録ページの作成

続いて通知を登録するページを作ります。ここではカレンダーを中央に配置し、日付と時間をすぐに指定できるようにします。
あとは登録を実行するためのボタンを設置します。

EntryNotificationView.swift
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つ目の登録された通知をリストで表示するページを作成します。

ListNotificationView.swift
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行は管理しやすいように別ビューに定義しておきます。

RowNotificationView.swift
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つのページをタブページとして切り替えられるようにしておきます。

ContentView.swift
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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3