模仿 iOS WorldClock
資料儲存練習,用 delegate 傳資料
這次作業模仿 iPhone內建的 worldClock 來練習 table view controller 的新增,刪除,和儲存資料
1. 取得世界城市 timezone 的清單
let knownTimeZoneID = TimeZone.knownTimeZoneIdentifiers.filter { ID in
ID.contains("/")
}
2. 建立列表上所要顯示的資料
- identify : 從 TimeZone.knownTimeZoneIdentifiers 裡得到的 ID
- cityName : 取 id / 後面的城市名
- cityPrefix : 取城市的第一個字母,在 searchTableViewController 裡當 section 的名字用
- relativeDate : 相對local 的日期,顯示 昨天、今天、明天
- relativeHour:相對 local 的小時差 Ex: +- HRs
- localTime : 當地時間
struct cityInfo: Codable{
var identify: String
var cityName: String {
let fullName = identify.replacingOccurrences(of: "_", with: " ").components(separatedBy: "/")
if fullName.count > 2{
return fullName[1] + ", " + fullName[2]
}
return fullName[1]
}
var cityPrefix : String{
return String(cityName.prefix(1))
}
var relativeDate : String{
let dateformatter = DateFormatter()
dateformatter.timeZone = TimeZone(identifier: identify)
dateformatter.dateStyle = .medium
dateformatter.timeStyle = .none
dateformatter.doesRelativeDateFormatting = true
return dateformatter.string(from: .now) + ","
}
var relativeHour : String {
let hrInterval = (TimeZone(identifier: identify)!.secondsFromGMT() - TimeZone.current.secondsFromGMT())/3600
if hrInterval == 1 || hrInterval == -1 {
return hrInterval.description + "HR"
}
return hrInterval.description + "HRS"
}
var localTime : String{
let dateformatter = DateFormatter()
dateformatter.timeZone = TimeZone(identifier: identify)
dateformatter.dateFormat = "HH:mm"
return dateformatter.string(from: .now)
}
}
3. 建立搜索的頁面
- 幫城市建立一個 dictionary, key 是 cityPrefix (也是 section header), value 是 以那個字母為開頭的 array of cityInfo
var delegate : SearchTableViewControllerDelegate?
var cityDictionary = [String : [cityInfo]]()
var cityDictinaryKey = [String]()
var searching = false
lazy var filterCityList = knownCityList
var knownCityList = [cityInfo]()
var city : cityInfo?
func sectionHeaderSetting(){
for cityID in knownTimeZoneID{
let city = cityInfo(identify: cityID)
knownCityList.append(city)
if cityDictionary.keys.contains(city.cityPrefix){
cityDictionary[city.cityPrefix]!.append(city)
}else{
cityDictionary[city.cityPrefix] = [city]
}
}
cityDictinaryKey = Array(cityDictionary.keys).sorted(by: <)
}
- 設定 number of sections and number of rows in section 和要顯示的內容
在搜尋的時候沒有按字母分 section,所以設一個 var boolean variable 來看當時有沒有在搜尋,並用 if statement 把他分成兩個狀況顯示
override func numberOfSections(in tableView: UITableView) -> Int {
if searching {
return 1
}
return cityDictionary.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return filterCityList.count
}
return cityDictionary[cityDictinaryKey[section]]!.count
}override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(SearchTableViewCell.self)", for: indexPath) as? SearchTableViewCell else {return UITableViewCell()}
if searching {
let cityId = filterCityList[indexPath.row]
cell.SearchCityLabel.text = cityId.cityName
}else{
let cityId = cityDictionary[cityDictinaryKey[indexPath.section]]![indexPath.row]
cell.SearchCityLabel.text = cityId.cityName }
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if searching {
return nil
}
return cityDictinaryKey[section]
}
- 參考下面那篇文章,在navigationBar 上設立 searchBar
func searchBarSetting(){
let searchController = UISearchController()
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
searchController.searchBar.barStyle = .black
searchController.searchBar.searchTextField.textColor = .white
searchController.automaticallyShowsCancelButton = true
}
- 實現 search 的時候 下面的資料也會跟著 搜索關鍵字 update
會用到之前的 lazy var filterCityList = knownCityList
記得最後要讓 tableView reload data ,畫面才會 update
extension SearchTableViewController : UISearchResultsUpdating{
func updateSearchResults(for searchController: UISearchController) {
if let searchText = searchController.searchBar.text,
searchText.isEmpty == false{
searching = true
filterCityList = knownCityList.filter({ city in
city.cityName.localizedStandardContains(searchText)
})
}else{
searching = false
filterCityList = knownCityList
}
tableView.reloadData()
}
}
- 把選到的資料傳回上一頁 (unwind segue)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if searching{
guard let row = tableView.indexPathForSelectedRow?.row else {return }
city = filterCityList[row]
}else {
guard let section = tableView.indexPathForSelectedRow?.section else {return }
guard let row = tableView.indexPathForSelectedRow?.row else {return }
city = cityDictionary[cityDictinaryKey[section]]![row]
}
}
3. 建立世界時鐘的清單頁面
- 儲存使用者選擇的城市資料
struct 要是 codable才能讀取跟存資料 ,並把資料存在 document 資料夾裡
struct cityInfo: Codable{
// save and load data
static let documentDictionary = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static func loadData() -> [cityInfo]?{
let url = documentDictionary.appendingPathComponent("cityList")
guard let data = try? Data(contentsOf: url) else {return nil}
let decoder = JSONDecoder()
return try? decoder.decode([cityInfo].self, from: data)
}
static func saveInfo(info :[cityInfo]){
let encoder = JSONEncoder()
guard let data = try? encoder.encode(info) else {return }
let url = documentDictionary.appendingPathComponent("cityList")
try? data.write(to: url)
}
}
在 worldClockTableViewController 裡在叫出function 讀取還有存資料
didSet 會在新的值存進去的時候被叫出來
記得讓 timer 每一秒重新 reload data ,時間才會跟著動
var cityList = [cityInfo](){
didSet{
cityInfo.saveInfo(info: cityList)
}
}override func viewDidLoad() {
super.viewDidLoad()
if let cityList = cityInfo.loadData(){
self.cityList = cityList
}
tableView.rowHeight = 90
tableView.separatorStyle = .singleLine
tableView.separatorColor = .gray
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.tableView.isEditing ? nil : self.tableView.reloadData()
}
timer.fire()
}
- 讓使用者可以刪除或移動想要的順序 並設定 Edit button
super.setEditing( ) 可以讓我們edit table view
isEditing 可以告訴我們當下有沒有正在 edit (boolean value)
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
cityList.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}// make the cell become movable
// Override to support conditional rearranging of the table view.
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
// Override to support rearranging the table view.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
let removeItem = cityList[fromIndexPath.row]
cityList.remove(at: fromIndexPath.row)
cityList.insert(removeItem, at: to.row)}@IBAction func clickEditButton(_ sender: UIBarButtonItem) {
// set whether the view controller show the editable view
super.setEditing(!tableView.isEditing , animated: true)
sender.title = isEditing ? "Done": "Edit"
tableView.allowsSelectionDuringEditing = true
tableView.reloadData()
}
4. 用 delegate 把資料傳回前一頁
本來這裡我是用 unwind segue傳資料的,但發現用 search bar 搜尋城市後,要點兩次才能傳回前一頁,卡了很久後,再經過 Peter 的神救援,改用 tableView(_:didSelectRowAt:) 和 delegate傳資料
- 建立 protocol並呼叫裡面的 function
In SearchViewTableViewController,
protocol SearchTableViewControllerDelegate {
func searchTableViewController( _ controller : SearchTableViewController, addCity city:cityInfo)
}class SearchTableViewController: UITableViewController {
var delegate : SearchTableViewControllerDelegate?
var city : cityInfo?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if searching{
city = filterCityList[indexPath.row]
}else {city = cityDictionary[cityDictinaryKey[indexPath.section]]![indexPath.row]
}
delegate?.searchTableViewController(self, addCity: city!)
navigationController?.popViewController(animated: true)
}}
- 連一個 IBSegueAction, 在裡面設定 delegate,記得也要繼承 SearchTableViewControllerDelegate
In WorldClockTableViewController,
@IBSegueAction func addCity(_ coder: NSCoder) -> SearchTableViewController? {
let searchTableViewController = SearchTableViewController(coder: coder)
searchTableViewController?.delegate = self
return searchTableViewController
}
繼承 SearchTableViewControllerDelegate ,寫一個一樣的function 名,並在 function 裡面放想做的事
extension WorldClockTableViewController : SearchTableViewControllerDelegate{
func searchTableViewController( _ controller : SearchTableViewController, addCity city:cityInfo){
if !(cityList.contains(where: { cityInfo in
cityInfo.cityName == city.cityName
})){
cityList.append(city)
}
tableView.reloadData()
}
}
最後,特別感謝 Peter 的神救援,還有下面這位前輩的作業參考,才能成功將這個作業做出來!!!
Link:
GitHub: