RealmSwiftを使ってオリジナルアプリを作成してみた


これまで現場で携わったアプリ歴

・住宅関連のデータを表示するアプリ
・列車設備点検をアナログ作業からタブレットに切り替える為のアプリ
・オンラインで飲み会を開き、盛り上がり度に応じてお酒を勧めるアプリ

環境

Mac(macOS Catalina 10.15.7)
Xcode(ver 12.2)

使用ライブラリ

・RealmSwift
・FSCalendar

アプリの目的

・毎月100時間学習するために、学習時間を見える化すること

アプリの仕様

・グラフを100時間に対する当月の学習時間の割合に設定
・カレンダーから記録したい日付を選んで内容と時間を記述して保存
・保存後は、TextFieldを固定して、修正ボタンを押さないと変更できない仕様

デザイン

アプリのデザインは、シンプルなものにしました
(画像は、著作権フリーのものを使っています)

画面①

画面②

ソースコード

Realmに保存するデータ

class RecordStudyData:Object {//画面②で保存するデータ
@objc dynamic var contentText:String? //学習内容
@objc dynamic var hourText:String?    //学習時間(時)
@objc dynamic var minuteText:String?  //学習時間(分)
}

class allData: Object {
@objc dynamic var saveDate:String?         //保存した日付
@objc dynamic var hour:String?             
@objc dynamic var minute:String?
@objc dynamic var total_hour:String?      //合計時間(時)
@objc dynamic var total_minute:String?    //合計時間(分)
let recordData = List<RecordStudyData>()  //保存したデータをリストにまとめる
}

画面①

class ViewController: UIViewController {

@IBOutlet weak var thisMonthLabel: UILabel!       //今月
@IBOutlet weak var totalTimeShow: UILabel!        //合計時間を表示
@IBOutlet weak var studyGraphView: PieChartView!  //グラフ
@IBOutlet weak var imageView: UIImageView!        //背景画像
@IBOutlet weak var nextBtn: UIButton!             //戻るボタン


var thisMonth = Date()             
var thisMonthFormatter = DateFormatter().     
var totalTime = ""  //画面②から戻る時に合計時間を受け取る変数
var totalHour = 0   //画面②から戻る時に受け取る時間(時)
var totalMinute = 0 //画面②から戻る時に受け取る時間(分)


override func viewDidLoad() {
    super.viewDidLoad()
    self.navigationController?.isNavigationBarHidden = true
    print(Realm.Configuration.defaultConfiguration.fileURL!)
    thisMonthFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "MM", options: 0, locale: Locale(identifier: "ja_JP"))
    thisMonthLabel.text = "\(thisMonthFormatter.string(from: thisMonth))の学習時間"
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    //内部で保存したデータをアンラップ変数で受け取ってグラフの値を入れる
    if let total_hour = UserDefaults.standard.object(forKey: "totalHour"),let total_minute = UserDefaults.standard.object(forKey: "totalMinute"){
        totalTime = "\(total_hour)時間\(total_minute)分"
        studyGraphView.value = CGFloat((total_hour as! Int) * 60 + (total_minute as! Int))
        print(studyGraphView.value)
    }

    //合計時間の表示
    if totalTime == ""{
        totalTimeShow.text = "00時間00分"
    }else{
        totalTimeShow.text = totalTime
    }


}

