项目档案

SwiftUI天气示例

我们的下一个例子是一个可以显示天气预报的赢博体育程序。预报数据将从网上下载。

赢博体育程序的第一个屏幕显示当前天气预报。

Location和LocationInfo类

天气赢博体育允许用户指定他们的位置信息。该位置信息反过来用于从openweathermap.org web服务获取天气预报。

为了记录用户位置的关键信息,我们使用location类:

import Foundation类Location: Codable {var zip:String var latitude:Double var longitude:Double var name:String init() {zip = "54911" latitude = 44.2619 longitude = -88.4154 name = " applpleton "} init(zip z:String,latitude lat:Double,longitude lo:Double,name n:String) {zip = z latitude = lat longitude = lo name = n}}

还有一个LocationInfo类。这个类有两个主要职责:将位置信息备份到本地文件,以及处理与处理位置信息的web服务之间的任何通信。

import Foundation import SwiftUI struct GeoLocation: Codable {let zip: String let name: String let lat: Double let lat: Double let country: String} class LocationInfo: ObservableObject {@Published var location: location let itemArchiveURL: URL = {let documentsDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentDirectory = documentsDirectories.first!return documentDirectory.appendingPathComponent("location.plist")}() init() {do{让data = try data (contentsOf: itemArchiveURL)让unarchiver = PropertyListDecoder()让loc = try unarchiver.decode(location. self, from: data) location = loc} catch {location = location ()} func saveChanges(){让encoder = PropertyListEncoder()让data = try!encoder.encode(位置)试试!数据。write(to: itemArchiveURL)} func searchLocation(code: String!) async{如果让zip = code{让key = Settings。key let urlStr = "https://api.openweathermap.org/geo/1.0/zip?zip=\(zip),US&appid=\(key)" guard let url = url (string: urlStr) else {return} do {let (data, response) = try await URLSession.shared。数据(来自:url)守卫(响应为!)HTTPURLResponse)。statusCode == 200 else {return} let result = try JSONDecoder().decode(GeoLocation.self, from: data) DispatchQueue.main.async {self。location = location (zip:zip,纬度:result.lat,经度:result.lon,名称:result.name) self.saveChanges()}} catch {}}}

这个类本质上是赢博体育位置信息的赢博体育者。这个类负责将Location数据保存到本地文件,然后在赢博体育程序再次启动时从文件中加载该信息。

searchLocation方法处理按服务器上的邮政编码查找位置。该方法以邮政编码的文本形式作为输入,然后在openweathermap.org服务器上运行GET请求以查找该邮政编码的位置信息。我将在异步代码一节中更详细地讨论该方法中的代码。

由于LocationInfo是一个发布位置属性的可观察对象,所以当位置更新时,赢博体育将能够根据需要重新绘制视图。

使用async和await来简化异步任务

上面代码的一个重要特性是与openweathermap.org服务器通信的代码。Swift将迫使我们以一种特殊的方式处理任何此类通信。上面向服务器发送请求的关键代码是这样的:

let (data, response) = try await URLSession.shared。数据(来自:url)

这里最重要的事情是将await关键字与URLSession.shared.data()方法结合使用。在Swift中,我们使用await关键字来调用可能需要一段时间才能完成的代码。使用此关键字意味着我们愿意等待,直到我们即将启动的后台任务完成。只要在方法中使用await特性,我们就有义务将async关键字附加到该方法上。这向Swift发出信号,该方法包含执行异步操作的代码,并且任何调用该方法的代码都必须特别注意在后台线程中运行该方法。在后面的讲义中,您将看到正确调用searchLocation()方法的代码。

使用await关键字的一个优点是,它允许我们说,我们愿意坐下来等待,直到我们调用的方法完成并返回结果给我们。这允许我们接收从该方法返回的任何信息,然后在该方法调用之后运行处理我们接收到的任何结果的代码。总而言之,这是使用回调闭包来处理结果的更简洁的替代方案。

这里还需要注意searchLocation()方法最后一个微妙的方面。当我们从服务器获得Location信息时,我们会想要更新Location属性。由于这个属性已经被标记为@Published更新,它将对我们的赢博体育程序的用户界面产生影响。出于这个原因,我们需要将更新位置的代码放在闭包中,并安排该闭包在用户界面线程中运行。这样做的代码是

DispatchQueue.main.async {self。location = location (zip:zip,纬度:result.lat,经度:result.lon,名称:result.name) self.saveChanges()}

主视图

赢博体育程序的主视图显示当前位置的天气预报。下面是这个视图的完整代码:

struct ContentView:视图{@状态变量预测:预测?let formatter = DateFormatter() @EnvironmentObject private var lc: LocationInfo var body: some View {NavigationStack {List {Section(header: Text("\(lc.location.name)")) {if let fc = forecast {ForEach(fc.daily) {day in let interval = TimeInterval(day.dt+fc.timezone_offset) let date = date (timeIntervalSince1970:interval) ForecastView(fd:day,date:date)}} else {Text(“Loading…”)}}。任务{等待getForecast()}.navigationTitle(“天气”).navigationBarTitleDisplayMode(.inline) .navigationBarItems(拖尾:NavigationLink(“设置”){WeatherSettings()})}} func getForecast() async{让urlStr = "https://api.openweathermap.org/data/3.0/onecall?lat=\(lc.location.latitude)&lon=\(lc.location.longitude)&APPID=\(Settings.key)&exclude=current,minute,hour,alerts&units=imperial" guard让url = url(字符串:urlStr) else {return} do {let (data, _) = try await URLSession.shared。data(from: url) forecast = try JSONDecoder().decode(forecast .self, from: data)} catch {}}}

这个视图的属性之一是LocationInfo对象。这是我们赢博体育程序的数据模型类之一,因此我们需要确保每个视图都可以访问适当的LocationInfo对象,并且它们都使用相同的LocationInfo对象。在之前的例子中,我们通过在我们的第一个视图中创建模型对象,然后安排该视图将该对象传递给我们随后segue到的任何其他视图来处理类似的情况。

我在这里使用的@EnvironmentObject修饰符实现了处理模型对象的另一种方式。如果我们有一个计划跨多个视图使用的模型对象,我们可以将该模型类添加为属性,并在需要它的每个视图中使用@EnvironmentObject对其进行注释。接下来,我们安排创建模型对象并将其传递给赢博体育程序中的第一个视图。我们通过修改app类中设置赢博体育程序第一个视图的代码来实现这一点:

导入SwiftUI @main struct UIWeatherApp: App {var body: some Scene {WindowGroup {ContentView().environmentObject(LocationInfo())}}}

一旦我们安排赢博体育程序中的第一个视图接收这个环境对象,它就会自动将这个环境对象传递给它创建的任何新视图。这将使我们免于将对象从一个视图传递到另一个视图。

获取预测数据

计算ContentView类主体的代码设置了一个NavigationStack。在NavigationStack中显示的第一个视图是一个List,它显示当前的预报。由于List需要显示数据,因此我们需要设置一个任务来获取该数据。在SwiftUI中,我们通过在设置列表的代码之后链接一个任务请求来实现这一点:

.task{等待getForecast()}

因为task要求我们提供一个闭包,其中包含我们想要在任务中运行的代码。这段代码调用一个异步方法,该方法完成从服务器获取预报的工作。

自定义ForecastView类

我们希望我们的主视图在一个列表中显示几天的天气信息。我们通过设置一组结构来模拟服务器将发送给我们的天气数据来开始显示这些信息的过程:

struct TempRange:可解码{让天:双让分钟:双?马克斯:双份?让夜:双让夏娃:双让早晨:双重}struct天气:可解码的{让id: Int让主要:字符串来描述:字符串让图标:字符串}struct ForecastDay:可解码,可识别的{var id: Int{返回dt}让dt: Int让日出:Int让日落:Int让月光:Int让月落:Int让moon_phase:双让临时:TempRange让feels_like: TempRange让压力:Int让湿度:Int让dew_point:双让wind_speed:双让wind_deg: Int让wind_gust:双让天气:[天气]让云:Int让pop:双让雨:双?let uvi: Double} struct Forecast:可解码{let lat: Double let lon: Double let timezone: String let timezone_offset: Int let daily: [ForecastDay]}

ContentView类中的forecast属性将存储我们接收到的预测数据。在该Forecast对象中,将有一个ForecastDay对象数组。List将显示的就是这个数组。

像往常一样,我们将在List中使用ForEach()构造来处理在列表中设置视图的问题。在典型的ForEach()构造中,我们为需要显示的每个数据项创建一个视图。在前面的示例中,我为此使用了内置视图类型,如Text()。对于这个赢博体育程序,我需要为每个项目显示更复杂的数据,所以我决定创建自己的视图类来显示天气数据。下面是这个类的代码:

import SwiftUI struct ForecastView: View {var fd: ForecastDay var date: date var body: some View {HStack {VStack(alignment: .leading) {Text(date,style:.date) Text("High \(Int(fd.temp.max!)) Low \(Int(fd.temp.min!))")} Image(systemName: iconName(fd.weather[0].icon)).font(.system(size:32)).padding()}} func iconName(_ icon: String) -> String {switch icon {case "01d": return "sun. day ")最大“case”02d:返回“cloud”。Sun“case”03d:返回“cloud”case“04d”:返回“cloud”。填写“case”09d:返回“cloud”。雨“情况”10d“:返回”云。太阳。"雨“例”11d:返回“云”。螺栓“壳”13d:返回“云”。雪“案”50d:归“云”。雾“默认:返回”太阳。Max "}}}

这里的一个特殊功能是图标的使用。该服务发送给我们的天气数据包括我们得到的每个预报日的图标代码。我设置了一个辅助函数,将这些openweathermap图标代码转换为我们可以在SwiftUI Image()视图中使用的iOS图标名称。

位置设置视图

该赢博体育程序包括第二个视图,用户可以在其中输入邮政编码来更改他们的位置。

下面是设置视图的代码:

import SwiftUI struct WeatherSettings: View {@EnvironmentObject private var lc: LocationInfo @State var zip: String = "" var body: some View {VStack(alignment: .leading) {HStack {Text(" zip Code") TextField("", Text:$zip). keyboardtype (. numberpad). textfieldstyle (. roundedborder)} Button("Search"){Task {let zipPattern = #"^\d{5}$"# let result = “ zip. ”range(of: zipppattern, options: . regulareexpression) if result != nil {await lc。}}.buttonStyle(. borderdered).padding() Text("Latitude: \(lc.location.latitude)") Text("Longitude: \(lc.location.longitude)") Text("Name: \(lc.location.name)")}}}

与第一个视图一样,该视图将LocationInfo对象存储在带有@EnvironmentObject属性修饰符的属性中。当第一个视图将我们导航到这个视图时,这个属性将自动为我们设置。

这个视图中最有趣的部分是搜索按钮的代码。当用户单击这个按钮时,我们将调用LocationInfo的searchLocation()方法。由于该方法通过向web服务发送GET请求来进行位置搜索,因此我们必须将其声明为异步方法。我们不能在按钮的操作代码中直接调用异步方法,所以我们必须将此代码包装在一个将在后台运行异步方法的Task中。由于LocationInfo类也是一个ObservableObject,它包含一个searchLocation()将更新的@Published属性,因此调用searchLocation()将导致这个设置视图被重建。当视图重建时它会在视图中显示新的位置信息。

由于第一个视图也使用与我们使用的相同的LocationInfo对象,因此我们的更新还将强制重建第一个视图:这将显示新位置的天气预报。