swiftでiPhone標準の時計アプリを作ろう 完成コード 世界時計

2019年9月17日火曜日

swift 時計 世界時計

t f B! P L

Xcode 10.3
ios 12.4
swift5

完成品


UI

 

コード

WorldClockVC

import UIKit

class WorldClockVC: UIViewController ,UITableViewDelegate,UITableViewDataSource{
    
    var selectTimezones = [String]()
    var userDefaults = UserDefaults.standard
    var timer = Timer()

    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        loadTimeZones()
        tableView.register(UINib(nibName: "WorldClockCell", bundle: nil), forCellReuseIdentifier: "WorldClockCell")
        self.navigationItem.setLeftBarButton(self.editButtonItem, animated: true)
    }
        
    
    func loadTimeZones(){
        if let getTimeZones = userDefaults.object(forKey: "TimeZones") as? [String]{
            selectTimezones = getTimeZones
        }
    }
    
    func saveTimeZones(){
        userDefaults.set(selectTimezones, forKey: "TimeZones")
        userDefaults.synchronize()
    }
    
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tableView.setEditing(editing, animated: animated)
    }
    
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let sourceCellItem = selectTimezones[sourceIndexPath.row]
        guard let indexPath = selectTimezones.firstIndex(of: sourceCellItem) else { return }
        selectTimezones.remove(at: indexPath)
        selectTimezones.insert(sourceCellItem, at: destinationIndexPath.row)
        saveTimeZones()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return selectTimezones.count
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "WorldClockCell") as! WorldClockCell
        cell.setCity(name: selectTimezones[indexPath.row]) 
        return cell
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            selectTimezones.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)
            saveTimeZones()
        }
    }
    
    func timeZone(city:String) -> String{
        let formatter = DateFormatter()
        
        formatter.dateFormat = "HH:mm"
        let timeZoneIdentifiers = TimeZone.knownTimeZoneIdentifiers
        for identifier in timeZoneIdentifiers {
            if identifier.contains(city){
                formatter.timeZone = TimeZone(identifier: identifier)
            }
            }
        return formatter.string(from: Date())
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let nvc = segue.destination as? UINavigationController else {return}
        guard let vc = nvc.topViewController as? WorldClockSelectVC else {return}
        vc.delegate = self
    }
}

extension WorldClockVC:WorldClockSelectVCDelegte{
    func worldClockSelect(cancel: WorldClockSelectVC) {
        self.setEditing(false, animated: false)
    }
    
    func worldClockSelect(selectedWorldClock: WorldClockSelectVC, selected: String) {
        if !selectTimezones.contains(selected){
            selectTimezones.append(selected)
        }
        tableView.reloadData()
        saveTimeZones()
    }
}

WorldClockCell

import UIKit

class WorldClockCell: UITableViewCell {

    @IBOutlet weak var timeLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var hrsLabel: UILabel!
    var timer = Timer()
    var cityName:String!

    override func awakeFromNib() {
        super.awakeFromNib()
        self.accessoryView = timeLabel
    }
    
    func setCity(name:String){
        cityLabel.text = name
       timeLabel.text = timeZone(city: name)
        hrsLabel.text = "Today,+0HRS"
        cityName = name
        timer = Timer.scheduledTimer(
            
            timeInterval: 1.0,                              // 時間間隔
            
            target: self,                       // タイマーの実際の処理の場所
            
            selector: #selector(WorldClockCell.tickTimer(_:)),   // メソッド タイマーの実際の処理
            
            userInfo: nil,
            
            repeats: true)
        RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
    }
    
    @objc func tickTimer(_ timer: Timer) {
        timeLabel.text = timeZone(city: cityName)
    }
    
    func timeZone(city:String) -> String{
        let formatter = DateFormatter()
        
        formatter.dateFormat = "HH:mm"
        formatter.timeStyle = .short
        formatter.locale = Locale(identifier: "ja_JP")
        let timeZoneIdentifiers = TimeZone.knownTimeZoneIdentifiers
        for identifier in timeZoneIdentifiers {
            if identifier.contains(city){
                formatter.timeZone = TimeZone(identifier: identifier)
            }
        }
        let timeDiff = formatter.timeZone.secondsFromGMT()
        hrsLabel.text = timeDiff.timeString()
        return formatter.string(from: Date())
    }
}

