Hacking with iOS: SwiftUI Editio
扩展现有类型以支持 ObservableObject
用户现在可以在我们的MapView
上放置标注,但他们无法执行任何操作——他们无法附加自己的标题和副标题。解决此问题需要一些思考,因为MKPointAnnotation
使用可选字符串作为标题和副标题,而 SwiftUI 不允许我们将可选字符串绑定到文本字段。
有两种解决方法,但到目前为止,最简单的方法是编写MKPointAnnotation
扩展,以在标题和副标题添加计算属性,这意味着我们可以使该类与ObservableObject
保持一致,而无需任何进一步的工作。您可以随意调用这些计算属性——名称,信息,详细信息等——但从长远来看,您可能会发现将它们标记为简单包装从长远来看更容易记住,这就是为什么我要使用命名为wrapTitle
和wraptedSubtitle
。
创建一个名为MKPointAnnotation-ObservableObject.swift
的新Swift文件,更改其 Foundation 导入为 MapKit 的 ,然后为其提供以下代码:
extension MKPointAnnotation: ObservableObject {
public var wrappedTitle: String {
get {
self.title ?? "Unknown value"
}
set {
title = newValue
}
}
public var wrappedSubtitle: String {
get {
self.subtitle ?? "Unknown value"
}
set {
subtitle = newValue
}
}
}
请注意,我还没有将这些计算出的属性标记为@Published
, 这是可以的,因为在更改属性时我们实际上不会读取属性,因此无需在用户输入时继续刷新视图。
有了新的扩展之后,我们在MKPointAnnotation
上有了两个非可选的属性,这意味着我们现在可以在SwiftUI视图中将一些UI控件绑定到它们——我们可以创建一个用于编辑地标的UI。
与往常一样,我们将从小处着手,逐步进行,因此,请创建一个名为“EditView”的新SwiftUI视图,为其添加MapKit 导入,然后为其提供以下代码:
import SwiftUI
import MapKit
struct EditView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject var placemark: MKPointAnnotation
var body: some View {
NavigationView {
Form {
Section {
TextField("Place name", text: $placemark.wrappedTitle)
TextField("Description", text: $placemark.wrappedSubtitle)
}
}
.navigationBarTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
让您更新预览代码,以便它传递到我们的示例MKPointAnnotation
中,如下所示:
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(placemark: MKPointAnnotation.example)
}
}
我们想在ContentView
中的两个地方显示它:当用户添加一个地方时,我们希望他们立即对其进行编辑,以及当他们在我们的固定警报中按下Edit
按钮时。
这两个条件都将由布尔条件触发,因此首先将此@State
属性添加到ContentView
:
@State private var showingEditScreen = false
当用户在我们的警报中点击“Edit”时,应将其设置为true
,这表示将// edit this place
注释替换为:
self.showingEditScreen = true
而且,这还意味着当他们刚刚向地图添加新地点时将其设置为true,但是我们还需要设置selectedPlace
属性,以便我们的代码知道应编辑哪个地点。因此,将其放在self.locations.append(newLocation)
行下面:
self.selectedPlace = newLocation
self.showingEditScreen = true
最后,我们需要将showingEditScreen
绑定到工作表,以便在适当的时候为我们的EditView
结构体显示一个地标。请记住,如果在此处我们无法使用 if let
解除selectedPlace
可选,我们将无法使用,因此我们将进行简单的检查然后强制进行包装——同样安全。
请在现有警报(.alert()
)之后将此sheet()
修饰符附加到ContentView
:
.sheet(isPresented: $showingEditScreen) {
if self.selectedPlace != nil {
EditView(placemark: self.selectedPlace!)
}
}
这是我们应用程序的下一步,现在几乎有用了——您可以浏览地图,点击以放置标注,然后为其赋予有意义的标题和副标题。
从高德地图查询POI数据
为了使整个应用程序更有用,我们将修改EditView
界面,使其显示有趣的地方。毕竟,如果您将伦敦旅游列入您的购物清单,您可能希望对附近的景点有一些建议。这听起来很难做到,但是实际上我们可以使用GPS坐标查询 Wikipedia(国内访问不了,替换为高德的POI接口,部分代码和设计相对原文有改动),并且它将返回附近的地点列表。
高德的API以精确的格式发送回JSON数据,因此我们需要做一些工作来定义能够存储所有内容的Codable
结构。结构是这样的:
- 主要结果在名为“pois”的键中包含我们查询的结果。
- "pois"是一个数组,数组内容是一个字典,key值为索引,内容为POI详情
- 每个POI都有很多信息,包括其坐标,标题,描述,位置等。
{
"suggestion":Object{...},
"count":"6",
"infocode":"10000",
"pois":[
{
"distance":"1519",
"pcode":"620000",
"type":"风景名胜;风景名胜;纪念馆",
"gridcode":"5303512311",
"typecode":"110204",
"citycode":"0930",
"adname":"临夏县",
"id":"B0GUKCE3A6",
"timestamp":"2020-08-16 10:20:06",
"address":"莲花乡",
"pname":"甘肃省",
"biz_type":"tour",
"cityname":"临夏回族自治州",
"name":"解放军抢渡黄河纪念馆",
"location":"103.167187,35.770813",
},
Object{...},
Object{...},
Object{...},
Object{...},
Object{...}
],
"status":"1",
"info":"OK"
}
我们可以使用两个结构体来表示它,因此创建一个名为 Result.swift 的新Swift文件并为其提供以下内容:
struct Result: Codable {
let pois: [POI]
let count: String
}
struct POI: Codable {
let id: String
let name: String
let cityname: String
}
我们将使用它来存储从高德地图获取的数据,然后立即将其显示在我们的UI中。但是,我们需要在抓取过程中显示一些内容——文字视图中显示 “正在加载” 或类似内容应该可以解决问题。
这意味着根据当前加载状态有条件地显示不同的用户界面,这意味着定义一个枚举,该枚举实际存储当前加载状态,否则我们不知道要显示什么。
首先将此嵌套枚举添加到EditView
:
enum LoadingState {
case loading, loaded, failed
}
这覆盖了我们网络请求所需的所有状态。
接下来,我们将在EditView
中添加两个属性:一个用于表示加载状态,另一个用于在提取完成后存储一系列POI信息。因此,现在添加这两个:
@State private var loadingState = LoadingState.loading
@State private var pois = [POI]()
在处理网络请求本身之前,我们要做的最后一件事是:在表单中添加一个新部分,以显示页面是否已加载,否则显示状态文本视图。我们可以将这些if / else if
条件放到Section
中,SwiftUI会弄清楚。
因此,请将这个Section
放在现有的下面:
Section(header: Text("附近...")) {
if loadingState == .loaded {
List(pois, id: \.id) { poi in
Text(poi.name)
.font(.headline)
+ Text(": ") +
Text(poi.cityname)
.italic()
}
} else if loadingState == .loading {
Text("加载中…")
} else {
Text("请稍后重试.")
}
}
现在,对于真正将所有这些结合在一起的部分:我们需要从高德地图API中获取一些数据,将其解码为Result
,将其页面分配给我们的pois
属性,然后将loadingState
设置为.loaded
。如果抓取失败,我们将loadingState
设置为.failed
,SwiftUI将加载相应的UI。
将此方法添加到EditView
中:
func fetchNearbyPlaces() {
// URL 参数请参考高德地图 POI - API
let urlString = "http://restapi.ama.com/v3/place/aroundkey=2d20ea6c631d11822d331dac71f2bcbf&location\(placemark.coordinate.longitude),\(placemark.coordinat.latitude)&keywords=&types=110000&radius=10000&offset=20&page=1&extensons=all"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
// 请求成功!
let decoder = JSONDecoder()
if let items = try? decoder.decode(Result.self, from: data) {
// 解码成功,赋值
print(items.count)
self.pois = items.pois
self.loadingState = .loaded
return
}
}
// 请求失败
self.loadingState = .failed
}.resume()
}
因为是http 所以需要在Info.plist 做如下配置:
该请求应在视图出现后立即开始,因此请在现有navigationBarItems()
修饰符之后添加此onAppear()
修饰符:
.onAppear(perform: fetchNearbyPlaces)
北京景点示例
现在继续运行该应用程序——您会发现在按下标注时,我们的EditView
屏幕将向上滑动并显示附近的所有地点。真好!
给请求结果pois
排序
可以通过让POI
结构体遵守Comparable
协议实现直接使用sorted()
排序。 修改 POI
结构体如下:
struct POI: Codable, Comparable {
let id: String
let name: String
let cityname: String
static func < (lhs: POI, rhs: POI) -> Bool {
lhs.name < rhs.name
}
}
现在,Swift了解了该如何对pois
进行排序,它将自动为我们提供页面数组上无参数的sorted()
方法。这意味着当我们在fetchNearbyPlaces()
中设置self.pois
时,我们现在可以在末尾添加sorted()
,如下所示:
self.pois = items.pois.sorted()
如果您现在运行该应用程序,将会看到地图标注附近的位置现在按其名称的字母顺序进行了排序!
按照字母顺序也许并不是最优解,也许距离不错,希望你们能试试实现。
译自
Extending existing types to support ObservableObject
Downloading data from Wikipedia