//画面レイアウト調整
override func viewDidLayoutSubviews() {
    let iphoneSize_width = UIScreen.main.bounds.width
    let iphoneSize_height = UIScreen.main.bounds.height

    //iPhoneSE1用のサイズ
    if(iphoneSize_width == 320){
        thisMonthLabel.frame.origin.x -= 30
        totalTimeShow.frame.origin.x -= 30
        totalTimeShow.frame.origin.y -= 10
        studyGraphView.frame.origin.x = 0
        studyGraphView.frame.origin.y -= 20
        studyGraphView.frame.size.width -= 20
        studyGraphView.frame.size.height -= 55
        nextBtn.frame.origin.y -= 88
        nextBtn.frame.origin.x = 0
        nextBtn.frame.size.width = totalTimeShow.frame.size.width
    }else if(iphoneSize_width == 375){
        //iPhoneX,XS,11Pro,12mini
        if(iphoneSize_height == 812){
            imageView.frame.size.height = iphoneSize_height
        }
    }else if(iphoneSize_width == 390){//iPhone12,Pro
        imageView.frame.size.width = iphoneSize_width
        imageView.frame.size.height = iphoneSize_height
        studyGraphView.frame.origin.x += 10
        nextBtn.frame.origin.x += 10
        totalTimeShow.frame.origin.x += 10
        thisMonthLabel.frame.origin.x += 10
    }else if(iphoneSize_width == 414){
        imageView.frame.size.height = iphoneSize_height
        imageView.frame.size.width = iphoneSize_width
        totalTimeShow.frame.origin.x += 10
        thisMonthLabel.frame.origin.x += 10
        studyGraphView.frame.origin.x += 17
        nextBtn.frame.origin.x += 17
    }else if(iphoneSize_width == 428){
        imageView.frame.size.height = iphoneSize_height
        imageView.frame.size.width = iphoneSize_width
        totalTimeShow.frame.origin.x += 20
        thisMonthLabel.frame.origin.x += 20
        studyGraphView.frame.origin.x += 24
        nextBtn.frame.origin.x += 24
    }
  }
}

画面②

class StudyTimeRecordViewController: UIViewController,UITextViewDelegate,UITextFieldDelegate,FSCalendarDelegate,FSCalendarDataSource,FSCalendarDelegateAppearance{

@IBOutlet weak var calendar: FSCalendar!
@IBOutlet weak var contentTextView: UITextView!
@IBOutlet weak var hourTextField: UITextField!
@IBOutlet weak var minuteTextField: UITextField!
@IBOutlet weak var backBtn: UIButton!
@IBOutlet weak var saveBtn: UIButton!
@IBOutlet weak var updateBtn: UIButton!
@IBOutlet weak var editView: UIView!
@IBOutlet weak var studyLabel: UILabel!
@IBOutlet weak var hourLabel: UILabel!
@IBOutlet weak var minuteLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!


var dateFomat:DateFormatter = DateFormatter()
let all_data = allData()
let recordData = RecordStudyData()

override func viewDidLoad() {
    super.viewDidLoad()
    contentTextView.delegate = self
    hourTextField.delegate = self
    minuteTextField.delegate = self
    calendar.delegate = self

    contentTextView.isEditable = true
    hourTextField.isEnabled = true
    minuteTextField.isEnabled = true

    backBtn.layer.cornerRadius = 8.0
    saveBtn.layer.cornerRadius = 8.0
    updateBtn.layer.cornerRadius = 8.0

    self.navigationController?.isNavigationBarHidden = true

    //キーボード出現時
    NotificationCenter.default.addObserver(self, selector: #selector(StudyTimeRecordViewController.keyboardWillShow(_ :)), name: UIResponder.keyboardWillShowNotification, object: nil)
    //キーボードが隠れる時
    NotificationCenter.default.addObserver(self, selector: #selector(StudyTimeRecordViewController.keyboardWillHide(_ :)), name: UIResponder.keyboardWillHideNotification, object: nil)
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(true)
    let today = Date()
    dateFomat.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: Locale(identifier:"ja_JP"))
    all_data.saveDate = dateFomat.string(from: today)

    let recordRealm = try! Realm()
    let results = recordRealm.objects(allData.self)

    //画面②の画面が出てくる際に、過去に保存したデータが有れば表示する
    for i in 0..<results.count {

        if all_data.saveDate == results[i].saveDate{
            contentTextView.text = results[i].recordData[0].contentText
            hourTextField.text = results[i].recordData[0].hourText
            minuteTextField.text = results[i].recordData[0].minuteText
            contentTextView.isEditable = false
            hourTextField.isEnabled = false
            minuteTextField.isEnabled = false
        }
    }
}

//カレンダーで日付を押した時、過去に保存されたデータがあれば表示
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
    dateFomat.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: Locale(identifier:"ja_JP"))
    print(dateFomat.string(from: date))
    all_data.saveDate = dateFomat.string(from: date)


    let recordRealm = try! Realm()
    let results = recordRealm.objects(allData.self).sorted(byKeyPath: "saveDate")

