#27 模仿 iOS Clock App - 1 :World Clock

原本只打算利用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.knownTimeZoneIdentifiers
var 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)
}
}

成果展示

--

--