これまで現場で携わったアプリ歴
・住宅関連のデータを表示するアプリ
・列車設備点検をアナログ作業からタブレットに切り替える為のアプリ
・オンラインで飲み会を開き、盛り上がり度に応じてお酒を勧めるアプリ
環境
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の保存処理の実装時のエラー解決
・合計時間を計算するメソッドの作成