    for i in 0..<results.count {

        if all_data.saveDate == results[i].saveDate{
            contentTextView.text = results[i].recordData[0].contentText
            hourTextField.text = results[i].recordData[0].hourText
            minuteTextField.text = results[i].recordData[0].minuteText
            contentTextView.isEditable = false
            hourTextField.isEnabled = false
            minuteTextField.isEnabled = false

            return

        }else {
            contentTextView.text = ""
            hourTextField.text = ""
            minuteTextField.text = ""
            contentTextView.isEditable = true
            hourTextField.isEnabled = true
            minuteTextField.isEnabled = true
        }
    }
}

//当月に保存した学習時間の合計時間を計算
func totalStudyTimeOfMonth(){
    let savedAllDataRealm = try! Realm()
    let savedAllData = savedAllDataRealm.objects(allData.self).sorted(byKeyPath: "saveDate")
    var totalHour = 0
    var totalMinute = 0

    print(savedAllData)
    for i in 0..<savedAllData.count {

        if let hour = savedAllData[i].recordData[0].hourText, let minute = savedAllData[i].recordData[0].minuteText{
            if hour == "" || minute == ""{
                return
            }
            print(hour)
            totalHour += Int(hour)!
            totalMinute += Int(minute)!
        }

        if totalMinute >= 60{
            totalHour += totalMinute / 60
            totalMinute = totalMinute % 60
        }
    }

    UserDefaults.standard.set(totalHour, forKey: "totalHour")
    UserDefaults.standard.set(totalMinute,forKey: "totalMinute")

    navigationController?.popViewController(animated: true)
}

@IBAction func saveAction(_ sender: Any) {
    //空白のまま、保存ボタンを押した時、未入力のフィールドの枠を赤色にする
    if contentTextView.text == ""{
        contentTextView.layer.borderColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        contentTextView.layer.borderWidth = 1.5
        return
    }else if hourTextField.text == ""{
        hourTextField.layer.borderColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        hourTextField.layer.borderWidth = 1.5
        contentTextView.layer.borderColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        contentTextView.layer.borderWidth = 0.0
        return
    }else if minuteTextField.text == "" {
        minuteTextField.layer.borderColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)
        minuteTextField.layer.borderWidth = 1.5
        contentTextView.layer.borderColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        contentTextView.layer.borderWidth = 0.0
        hourTextField.layer.borderColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        hourTextField.layer.borderWidth = 0.0
        return
    }


    let savedDataRealm = try! Realm()
    let savedDataResults = savedDataRealm.objects(allData.self).sorted(byKeyPath: "saveDate")

    if savedDataResults.count > 0 {
        print(savedDataResults)
        //過去に保存されていないかをチェック
        for i in 0..<savedDataResults.count {
            if all_data.saveDate == savedDataResults[i].saveDate{
                try! savedDataRealm.write{
                    savedDataRealm.delete(savedDataResults[i])
                }
                break
            }
        }
    }

    //記入事項を保存する処理
    if let studyContent = contentTextView.text,let hour = hourTextField.text, let minute = minuteTextField.text{
        recordData.contentText = studyContent
        recordData.hourText = hour
        recordData.minuteText = minute

        let saveData = allData(value: [
            "saveDate":all_data.saveDate!,
            "hour":recordData.hourText!,
            "minute":recordData.minuteText!,
            "recordData": [
                ["contentText":recordData.contentText!,"hourText":recordData.hourText!,"minuteText":recordData.minuteText!]
            ]
        ])

        let saveDataRealm = try! Realm()

        try! saveDataRealm.write{
            saveDataRealm.add(saveData)
        }
    }
    contentTextView.isEditable = false
    hourTextField.isEnabled = false
    minuteTextField.isEnabled = false

    totalStudyTimeOfMonth()
}

@IBAction func updateAction(_ sender: Any) {
   contentTextView.isEditable = true
   hourTextField.isEnabled = true
   minuteTextField.isEnabled = true
}

@IBAction func back(_ sender: Any) {
    navigationController?.popViewController(animated: true)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    view.endEditing(true)
}

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}

@objc func keyboardWillShow(_ notification:Notification){
    let keyboardHeight = ((notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as Any) as AnyObject).cgRectValue?.height

    editView.frame.origin.y = UIScreen.main.bounds.height - keyboardHeight! - editView.frame.size.height
}

@objc func keyboardWillHide(_ notification:Notification){
    editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height

    guard let _ = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue,
        let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval else {return}

    UIView.animate(withDuration: duration){
        let transform = CGAffineTransform(translationX: 0, y: 0)

        self.view.transform = transform
    }

}

override func viewDidLayoutSubviews() {

    let iphoneSize_width = UIScreen.main.bounds.width
    let iphoneSize_height = UIScreen.main.bounds.height

    //iPhoneSE1用のレイアウト
    if(iphoneSize_width == 320){
        editView.frame.origin.y += 90
        editView.frame.size.width = iphoneSize_width
        editView.frame.size.height -= 90
        contentTextView.frame.size.width -= 55
        contentTextView.frame.size.height -= 20
        updateBtn.frame.origin.x -= 55
        updateBtn.frame.origin.y -= 85
        saveBtn.frame.origin.y -= 85
        backBtn.frame.origin.y -= 25
        studyLabel.frame.origin.y -= 20
        hourLabel.frame.origin.y -= 25
        minuteLabel.frame.origin.y -= 25
        hourTextField.frame.origin.y -= 25
        minuteTextField.frame.origin.y -= 25
        calendar.frame.size.width = iphoneSize_width
        calendar.frame.size.height -= 20
        calendar.frame.origin.y -= 20
    }else if(iphoneSize_width == 375 && iphoneSize_height == 812){
        editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height
        calendar.frame.size.height += 100
        calendar.frame.size.width -= 20
        calendar.frame.origin.x += 10
        calendar.frame.origin.y += 20
        backBtn.frame.origin.y += 20
    }else if(iphoneSize_width == 390){
        imageView.frame.size.width = iphoneSize_width
        editView.frame.size.width = iphoneSize_width
        editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height
        contentTextView.frame.size.width += 16
        calendar.frame.size.height += 100
        calendar.frame.size.width = iphoneSize_width
        calendar.frame.size.width -= 20
        calendar.frame.origin.x += 10
        calendar.frame.origin.y += 20
        updateBtn.frame.origin.x += 15
        backBtn.frame.origin.y += 20
    }else if(iphoneSize_width == 414){
        if(iphoneSize_height == 736){
            imageView.frame.size.width = iphoneSize_width
            imageView.frame.size.height = iphoneSize_height
            editView.frame.size.width = iphoneSize_width
            editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height
            contentTextView.frame.size.width += 37
            updateBtn.frame.origin.x += 40
            calendar.frame.size.width = iphoneSize_width
            calendar.frame.size.height += 40
            calendar.frame.origin.y += 30
            backBtn.frame.origin.y += 20

        }else{
            imageView.frame.size.width = iphoneSize_width
            imageView.frame.size.height = iphoneSize_height
            editView.frame.size.width = iphoneSize_width
            editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height
            contentTextView.frame.size.width += 37
            updateBtn.frame.origin.x += 40
            calendar.frame.size.width = iphoneSize_width
            calendar.frame.size.height += 80
            calendar.frame.origin.y += 80
            backBtn.frame.origin.y += 20
        }
    }else if(iphoneSize_width == 428){
        imageView.frame.size.width = iphoneSize_width
        imageView.frame.size.height = iphoneSize_height
        editView.frame.size.width = iphoneSize_width
        editView.frame.origin.y = UIScreen.main.bounds.height - editView.frame.size.height
        contentTextView.frame.size.width += 50
        updateBtn.frame.origin.x += 50
        calendar.frame.size.width = iphoneSize_width
        calendar.frame.size.height += 100
        calendar.frame.origin.y += 80
        backBtn.frame.origin.y += 20
    }
  }
}

今回のアプリ制作で苦労したこと

・DBに保存するデータを決める
・デバイス毎のレイアウトの調整
・RealmSwiftの保存処理の実装時のエラー解決
・合計時間を計算するメソッドの作成

参考

グラフ

Realm

Udemy