Hacking with iOS: SwiftUI Edition - 愿望清单项目(二)

时间:2022-07-24
本文章向大家介绍Hacking with iOS: SwiftUI Edition - 愿望清单项目(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

扩展现有类型以支持 ObservableObject

用户现在可以在我们的MapView上放置标注,但他们无法执行任何操作——他们无法附加自己的标题和副标题。解决此问题需要一些思考,因为MKPointAnnotation使用可选字符串作为标题和副标题,而 SwiftUI 不允许我们将可选字符串绑定到文本字段。

有两种解决方法,但到目前为止,最简单的方法是编写MKPointAnnotation扩展,以在标题和副标题添加计算属性,这意味着我们可以使该类与ObservableObject保持一致,而无需任何进一步的工作。您可以随意调用这些计算属性——名称,信息,详细信息等——但从长远来看,您可能会发现将它们标记为简单包装从长远来看更容易记住,这就是为什么我要使用命名为wrapTitlewraptedSubtitle

创建一个名为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