extension Int {
    func timeString() -> String {
        let adjustSecondsForJapan : TimeInterval = 9 * 60 * 60
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day,.hour, .minute]
        formatter.unitsStyle = .positional
        let formattedString = formatter.string(from: TimeInterval(self) - adjustSecondsForJapan) ?? "0"
        let df = DateFormatter()
        df.dateStyle = .short
        df.doesRelativeDateFormatting = true
        
        let day = df.string(from: Date(timeIntervalSinceNow: TimeInterval(self) - adjustSecondsForJapan))
        if formattedString == "0" {
            return day + ", +0HRS"
        } else {
            if formattedString.hasPrefix("-") {
                return day + ", \(formattedString)HRS"
            } else {
                return day + "+\(formattedString)HRS"
            }
        }
    }  
}

WorldClockSelectVC

import UIKit

protocol WorldClockSelectVCDelegte {
    func worldClockSelect(selectedWorldClock:WorldClockSelectVC,selected:String)
    func worldClockSelect(cancel:WorldClockSelectVC)
}

class WorldClockSelectVC: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var nav: UINavigationBar!
    var delegate:WorldClockSelectVCDelegte!
    
    private var searchController: UISearchController!
    
    var filteredTitles = [[String]]()
    var timeZoneIdentifiers = [String]()
    var allCities: [String] = []
    var sortedFirstLetters: [String] = []
    var sections: [[String]] = [[]]
     var searchString:String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        searchController = UISearchController(searchResultsController: nil)
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.dimsBackgroundDuringPresentation = false
        searchController.searchBar.sizeToFit()
        searchController.searchBar.showsCancelButton = true
        searchController.searchBar.delegate = self
        // UISearchControllerをUINavigationItemのsearchControllerプロパティにセットする。
        navigationItem.searchController = searchController
        
        // trueだとスクロールした時にSearchBarを隠す(デフォルトはtrue)
        // falseだとスクロール位置に関係なく常にSearchBarが表示される
        navigationItem.hidesSearchBarWhenScrolling = false
        
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        timeZoneIdentifiers = TimeZone.knownTimeZoneIdentifiers.sorted()
                for identifier in timeZoneIdentifiers {
                    if let cityName = identifier.split(separator: "/").last {
                        allCities.append("\(cityName)")
                    }
                }
        let firstLetters = allCities.map { $0[$0.startIndex].uppercased() }
        let uniqueFirstLetters = Array(Set(firstLetters))
        sortedFirstLetters = uniqueFirstLetters.sorted()
        
        sections = sortedFirstLetters.map({firstLetter in return allCities.filter({ $0[$0.startIndex].uppercased() == firstLetter }).sorted(by: {$0 < $1})
        })
        filteredTitles = sections
    }
    
    @objc private func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
        }
    }
    
    @objc private func keyboardWillHide(notification: NSNotification) {
        tableView.contentInset = .zero
    }
}

// MARK: - UISearchResultsUpdating

extension WorldClockSelectVC: UISearchResultsUpdating ,UISearchBarDelegate{
    
    func updateSearchResults(for searchController: UISearchController) {
        
        // SearchBarに入力したテキストを使って表示データをフィルタリングする。
        searchString = searchController.searchBar.text ?? ""
        if searchString.isEmpty {
            filteredTitles = sections
        } else {
            filteredTitles = sections.map({
                filter in return filter.filter({$0.contains(searchString)})
                }
            )
        }
        tableView.reloadData()
    }
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        delegate.worldClockSelect(cancel: self)
        dismiss(animated: true, completion: nil)
    }
}

// MARK: - UITableViewDataSource UITableViewDelegate
extension WorldClockSelectVC: UITableViewDataSource,UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return filteredTitles[section].count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text =  filteredTitles[indexPath.section][indexPath.row]
        return cell
    }
    
    public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return searchString != "" ? nil : sortedFirstLetters[section]
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return filteredTitles.count
    }
    
    public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return  searchString != "" ? nil : sortedFirstLetters
    }
    public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        return UILocalizedIndexedCollation.current().section(forSectionIndexTitle: index)
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        searchController.searchBar.resignFirstResponder()
        searchController.dismiss(animated: false)
        delegate.worldClockSelect(selectedWorldClock: self, selected: filteredTitles[indexPath.section][indexPath.row])
        dismiss(animated: true, completion: nil)
    }
}
githubはこちら

参考

UISearchControllerの使い方。主に結果画面からNavigationControllerでPush
swift – カスタムモデルクラスを使用して、tableView内のデータをセクションごとにアルファベット順にソートする方法
Replacement of date object with “today” and “yesterday” strings in iphone

QooQ