#035 頭痛日記 App — part 1 CoreData
My Migraine Diary 痛痛日記
Concept
隨時隨地紀錄頭痛,正在持續的頭痛狀態也能即時紀錄,長期之下方便醫生問診,也能從中找出引發自身頭痛的真兇。
製作這個 App 的緣由,來自於自己偶爾的偏頭痛,有陣子痛到去看醫生,才知道長期紀錄有助於釐清自己頭痛來源,當時一直找不到適合的 App ,只好用備忘錄記錄,整體紀錄起來麻煩外,也難統計數據與瀏覽,因此催生出了這個 App,後續有很多想繼續增加的功能,比如新增客製化選項、第三方登入、推播及數據分析等等…
前期規劃
用到的技術
– CoreData 儲存資料
– News API 串接與 Json 格式解析
– UIButton & ScrollView Programmatically
– 實作搜尋過往頭痛紀錄功能
– SPM Third Party — Charts / 實作 Combine Chart
– SPM Third Party — Google AdMob/ 實作 GADBanner
資料儲存Core Data
有些數據要用 array 的形式儲存,例如頭痛的誘因及症狀,有時會有多種狀況同時發生
將 Type 選為 Transformable 就可以儲存 Array, Dictionary, UIColor… 等類型,但記得 Transformer 要改為 NSSecureUnarchiveFromData ,不然會一直印出這些資料未來不能被讀取的 log
在讀取的時候,記得要轉型出來才能夠讀取資料,以下我在 NSObject 新增 extension ,需要讀取資料的時候就可以呼叫這些方法
extension NSObject {
func toStringArray(_ array: NSObject?) -> [String]? {
if let nsArray = array as? [String] {
return nsArray
} else {
return nil
}
}
}
let selectStrs = record.symptom?.toStringArray(record.symptom)
// 將 Record 內儲存的 symptom 轉成 [String]
前置作業
在 AppDelegate 中建立 persistentContainer ,記得 name 要填入coreData 建立的名字,不然 App 會直接打不開
建立 saveContext 方法和 extension 方便 controller 內可以運用 persistentContainer 呼叫
SceneDelegate 中可以直接餵給需要用的 controller ,但切記,如果是第二頁的畫面,SceneDelegate 這邊是給不到的,我在這邊疑惑超久,問了彼得潘大大一秒解惑,覺得自己很搞笑,因為第二頁你沒點擊根本不會產生,所以 SceneDelegate 想給也給不了
Controller 內讀取、儲存、更新、刪除資料
// 第二頁的 controller
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// update record 更新資料
if let record {
record.startTime = start
record.endTime = end
record.location = location
record.score = Int16(score)
record.symptom = symptomCell.selectStrs as NSArray
record.sign = signCell.selectStrs as NSArray
record.cause = causeCell.selectStrs as NSArray
if let placeCellSelectStr = placeCell.selectStr {
record.place = placeCellSelectStr
}
if let medCellSelectStr = medCell.selectStr {
record.med = medCellSelectStr
}
if let effectCelSelectStr = effectCell.selectStr {
record.medEffect = effectCelSelectStr
}
record.medQuantity = quantity ?? 0.0
record.note = note
record.stillGoing = stillGoingBtn.isSelected
record.medUnit = Int16(quantitySegment.selectedSegmentIndex)
if let quantity {
record.medQuantity = quantity
}
appDelegate.persistentContainer.saveContext()
}
// save new record 儲存
save(start: start, end: end, location: location, score: score, symptom: symptomCell.selectStrs, sign: signCell.selectStrs, cause: causeCell.selectStrs, place: place, med: med, medEffect: effect, medQuantity: quantity, note: note, quantitySegment: quantitySegment.selectedSegmentIndex)
// delete record 刪除
let context = appDelegate.persistentContainer.viewContext
context.delete(record)
appDelegate.persistentContainer.saveContext()
//第一頁的 controller
var container: NSPersistentContainer!
records = container.getRecordsTimeAsc()
func getRecordsTimeAsc() -> [Record] {
var records = [Record]()
// fetch 所有資料
let request = Record.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \Record.startTime, ascending: false)
]
do {
records = try viewContext.fetch(request)
} catch {
print("fetch faild")
}
return records
}
UIButton & ScrollView Programmatically
未來想要可以讓使用者自行新增選項,所以這邊用程式生成按鈕們,再塞進 StackView 中為了讓大家都是 top alignment ,再包進 ScrollView 中
小插曲,最近面試的時候,有面試官提議說,其實以上這些可以用 CollectionView ,會更快也更方便未來新增,自己完全沒想到!
func configBtnsForMultiSelect(buttonCount: Int, view: UIView, title: [String], selectStrs: [String]?, imageNames: [String]){
// 建立樣式
var configuration = UIButton.Configuration.plain()
configuration.imagePlacement = .top
configuration.imagePadding = 6
// 建立scrollView
let scrollView = UIScrollView(frame: CGRect(x: 0, y: y, width: 375, height: viewH))
let totalBtnsWidth = (buttonWidth + spacing) * buttonCount
scrollView.contentSize = CGSize(width: totalBtnsWidth, height: viewH)
scrollView.showsHorizontalScrollIndicator = false
// 生成指定數量的UIButton
for i in 0..<buttonCount {
// 計算UIButton的x和y座標
let x = i * buttonWidth
// 建立文字
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attrStr = NSAttributedString(string: title[i], attributes: [
.font: UIFont.systemFont(ofSize: 12),
.foregroundColor: UIColor.appColor(.lightBlu)!,
.paragraphStyle: paragraph
])
// btn Image
configuration.image = UIImage(named: imageNames[i])
// 建立新的UIButton
let button = UIButton(configuration: configuration)
button.frame = CGRect(x: x, y: 0, width: buttonWidth, height: buttonHeight)
button.setAttributedTitle(attrStr, for: .normal)
// 判斷Button有沒有被選過
if let selectStrs {
selectStrs.forEach { str in
if str == attrStr.string {
button.isSelected = true
}
}
}
// 點擊Btn後要做的事
button.addTarget(self, action: #selector(buttonSelected(sender:)), for: .touchUpInside)
// 將新的UIButton加入陣列中
buttons.append(button)
}
// 建立stackView
let stackV = UIStackView(frame: CGRect(x: 0, y: 0, width: totalBtnsWidth, height: viewH))
stackV.axis = .horizontal
stackV.alignment = .top
stackV.distribution = .equalSpacing
stackV.spacing = CGFloat(spacing)
for button in buttons {
stackV.addArrangedSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: button, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: CGFloat(buttonWidth)).isActive = true
}
scrollView.addSubview(stackV)
view.addSubview(scrollView)
// add constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
stackV.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
NSLayoutConstraint(item: scrollView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: scrollView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: scrollView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: CGFloat(y)),
NSLayoutConstraint(item: scrollView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: CGFloat(viewH)),
NSLayoutConstraint(item: stackV, attribute: .leading, relatedBy: .equal, toItem: scrollView.contentLayoutGuide, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: stackV, attribute: .trailing, relatedBy: .equal, toItem: scrollView.contentLayoutGuide, attribute: .trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: stackV, attribute: .top, relatedBy: .equal, toItem: scrollView.contentLayoutGuide, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: stackV, attribute: .bottom, relatedBy: .equal, toItem: scrollView.contentLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)
])
}
下集來介紹 第三方套件們~~~