[iOS] Weather App-Part2.串接OpenWeatherMap
CoreLocation/CLLocationManagerDelegate/UISearchBarDelegate/searchBarSearchButtonClicked(_:)/JSONDecoder()/reloadData()/Type Casting/deselectRow(at:animated:)
功能
- 請求定位權限取得所在地點,顯示當地天氣與時間
- 搜尋世界各城市的天氣,顯示當地天氣與時間
- 顯示未來五天每間隔3小時的天氣預報
架構
Model
API、CurrentWeatherData、ForecastWeatherData
View
Main.storyboard、LaunchScreen.storyboard
Controller
CurrentViewController、ForecastTableViewController、ForecastTableViewCell
Model
因為我總共使用兩組API,為了方便取用url,我直接將API相關資訊獨立成一個Data
import Foundationclass API { static let apiKey = "Your API Key" static let weather = "weather" static let forecast = "forecast" static let imperial = "imperial"}
CurrentViewController的urlstr
let urlstr = "https://api.openweathermap.org/data/2.5/\(API.weather)?lat=\(lat)&lon=\(lon)&appid=\(API.apiKey)&units=\(API.imperial)"
ForecastViewController的urlstr
let urlstr = "https://api.openweathermap.org/data/2.5/\(API.forecast)?q=\(city)&appid=\(API.apiKey)&units=\(API.imperial)&lang=zh_tw"
至於CurrentWeatherData、ForecastWeatherData的解析JASON邏輯可參考上一篇
CurrentViewController
請求定位權限
CoreLocation
CLLocationManagerDelegate
為了取得使用者當前位置,要先套入 CoreLocation,並且讓CurrentViewController遵從CLLocationManagerDelegate
import UIKitimport CoreLocationclass CurrentViewController: UIViewController, CLLocationManagerDelegate {}
CLLocationManager()
接著創建locationManager
let locationManager = CLLocationManager()
創建setupLocation()
,讓CurrentViewController
成為locationManager
的代理人,並透過requestWhenInUseAuthorization()
請求定位權限,接著開始更新地點
func setupLoaction() { locationManager.delegate = self locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation()}
為了讓App初使用的時候跳出請求定位權限的視窗,必須到info新增彈出視窗敘述
運行之後便會出現彈出視窗
若運行失敗出現以下錯誤,可參考連結說明
This app has attempted to access privacy-sensitive data without a usage description. The app’s Info.plist must contain both “NSLocationAlwaysAndWhenInUseUsageDescription” and “NSLocationWhenInUseUsageDescription” keys with string values explaining to the user how the app uses this data
若一直無法更新定位,極有可能是模擬器的location設定None的關係,只要調整成Apple地點測試一下即可
CLLocation
定義全域變數獲得目前的定位資訊
var currentLocation: CLLocation?
根據Apple文件CLLocation
物件包含裝置的地理位置(經緯度)、海拔高度的資訊。
A CLLocation
object contains the geographical location and altitude of a device, along with values indicating the accuracy of those measurements and when they were collected.
locationManager(_ :didUpdateLocations:)
接著告訴代理人已經可以開始更新地點了
如果locations
有資料但是currentLocation
還是空的狀態,將locations
陣列中的第一筆資料派給currentLocation
,接著停止更新地點,最後透過自定義的程式fetchDataFromCoordinate()
抓取API資料
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if !locations.isEmpty, currentLocation == nil { currentLocation = locations.first locationManager.stopUpdatingLocation() fetchDataFromCoordinate() }}
使用SearchBar搜尋
UISearchBarDelegate
首先讓CurrentViewController
遵從UISearchBarDelegate
class CurrentViewController: UIViewController, CLLocationManagerDelegate, UISearchBarDelegate {
接著讓CurrentViewController
成為searchBar
的代理人
override func viewDidLoad() { super.viewDidLoad() searchBar.delegate = self searchBar.isHidden = true searchBar.placeholder = "Please enter a city name."}
searchBarSearchButtonClicked(_:)
當searchBar按下搜尋之後,resignFirstResponder()
可以收鍵盤,而當locationString
拿到使用者輸入的地點之後,便可開始用fetchDataFromCity
抓取新地點的天氣資料。
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { //收鍵盤 searchBar.resignFirstResponder() if let locataionString = searchBar.text, !locataionString.isEmpty { //用搜尋地點抓取資料更新 fetchDataFromCity(city: locataionString) searchBar.isHidden = true }}
抓取資料
fetchDataFromCoordinate()
、fetchDataFromCity(city: String)
程式設定如下
首先設定全域變數weatherInfo
繼承CurrentWeatherData中的struct CurrentWeather
JSONDecoder()
用URLSession執行抓取url資料的任務,並選擇用JSONDecoder()
解析資料
關於解析時間可參考這篇
因為可能抓不到資料,所以用if let 做保險,讓decoder
去解析CurrentWeather
,取得到的Response指派給weatherInfo
,接著開始讓背景抓取到的資料,透過DispatchQueue.main.async
顯示在畫面(Main Queue)
白天黑夜的背景設定是運用icon的最後一個英文字,n是夜晚,d是白天,使用.suffix(1)
即可抓到最後一個字
設定三組格式轉換,因為風速我想顯示的是km/h單位,於是url選擇用imperial單位,但這個單位設定是華氏,所以額外編寫華氏轉攝氏的程式
ForecastTableViewController
抓取資料
另外解析未來五天的天氣預報API,自訂型別struct之後,設定全域變數繼承,每一行row將會對應List裡面的資料。
reloadData()
這邊比較不一樣的地方是第17行的.reloadData()
,根據Apple文件此方法可以重新載入表格中的所有資訊。
因為更新畫面要在main queue,所以要寫在DispatchQueue.main.async
裏面。
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on.
Section&Row
section設定1(也可不設定,預設是1),row設定forecastRow的數量。
先到storyboard設定cell的id等同ForecastTableViewCell的名稱,之後在第12行的withIdentifier
就可以直接用\()選取,避免打錯字
Type Casting轉型
從第11行可得知此段程式最終會回傳UITableViewCell
,但是我創建的cell是ForecastTableViewCell
,於是使用as向下轉換(downcast)。
至於為何是as?,因為有可能轉型失敗。
而使用guard let寫法也可讓程式更易理解。
deselectRow(at:animated:)
第14行用此方法並且讓animated為false
,可以立刻執行取消的動畫。
ForecastTableViewCell
這裡我只有拉好storyboard上的outlet,其餘無動作。
利用Prepare傳資料
為了讓ForecastTableViewController可以顯示未來五天的天氣預報,必須將CurrentViewController的location傳送過去。
首先到ForecastTableViewController
創建全域變數cityName
var cityName: String?
到storyboard設定segue的id為showForecast
再來回到CurrentViewController寫入prepare方法,將locationLabel的text
派給ForecastTableViewController的變數cityName
。
這次練習參考了很多高手的文章跟影片,終於完成心中想要的功能!🌈