#27 模仿 iOS Clock App - 1 :World Clock
Published in
13 min readJan 17, 2022
原本只打算利用iOS Clock來練習頁面間資料傳遞的功能,但發現其所有功能涵蓋了很多之前沒有練習過的技術,可作為交叉學習與練習的範例。
實現功能
- 取得世界上主要的城市清單。
- 城市清單的分類排序和右邊的索引功能。
- 搜尋城市的功能
- 取得城市的時間。
- 城市時間的新增,刪除和儲存。
- 表格資料的刪除和順序調整功能(利用右邊的三條線調整順序)。
- 使用者設定的資料儲存
練習目地
- 清單的section分類與sectionIndexTitles 索引功能
- UISearchBar搜尋功能與畫面控制
- 清單資料的選取與傳遞回前一頁
- tableView cell的新增、刪除與調整順序
- UserDefaults的使用者參數設定
時鐘顯示的資料
由於後續需要建立可索引的時區城市Dictionary,針對每筆城市先建立所有需要顯示在畫面上的物件。以下是要顯示的資料說明:
- identifier : 從系統獲取的時區id,基本上顯示的格式如 “Asia/Taipei”
- cityName:將id拆轉換成只有城市的名稱,如Taipei
- relativeHours:相對本地時區的時間差如 +8HRS,會需要特別處理正負跟複數顯示
- relativeDate:相對本地的日期,顯示成Yesterday, Today, Tomorrow
- localTime:當地時間
struct City { var identifier: String
init(identifier: String) {
self.identifier = identifier
}
var cityName: String {
String(identifier.split(separator: "/").last!).replacingOccurrences(of: "_", with: " ")
} var relativeHours: String {
let timeInterval = TimeZone(identifier: identifier)!.secondsFromGMT() - TimeZone.current.secondsFromGMT()
let hours = timeInterval / 3600
return "\(hours.signum() == 1 ? "+" : "")\(String(format: "%.1d", hours))\(abs(hours) == 1 ? "HR" : "HRS")"
} var relativeDate: String {
let dateFormater = DateFormatter()
dateFormater.timeStyle = .none
dateFormater.dateStyle = .medium
dateFormater.timeZone = TimeZone(identifier: identifier)
dateFormater.doesRelativeDateFormatting = true
return dateFormater.string(from: .now)
} var localTime: String {
let dateFormater = DateFormatter()
dateFormater.timeStyle = .short
dateFormater.timeZone = TimeZone(identifier: identifier)
dateFormater.dateFormat = "HH:mm"
return dateFormater.string(from: .now)
}
}
世界城市清單
利用 TimeZone.knownTimeZoneIdentifiers 獲取系統所知道的所有時區,再存入cities 的時區資料裡面
let timeZoneIdentifiers = TimeZone.knownTimeZoneIdentifiersvar cities = [City]()override func viewDidLoad() {
super.viewDidLoad()
for timeZoneIdentifier in timeZoneIdentifiers {
let city = City(identifier: timeZoneIdentifier)
cities.append(city)
}
利用 Dictionary 建立城市索引,再以索引key建立索引陣列。
var cityDictionary = [String: [City]]()var citySectionTitles = [String]()override func viewDidLoad() {
super.viewDidLoad()
for city in cities {
let cityKey = String(city.cityString.prefix(1))
if var cityValues = cityDictionary[cityKey] {
cityValues.append(city)
cityDictionary[cityKey] = cityValues
} else {
cityDictionary[cityKey] = [city]
}
}
citySectionTitles = [String](cityDictionary.keys)
citySectionTitles = citySectionTitles.sorted(by: < )
世界城市清單,包含section header與section側邊索引
extension CityViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
if searching {
return 1
} else {
return citySectionTitles.count
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return searchedCities.count
} else {
let cityKey = citySectionTitles[section]
if let cityValues = cityDictionary[cityKey] {
return cityValues.count
}
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(CityTableViewCell.self)", for: indexPath) as! CityTableViewCell
var city: City?
if searching {
city = searchedCities[indexPath.row]
} else {
let cityKey = citySectionTitles[indexPath.section]
if let cityValue = cityDictionary[cityKey] {
city = cityValue[indexPath.row]
}
}
cell.cityLabel.text = city?.cityName
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return searching ? nil : citySectionTitles[section]
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return searching ? nil : citySectionTitles
}
}
使用者自選的世界時鐘清單
設定資料的顯示。由於編輯模式時會縮窄畫面空間,當編輯時時間需要隱藏。
extension WorldClockViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
cities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(WorldClockTableViewCell.self)", for: indexPath) as! WorldClockTableViewCell
let city = cities[indexPath.row]
cell.cityLabel.text = city.cityName
cell.relativeDateLabel.text = city.relativeDate
cell.relativeHoursLabel.text = city.relativeHours
if tableView.isEditing {
cell.timeLabel.isHidden = true
} else {
cell.timeLabel.text = city.localTime
cell.timeLabel.isHidden = false
}
return cell
}
設定清單可編輯與移動的功能與資料處理
//Editable
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
cities.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
case .insert: return
case .none:
tableView.allowsSelectionDuringEditing = true
default: return
}
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
UITableViewCell.EditingStyle.delete
}//Movable
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let city = cities[sourceIndexPath.row]
cities.remove(at: sourceIndexPath.row)
cities.insert(city, at: destinationIndexPath.row)
}
}
使用者參數儲存
將使用者新增的程式儲存在UserDefaults。在原struct City 定義讀取與儲存參數。
struct City: Codable { static func loadCities() -> [City]? {
let userDefault = UserDefaults.standard
guard let data = userDefault.data(forKey: "cities") else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode([City].self, from: data)
}
static func saveCities(_ cities: [City]) {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(cities) else { return }
UserDefaults.standard.set(data, forKey: "cities")
}
viewDidLoad後讀取資料
override func viewDidLoad() {
super.viewDidLoad()
if let cities = City.loadCities() {
self .cities = cities
}
透過property observer 定義當資料有變動便儲存
var cities = [City]() {
didSet {
City.saveCities(cities)
}
}