Skip to main content

用Swift实现基本网络通讯 – PART 2

BASIC NETWORKING IN SWIFT – PART 2
上一讲中我们已经实现了基本的API请求,并返回了正确数据。这次我们需要吧数据做一些筛选和处理,如时间的格式化和提取正确的字段内容显示在UI上。同时我们也会在之前的基础上对请求数据的方法做进一步的优化。

首先我们需要吧WeatherEngine定义为一个类,以便在UI界面中调用它。定义类方式:

Swift
//  WeatherEngine.swift
//  SwiftNetworking

import Foundation
class WeatherEngine {
    //Functions here
    //......
}

同时我们需要对fetchWeatherData()这个方法进行一些优化,大体结构是不改变的,但我们添加了closure的方式。这有点类似Objective-C中的Block。但在Swift中,Closure更为灵活和强大。关于Closure的详细介绍我们放在未来的教学中慢慢讲解。

同时我们还需要建立一个新的Swift类用以储存和传递返回的天气数据。以我个人的习惯我还是偏好建立一个单独的文件,这样便于未来代码的管理和维护。新建一个Swift文件WeatherData.swift,在其中定义WeatherData类,这里作为教学,我们就暂时只处理返回JSON中两个字段,分别是:weather数组中第一个对象中的main(天气描述,如多云,晴朗等),和main中的feels_like(体感温度)。

JSON
//...........
"weather": [
  {
    "id": 804,
    "main": "Clouds",

    "description": "overcast clouds",
    "icon": "04n"
  }
],
//...........
"main": {
  "temp": 293.48,
  "feels_like": 293.81,

  "temp_min": 292.32,
  "temp_max": 294.04,
  "pressure": 1007,
  "humidity": 86
},
//...........
WeatherData.swift
Swift

//
//  WeatherData.swift
//  SwiftNetworking
import Foundation

class WeatherData {
    //定义变量
    var weatherMain: String
    var feelsLike: Double
    //初始化Weather对象
    init(weatherMain: String, feelsLike: Double) {
        self.weatherMain = weatherMain
        self.feelsLike = feelsLike
    }
}

下面我重写fetchWeatherData()方法。为了让代码看起来整洁一下,我们把API地址和参数定义成变量,这样也是为了将来通过GPS自动获取地址,通过用户参数显示不同的天气数据做准备。同时再加一个参数,让请求回的单位为公制。API参数为:units=metric

Swift
//  WeatherEngine.swift
//  SwiftNetworking
import Foundation
//Start of tutorial Part - 2
var API_LAT = "35.18147"//未来将通过CoreLocation获取
var API_LON = "136.90641"//未来将通过CoreLocation获取
var API_UNITS = "metric" //公制单位
var API_URL = "https://api.openweathermap.org/data/2.5/weather?lat=\(API_LAT)&lon=\(API_LON)&units=\(API_UNITS)&appid=\(API_KEY)" //合成请求URL

class WeatherEngine {
    //添加completion使方法返回WeatherData对象,或作异常处理
    func fetchWeathData(completion: @escaping (Result<WeatherData, Error>) -> Void) {
        //定义url并处理异常
        guard let url = URL(string: API_URL) else {
            completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
            return
        }
        //定义datatask
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            //如果出现异常直接返回
            if let error = error {
                completion(.failure(error))
                return
            }
            //定义http响应对象和异常处理
            guard let httpResponse = response as? HTTPURLResponse else {
                completion(.failure(NSError(domain: "Invalid response", code: 0)))
                return
            }
            //如果返回状态码200(OK)则开始处理返回数据
            if httpResponse.statusCode == 200 {
                guard let data = data else {
                    completion(.failure(NSError(domain: "No data", code: 0)))
                    return
                }
                do {
                    //创建WeatherData对象实例weather,并将解出的字段赋予给weather
                    if let weatherData = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                        if let weatherArray = weatherData["weather"] as? [[String: Any]],
                           let mainData = weatherData["main"] as? [String: Any],
                           let weatherMain = weatherArray.first?["main"] as? String,
                           let feelsLike = mainData["feels_like"] as? Double {
                            let weather = WeatherData(weatherMain: weatherMain, feelsLike: feelsLike)
                            completion(.success(weather))
                        } else {
                            completion(.failure(NSError(domain: "Invalid weather data", code: 0)))
                        }
                    } else {
                        completion(.failure(NSError(domain: "Invalid weather data", code: 0)))
                    }
                } catch {
                    completion(.failure(error))
                }
            } else {
                completion(.failure(NSError(domain: "API request with code\(httpResponse.statusCode)", code: httpResponse.statusCode)))
            }
        }
        //执行datatask
        task.resume()
    }
    //测试Log出API_URL地址,本例中没有调用。
    func apiUrlCheck(){
        print(API_URL)
    }
}
//End of tutorial Part - 2

其中这里需要注意一下:

Swift
//取出weather数组
let weatherArray = weatherData["weather"] as? [[String: Any]]
//取出weatherArray中第一个字典对象,并取出“main”字段内容
let weatherMain = weatherArray.first?["main"] as? String

从返回的JSON中我们可以看到weather这个key里面,存储的是一个数据,这里比较容易粗心忽略。在这个例子中我们请求的是当前天气,所以数组中只有一个字点对象,如果我们请求未来5天天气等情况下,数组里面将会有多个对象。

其他取值和赋值的方式跟Objective-C没有太大的差异,熟悉一下Swift的语法即可适应。

到现在我们已经完整重写了fetchWeatherData()方法。接下来我们到SwiftUI中重构用户界面,让屏幕上显示当前气象类型和体感温度。

ContentView.swift:

Swift
//  ContentView.swift
//  SwiftNetworking
import SwiftUI

struct ContentView: View {
    //建立WeatherEngine实例
    let weatherEngine = WeatherEngine()
    //建立两个@State字符串变量用以储存“天气类型”和“体感温度”
    @State var weatherMain = "Weather"
    @State var feelsLike = "Temp"
    
    var body: some View {
        VStack {
            //显示体感温度
            Text(String(feelsLike))
                .font(.system(size: 100, weight: .light, design: .serif))
                .multilineTextAlignment(.center)
            //显示天气类型
            Text(weatherMain)
                .font(.system(size: 24))
            //测试打印API地址
            Button(action: {
                weatherEngine.apiUrlCheck()
            }) {
                Text("Fetch Weather Data")
                    .padding()
                    .background(Color.gray)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        //是显示时调用fetchWeathData()方法
        .onAppear(){
            weatherEngine.fetchWeathData { result in
                switch result {
                case.success(let weatherData):
                    print(weatherData.weatherMain)
                    print(weatherData.feelsLike)
                    self.weatherMain = weatherData.weatherMain
                    self.feelsLike = String(Int(weatherData.feelsLike)) + "°C"
                case.failure(let error):
                    print("Error fetching weather data: \(error.localizedDescription)")
                }
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

唯一需要说明的是,这里定义变量时用到了@State var weatherMain = “Weather”

在 SwiftUI 中,视图是由数据(状态)驱动的。每当视图在创建或解析时,都会为该视图和该视图中使用的状态数据之间创建一个依赖关系,每当状态的信息发生变化,有依赖关系的视图会马上翻译出这些变化并重绘。

好了,本章结束。下一章,我们将会获取更多数据并开始SwiftUI构造界面的讲解。

Happy coding!

API, Apple, iOS, Programming, Swift, Tutorial, Xcode