[iOS] Weather App-Part2.串接OpenWeatherMap

CoreLocation/CLLocationManagerDelegate/UISearchBarDelegate/searchBarSearchButtonClicked(_:)/JSONDecoder()/reloadData()/Type Casting/deselectRow(at:animated:)

功能

  1. 請求定位權限取得所在地點,顯示當地天氣與時間
  2. 搜尋世界各城市的天氣,顯示當地天氣與時間
  3. 顯示未來五天每間隔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

--

--