Episode 104 — 模仿 iOS Clock App

Shien
彼得潘的 Swift iOS / Flutter App 開發教室
12 min readJul 26, 2022

目前的實力還無法完全模仿出整個功能,期望有招一日能跟上大神們的腳步。

世界時鐘

這區的挑戰是使用 UISearch Bar 來選擇時區,且如果選擇的時間是使用 Search Bar 顯示的,需要考慮到使用 Search Bar 會再 present 頁面,這樣無法在選擇後直接跳到前一頁。

問題

使用 Search Bar 後需要點選兩次才會移到前一頁,但結果會因為點兩次而出現兩次

因為我想再點選 cell 時顯示前往上一頁,但使用 Search Bar 時會自動 present 頁面,所以第一次選擇時無法順利回上一頁。

解決

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedRow = indexPath.row
searchBar.dismiss(animated: true) { self.navigationController?.popToRootViewController(animated: true)
}
}

呼叫 table view 的選擇方法,從 search bar 呼叫 dismiss 方法讓 search bar present 的頁面消失後,再使用 navigation controller 的 popToRootViewController(anitmated:) 回到上一頁。

鬧鐘

鬧鐘有一個特別的功能,在時間到時會跳出通知。

在桌面等待推播,點擊可以進到鬧鐘頁面

使用 UNUserNotificationCenter 顯示推播

extension AlarmSettingTableViewController {
func createNotification(_ time: Int) {
print("notification is triggered")
var trigger: UNTimeIntervalNotificationTrigger?
var request: UNNotificationRequest!
let content = UNMutableNotificationContent()
content.title = "鬧鐘"
content.body = "起床囉"
content.sound = .default
content.badge = 1

if time > 0 {
trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(time), repeats: false)
request = UNNotificationRequest(identifier: "notification", content: content, trigger: trigger)

} else {
request = UNNotificationRequest(identifier: "notification", content: content, trigger: nil)
}
UNUserNotificationCenter.current().add(request) { error in
print("build notification successfully")
}
}
}

分解

var trigger: UNTimeIntervalNotificationTrigger?

UNTimeIntervalNotificationTrigger 可以使用來觸發推播的物件

UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(time), repeats: false)

參數 timeInterval 為幾秒後觸發推播,repeats 為是否重複推播

let content = UNMutableNotificationContent()
content.title = "鬧鐘"
content.body = "起床囉"
content.sound = .default
content.badge = 1

UNMutableNotificationContent() 物件為推播的內容,要選有 Mutable 字樣的才能做內如上的修改。

var request: UNNotificationRequest!

UNNotificationRequest 為發出推播請求的物件

UNNotificationRequest(identifier: "notification", content: content, trigger: trigger)

再安排請求的內容裡要帶入 identifier 名稱、UNNotificationContent 的內容、UNNotificationTrigger 的觸發器。

UNUserNotificationCenter.current().add(request) { error in
print("build notification successfully")
}

在 UNUserNotificationCenter 的現階段物件中新增剛剛建立的 UNNotificationRequest 請求。

問題

新增鬧鐘的頁面用 present 由下往上呈現,無法直接編輯 navigation controller 的 bar item。

解決

func setNavBar() {
let navBackground = UINavigationBarAppearance()
navBackground.configureWithTransparentBackground()

let navBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 50))
navBar.standardAppearance = navBackground
navBar.standardAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
let navBarItem = UINavigationItem(title: "加入鬧鐘")
navBarItem.setLeftBarButtonItems([UIBarButtonItem(title: "取消", style: .done, target: self, action: #selector(dismissPage))], animated: true)
navBarItem.leftBarButtonItem?.tintColor = .orange
navBarItem.setRightBarButton(UIBarButtonItem(title: "儲存", style: .done, target: self, action: #selector(storeClock)), animated: true)
navBarItem.rightBarButtonItem?.tintColor = .orange
navBar.items?.append(navBarItem)
view.addSubview(navBar)
}

分解

let navBackground = UINavigationBarAppearance()
navBackground.configureWithTransparentBackground()

使用 UINavigationBarAppearance 物件讓 navigation bar 背景變透明

let navBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 50))
navBar.standardAppearance = navBackground

新增一個 UINavigationBar 物件並套用剛剛的透明背景物件

navBar.standardAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]

使用 standardAppearance 的 titleTextAttributes 才能更改 bar title 的顏色。

 let navBarItem = UINavigationItem(title: "加入鬧鐘")   
navBarItem.setLeftBarButtonItems([UIBarButtonItem(title: "取消", style: .done, target: self, action: #selector(dismissPage))], animated: true)
navBarItem.leftBarButtonItem?.tintColor = .orange
navBarItem.setRightBarButton(UIBarButtonItem(title: "儲存", style: .done, target: self, action: #selector(storeClock)), animated: true)
navBarItem.rightBarButtonItem?.tintColor = .orange

建立 UINavigationItem 製作標題及設定左右邊的按鈕,

navBar.items?.append(navBarItem)
view.addSubview(navBar)

將 UINavigationItem 的實例加入 UINavigationBar 實例的 items 裡。最後將 UINavigationBar 實例加到 view 上。

碼表

製作的難度在於分圈,要計算當前的時間與上一個紀錄的時間差。

問題

在分圈時紀錄為由上往下,應該要由下往上會更直覺

解決

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? RecordTableViewCell else {
return RecordTableViewCell()
}
cell.roundLabel.text = "第 \(records.count-indexPath.row) 圈"
cell.timeLabel.text = "\(records[records.count-indexPath.row-1])"

return cell
}

在顯示 cell 的方法中,應該都要從最後一個先顯示,也就是總數減掉當前 row 位置,這樣顯示才會是由上往下。

計時器

使用 UIPickerView 來選擇時間,計算所有的秒數,並使用 Timer 物件以一秒的時間進行倒數。要注意的是小時、分鐘、秒數的顯示,要判斷某個時間單位為 0 時是否需要做單位換算的動作。

問題

UIPicker 無法直接在 Inspector 調整字的顏色

解決

func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
switch component {
case 0:
return NSAttributedString(string: String(hours[row]), attributes: [.foregroundColor : UIColor.white])
case 1:
return NSAttributedString(string: String(minutes[row]), attributes: [.foregroundColor: UIColor.white])
case 2:
return NSAttributedString(string: String(minutes[row]), attributes: [.foregroundColor: UIColor.white])
default:
print("something wrong with row title")
return NSAttributedString(string: "", attributes: [.foregroundColor: UIColor.white])
}
}

使用 pickerView 的attributedTitleForRow 方法回傳 NSAttributedString。

--

--