美文网首页Hacking with iOS: SwiftUI Edition
Hacking with iOS: SwiftUI Editio

Hacking with iOS: SwiftUI Editio

作者: 韦弦Zhy | 来源:发表于2020-08-27 18:47 被阅读0次

    \color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

    {\Large \mathbf{愿望清单}}

    {\Large \mathbf{Bucket \ List}}

    扩展现有类型以支持 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

    相关文章

      网友评论

        本文标题:Hacking with iOS: SwiftUI Editio

        本文链接:https://www.haomeiwen.com/subject/ywonjktx.html