大师学SwiftUI第17章Part2 - WebKit 框架和JSON等网页内容加载

WebKit框架

对于某些应用,Safari视图控制器中包含的自定义选项还不够。为此Apple又提供WebKit 框架这一选项。借助于这个框架,我们可以在视图内展示网页内容。该视图通过UIView类的子类WKWebView定义。这个类提供了如下管理内容的属性和方法。

  • title:该属性返回文档标题字符串。
  • url :该属性返回带文档URL的URL结构体。
  • isLoading :该属性返回决定视图是否处于加载URL状态的布尔值。
  • canGoBack:该属性返回决定视图是否可导航至前一页的布尔值。
  • canGoForward:该属性返回决定视图是否可导航至下一页的布尔值。
  • estimatedProgress :该属性返回0.0到1.0之间Double类型的值,决定内容加载的占比。
  • load (URLRequest):该方法加载URL的内容。参数是希望打开URL的请求对象。
  • goBack() :该方法导航到导航历史记录中的上一页。
  • goForward() :该方法导航到导航历史记录中的下一页。
  • go (to : WKBackForwardListItem):该方法导航至参数指定的网页。to参数为表示导航列表中网页的对象。
  • reload() :该方法重新加载当前页(刷新网页)。
  • stopLoading() :该方法要求视图停止加载内容。

要加载网站,我们必须创建一个请求。为此UIKit 框架提供了URLRequest结构体。该结构体包含如下初始化方法。

  • URLRequest (url : URL, cachePolicy : CachePolicy, timeoutInterval : TimeInterval):这个初始化方法创建一个加载由url参数指定URL的请求。cachePolicy参数是一个枚举,指定请求如何操作缓存。可以使用的值有:useProtocolCachePolicy(默认值)、reloadIgnoringLocalCacheDatareloadIgnoringLocalAndRemoteCacheDatareturnCacheDataElseLoadreturnCacheDataDontLoadreloadRevalidatingCacheDatatimeoutInterval参数是允许系统处理请求的最大时间(默认为60.0)。只有第一个参数必填,其余的参数都有默认值。

WebKit 视图可以通过代理上报内容的状态。为此框架定义了WKNavigationDelegate协议。以下是此协议中包含的部分方法。

  • webView (WKWebView, decidePolicyFor : WKNavigationAction, decisionHandler : Closure):该方法对代理调用,指定视图是否应处理请求。decidePolicyFor参数是带有请求信息的对象,decisionHandler参数是一个闭包,必须执行它来上报我们的决策。闭包接收WKNavigationActionPolicy类型的值,这是一个属性为cancelallow的枚举。
  • webView (WKWebView, didStartProvisionalNavigation: WKNavigation!):该方法在视图开始加载新内容时对代理调用。
  • webView (WKWebView, didFinish: WKNavigation!):该方法在视图完成内容加载时对代理调用。
  • webView (WKWebView, didFailProvisionalNavigation : WKNavigation!, withError: Error):该方法在内容加载发生错误时对代理调用。
  • webView (WKWebView, didReceiveServerRedirectForProvisionalNavigation: WKNavigation!):该方法在服务端将导航器重定向到其它目标时对代理调用。

WebKit 视图是一个UIKit 视图,因此我们必须使用UIViewRepresentable进行创建。定义好representable视图后,在WebKit视图中加载网站的流程非常简单,创建请求、要求视图加载它。

示例17-10 :通过WebKit视图加载网站

swift 复制代码
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let searchURL: URL
    
    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        let request = URLRequest(url: searchURL)
        view.load(request)
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

本例中,我们通过从SwiftUI界面接收到的URL准备请求,然后使用load()方法加载网站。因为我们加载的是同一个网站,视图只需要定义好URL、传递给WebView实例。

示例17-11:显示WebKit视图

css 复制代码
struct ContentView: View {
    var body: some View {
        WebView(searchURL: URL(string: "https://www.google.com")!)
    }
}

✍️跟我一起做:创建一个多平台项目。使用示例17-10 中的代码创建Swift文件WebView.swift。用示例17-11 中的代码更新ContentView视图。在iPhone模拟器上运行应用。会看到Google的首页显示在屏幕上。

注意 :示例17-11的示例中,我们打开的是安全的URL(以https://开头的URL ),因为这是默认允许的URL 。我们在第9章 中学到,Apple实现了一个名为的应用传输安全(ATS)的系统来屏幕不安全的URL 。如果希望允许用户在WKWebView视图中加载不安全的URL,必须将ATS系统配置为Allow Arbitrary Loads 选项(见图9-3)。

通过WKWebView视图,我们可以加载包含用户指定在内的所有网站。我们只需要和前面例子一样为用户提供一种输入URL的方式,然后执行load()方法加载它。为此,在下面视图中包含有一个TextField视图和一个按钮。在点击按钮后,我们调用WebView结构体中的方法,通过用户输入的URL更新视图。

示例17-12 :允许用户插入URL

less 复制代码
struct ContentView: View {
    @State private var webView: WebView!
    @State private var inputURL: String = ""
    
    var body: some View {
        VStack {
            HStack {
                TextField("Insert URL", text: $inputURL)
                    .autocapitalization(.none)
                    .autocorrectionDisabled(true)
                Button("Load") {
                    let text = inputURL.trimmingCharacters(in: .whitespaces)
                    if !text.isEmpty {
                        webView.loadWeb(web: text)
                    }
                }
            }.padding(5)
            webView
        }.onAppear {
            webView = WebView(inputURL: $inputURL)
        }
    }
}

本例中,我们添加了一个webView属性,用于存储WebView结构体。该属性在视图出现时进行初始化,然后它用于调用结构体的方法并在屏幕上显示视图。

用户插入的URL 存储在inputURL属性中,它被传递给WebView结构体。这是为了在每次用户浏览新的页面时可以更新文本框中的值。

本例中的WebView结构体需要创建WKWebView视图、实现方法加载新URL并保持视图更新。

示例17-13 :通过用户插入的URL 更新WKWebView

swift 复制代码
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    @Binding var inputURL: String
    let view: WKWebView = WKWebView()
    
    func makeUIView(context: Context) -> WKWebView {
        view.navigationDelegate = context.coordinator
        let request = URLRequest(url: URL(string: "https://www.google.com")!)
        view.load(request)
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
    
    func loadWeb(web: String) {
        var components = URLComponents(string: web)
        components?.scheme = "https"
        if let newURL = components?.string {
            if let url = newURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
                if let loadURL = URL(string: url) {
                    let request = URLRequest(url: loadURL)
                    view.load(request)
                }
            }
        }
    }
    func makeCoordinator() -> CoordinatorWebView {
        return CoordinatorWebView(input: $inputURL)
    }
}

class CoordinatorWebView: NSObject, WKNavigationDelegate {
    @Binding var inputURL: String
    
    init(input: Binding<String>) {
        self._inputURL = input
    }
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        if let webURL = webView.url {
            inputURL = webURL.absoluteString
        }
    }
}

我们对WebView结构体做了几处修改,使其可以加载多个URL。首先,我们在makeUIView()方法外实例化了WKWebView视图,因此可以在我肯定义方法中访问。在makeUIView()方法内,我们通过将coordinator的指针赋值给视图的navigationDelegate属性来声明coordinator为视图的代理,然后创建并加载请求。视图完成初始化并会调用coordinator上报改变。但在实现coordinator前,我们定义了loadWeb()方法加载用户所输入的URL 。这一方法在用户点击文本框旁的Load按钮时执行。该方法接收一个字符串、准备好URL并通过load()方法加载它。用户输入的URL加载后内容会显示到屏幕上。下面做反向操作,我们需要在视图内容发生变化时更新文本框中的URL。这在用户点击页面上的链接导航至其它页面时发生。为此,我们让coordinator实现WKNavigationDelegate协议并实现webView(WKWebView, didCommit)方法。这个方法在加载新内容时由WKWebView调用。这里当前的URL 通过视图的url属性获取、赋值给inputURL属性,接着修改TextField视图中的值,这样文本框中的URL会与屏幕上显示的网站相一致。

✍️跟我一起做:使用示例17-12 中的代码更新ContentView.swift,用示例17-13 中的代码更新WebView.swift文件。在iPhone模拟器上运行应用。插入一个URL、点击Load 按钮。视图中会加载这一URL 并显示网站。点击页面上的链接导航至另一个页面。文本框中的URL会与屏幕上的页面地址保持一致。

我们到目前所构建的应用中,用户可以访问任意URL 并通过点击链接导航至其它页面,但界面中并没有提供访问导航历史前一页和后一页的方式。WKWebView类提供了一些控制内容的方法。比如,有一个goBack()方法可以回到前一页,goForward()方法可以回到上一页,而reload()方法可以刷新页面。要执行这些方法,我们在导航栏下添加三个按钮。

示例17-14:提供导航按钮

less 复制代码
struct ContentView: View {
    @State private var webView: WebView!
    @State private var inputURL: String = ""
    @State private var backDisabled: Bool = true
    @State private var forwardDisabled: Bool = true
    
    var body: some View {
        VStack {
            HStack {
                TextField("Insert URL", text: $inputURL)
                Button("Load") {
                    let text = inputURL.trimmingCharacters(in: .whitespaces)
                    if !text.isEmpty {
                        webView.loadWeb(web: text)
                    }
                }
            }.padding(5)
            
            HStack {
                Button(action: {
                    webView.goBack()
                }, label: {
                    Image(systemName: "arrow.left.circle")
                        .font(.title)
                }).disabled(backDisabled)
                Button(action: {
                    webView.goForward()
                }, label: {
                    Image(systemName: "arrow.right.circle")
                        .font(.title)
                }).disabled(forwardDisabled)
                Spacer()
                Button(action: {
                    webView.refresh()
                }, label: {
                    Image(systemName: "arrow.clockwise.circle")
                        .font(.title)
                })
            }.padding(5)
            webView
        }.onAppear {
            webView = WebView(inputURL: $inputURL, backDisabled: $backDisabled, forwardDisabled: $forwardDisabled)
        }
    }
}

以上视图增加了两个@State属性,指定前一页和后一页按钮是否可点击。在初次显示视图时,按钮应处于禁用,因为视图中只加载了一个文档,但在加载了新文档后,我们需要启用按钮让用户可在导航历史中向前或向后访问。为此我们必须将这些属性传给WebView结构体,每次加载文档时在coordinator中修改这些值。

示例17-15:在访问历史中向前或向后导航

less 复制代码
struct WebView: UIViewRepresentable {
    @Binding var inputURL: String
    @Binding var backDisabled: Bool
    @Binding var forwardDisabled: Bool
    
    let view: WKWebView = WKWebView()
    
    func makeUIView(context: Context) -> WKWebView {
        view.navigationDelegate = context.coordinator
        let request = URLRequest(url: URL(string: "https://www.google.com")!)
        self.view.load(request)
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
    
    func loadWeb(web: String) {
        var components = URLComponents(string: web)
        components?.scheme = "https"
        if let newURL = components?.string {
            if let url = newURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
                if let loadURL = URL(string: url) {
                    let request = URLRequest(url: loadURL)
                    view.load(request)
                }
            }
        }
    }
    func goBack() {
        view.goBack()
    }
    func goForward() {
        view.goForward()
    }
    func refresh() {
        view.reload()
    }
    func makeCoordinator() -> CoordinatorWebView {
        return CoordinatorWebView(input: $inputURL, back: $backDisabled, forward: $forwardDisabled)
    }
}

class CoordinatorWebView: NSObject, WKNavigationDelegate {
    @Binding var inputURL: String
    @Binding var backDisabled: Bool
    @Binding var forwardDisabled: Bool
    
    init(input: Binding<String>, back: Binding<Bool>, forward: Binding<Bool>) {
        self._inputURL = input
        self._backDisabled = back
        self._forwardDisabled = forward
    }
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        if let webURL = webView.url {
            inputURL = webURL.absoluteString
            backDisabled = !webView.canGoBack
            forwardDisabled = !webView.canGoForward
        }
    }
}

这段代码对WebView结构体添加了三个方法,来执行用户选择的操作(向前、向后或刷新页面)。在webView(WKWebView, didFinish:)方法中,我们和之前一样更新文本框中的URL ,但同时使用了canGoBackcanGoForward的值变更按钮的状态,这样只在有页面可供打开时才启用按钮。

图17-5:导航按钮

✍️跟我一起做:使用示例17-14 中的代码更新ContentView.swift文件,用示例17-15 中的代码更新WebView.swift文件。在iPhone模拟器中运行应用。在Google中搜索一个词,点击链接再点击返回按钮,这时视图会回到上一个页面。

注意WebKit 框架还提供了处理cookieJavaScript 代码的工具,让我们可以和文档的内容进行交互。这里暂不讨论。更多内容请参见苹果官方文档

Web内容

Safari视图控制器和WKWebView用于向用户展示内容,但内容与应用之间的集成却十分有限。有时会只需要从文档中提取部分信息,或是处理数据而不是将内容原封不动展示出来。这时,我们可以在后台加载、解析文档,只提取出我们需要的内容。

Foundation包含了一组获取URL指向内容的类。其中最重要的类是URLSession。这个类创建一个管理HTTP连接的会话,用于提取数据、下载或上传文件。下面是该类为创建会话所提供的部分属性和初始化方法。

  • shared:这一类型属性返回一些默认配置的标准会话,适于执行基本请求。
  • URLSession (configuraiton : URLSessionConfiguration):这个初始化方法按照参数配置新建会话。configuration参数是一个指定会话行为的对象。
  • URLSession (configuration : URLSessionConfiguration, delegate : URLSessionDelegate?, delegateQueue : OperationQueue?):这个初始化方法通过参数指定配置新建会话。configuration参数是一个指定会话行为的对象,delegate参数是我们希望赋给会话的代理对象指针,delegateQueue参数是代理方法中所要执行的队列。

会话配置连接,但并不执行任何任务。下载或上传数据,我们必须实现URLSession类中定义的如下方法。

  • data (from : URL, delegate : URLSessionTaskDelegate?):这一异步方法对会话添加任务,下载from参数指定URL 的数据。delegate参数是任务在处理过程上报更新所使用的代理对象。该方法返回一个带两个值的元组:一个是包含服务端返回数据的Data结构体,一个是包含请求状态的URLResponse对象。
  • download (from : URL, delegate : URLSessionTaskDelegate?):这个异步方法向会话添加任务,下载from参数指定的URL对应的文件。delegate参数是任务在处理过程上报更新所使用的代理对象。该方法返回一个带两个值的元组:一个是包含表示所下载文件位置的的URL结构体,一个是包含请求状态的URLResponse对象。

以下由该类定义的用于上传数据和文件的方法。

  • upload (for : URLRequest, from : Data, delegate : URLSessionTaskDelegate?):这一异步方法向会话添加一个任务,上传from参数所指定的数据。delegate参数是任务在处理过程上报更新所使用的代理对象。该方法返回一个包含两个值的元组:一个是包含服务端返回数据的Data结构体,一个是包含请求状态的URLResponse对象。
  • upload (for : URLRequest, fromFile : URL, delegate : URLSessionTaskDelegate?):这个异步方法向会话添加任务,上传fromFile参数指定的URL对应的文件。delegate参数是任务在处理过程上报更新所使用的代理对象。该方法返回一个带两个值的元组:一个是服务端返回数据的Data结构体,一个是包含请求状态的URLResponse对象。

这些方法都是异步的,也就是说它们在数据下载或上传完成后舞台结果。例如,我们使用data()方法获取 网站数据,返回值包含数据值及一个类型为URLResponse的带请求状态的对象。在通过HTTP 协议访问URL 时,响应由一个HTTPURLResponse类型(URLResponse的子类)的对象表示。这个类包含表示请求状态码的statusCode属性。有很多的状态,比如200,表示请求成功,或时301,表示网站跳转到另一个地址。如果要确保正确下载数据,可以在做处理前检查statusCode属性的值是否为200。下例展示了如何执行一个简单的请求。

示例17-16:加载远程文档

swift 复制代码
import SwiftUI
import Observation

@Observable class ApplicationData {
    var webContent: String = ""
    var buttonDisabled: Bool = false
    
    func loadWeb() async {
        buttonDisabled = true
        
        let session = URLSession.shared
        let webURL = URL(string: "https://www.yahoo.com")
        do {
            let (data, response) = try await session.data(from: webURL!)
            if let resp = response as? HTTPURLResponse {
                let status = resp.statusCode
                if status == 200 {
                    if let content = String(data: data, encoding: String.Encoding.ascii) {
                        await MainActor.run {
                            webContent = content
                            buttonDisabled = false
                        }
                        print(content)
                    }
                } else {
                    print("Error: (status)")
                }
            }
        } catch {
            print("Error: (error)")
        }
    }
}

这个模型加载www.yahoo.com网站的内容,将其赋值给webContent属性。这一操作由loadWeb()方法执行。该方法使用https://www.yahoo.com这一URL定义请求,然后调用会话的data()方法下载页面。这个方法下载指定地址的内容,检测操作是否成功(200),从数据接收字符串,用这个值更新webContent属性让其可在视图中使用。下面是处理这个数据的简单视图。

示例17-17:显示文档内容

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        VStack {
            Button("Load Web") {
                Task(priority: .high) {
                    await appData.loadWeb()
                }
            }.disabled(appData.buttonDisabled)
            
            Text("Total Characters: (appData.webContent.count)")
                .padding()
            Spacer()
        }.padding()
    }
}

www.yahoo.com返回的内容非常多。这里方便演示,在控制台中打印内容,按字符串进行字符计数,显示在屏幕上,但专业的应用通常会处理其值提取信息。

✍️跟我一起做:创建一个多平台项目。使用示例17-16 的代码创建一个名为ApplicationData.swift的Swift文件。使用示例17-17 的代码更新ContentView视图。记住要在应用和预览中将ApplicationData对象注入环境(第7章示例7-4 )。在iPhone模拟器上运行应用。点击Load Web 按钮。等待数秒,会看到从www.yahoo.com下载的文档在控制台中打印,并且在屏幕上显示了字符数。

我们在本例中使用的这种默认配置的标准会话适用于大多数场景,而自定义会话需要自行配置。为进行会话配置,Foundation提供了一个名为URLSessionConfiguration的类。下面的类型属性可用于获取带默认值的配置对象。

  • default :该属性返回一个带默认设置的URLSessionConfiguration

通过标准配置获得对象后,我们可以对其自定义满足自己应用的要求。下面来自URLSessionConfiguration类的属性可用于配置会话。

  • allowsCellularAccess:此属性设置或返回一个布尔值,指定在设备通过蜂窝网络连接时是否进行连接。
  • timeoutIntervalForRequest :该属性返回一个TimeInterval值(Double的别名),指定会话等待请求回复的秒数。默认值是60.
  • waitsForConnectivity :该属性设备或返回一个布尔值,指定会话是否等待设备连接到网络后再执行请求。默认值是false

处理自定义会员只需要改变会话的初始化,其它代码保持不变。

示例17-18:初始化自定义会话

ini 复制代码
@Observable class ApplicationData {
    var webContent: String = ""
    var buttonDisabled: Bool = false
    
    func loadWeb() async {
        buttonDisabled = true
        
        let config = URLSessionConfiguration.default
        config.waitsForConnectivity = true
        let session = URLSession(configuration: config)
        
        let webURL = URL(string: "https://www.yahoo.com")
        do {
            let (data, response) = try await session.data(from: webURL!)
            if let resp = response as? HTTPURLResponse {
                let status = resp.statusCode
                if status == 200 {
                    if let content = String(data: data, encoding: String.Encoding.ascii) {
                        await MainActor.run {
                            webContent = content
                            buttonDisabled = false
                        }
                        print(content)
                    }
                } else {
                    print("Error: (status)")
                }
            }
        } catch {
            print("Error: (error)")
        }
    }
}

上例中,我们没有实现data()方法的delegate参数。这是一个可选参数,但可以在需要响应及处理更新时进行声明。框架定义了一个URLSessionTaskDelegate协议来创建这个代理 。下面是协议中的一些方法。

  • urlSession (URLSession, task : URLSessionTask, didReceive : URLAuthenticationChallenge, completionHandler: Closure):该方法在服务端请求需要进行校验时对代理调用。我们的实现必须使用定义设置和认证信息的两个参数调用方法接收的完结处理器。
  • urlSession (URLSession, task : URLSessionTask, willPerformHTTPRedirection : HTTPURLResponse, newRequest : URLRequest, completionHandler : Block):该方法在服务端将请求重定向到另一个URL 时对代理调用。我们的实现必须通过新请求(newRequest参数的值)定义的参数或在不希望重定向时用nil调用方法接收的完结处理器。

一些网站,比如www.yahoo.com,自动将用户重定向到另一个适配用户地理位置和偏好网站版本的地址。这意味着我们所提供的URL 不是最终地址。服务端不返回任何数据,而是将用户重定向到另一个文档。这时,我们可以定义一个带代理的自定义会话,然后实现URLSessionTaskDelegate协议方法指定服务端重定向应用时我们希望做的操作。

示例17-19:重定向用户

swift 复制代码
@Observable class ApplicationData: NSObject, URLSessionTaskDelegate {
    var webContent: String = ""
    var buttonDisabled: Bool = false
    
    func loadWeb() async {
        buttonDisabled = true
        
        let session = URLSession.shared
        
        let webURL = URL(string: "https://www.yahoo.com")
        do {
            let (data, response) = try await session.data(from: webURL!, delegate: self)
            if let resp = response as? HTTPURLResponse {
                let status = resp.statusCode
                if status == 200 {
                    if let content = String(data: data, encoding: String.Encoding.ascii) {
                        await MainActor.run {
                            webContent = content
                            buttonDisabled = false
                        }
                        print(content)
                    }
                } else {
                    print("Error: (status)")
                }
            }
        } catch {
            print("Error: (error)")
        }
    }
    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? {
        print(request.url ?? "No URL")
        return request
    }
}

✍️跟我一起做:使用示例17-19 的代码更新ApplicationData类。在iPhone模拟器中运行应用。点击Load Web 按钮。会在控制台中打印出用户重定向的URL

www.yahoo.com等所返回的网页文档,是用HTML 写的。这是一种简单的编程语言,由网站用于组织信息。从这些文档中提取数据比较枯燥、易出错。因此,网站通常会提供以JSON 格式分享数据的服务。JSON 文档动态生成,仅包含应用所请求的信息。例如,www.openweathermap.org提供了生成包含天气信息JSON 文档的服务(https://openweathermap.org/api)。

为演示如何访问和处理这些服务生成的文档,我会从一个生成假文档的www.openweathermap.orgjsonplaceholder.typicode.com)网站上读取文章。这一处理不需要新知识。我们必须通过URLSession加载文档并使用JSONDecoder对象进行解码。

示例17-20 :加载JSON文档

swift 复制代码
struct Post: Codable, Identifiable {
    var id: Int
    var userId: Int
    var title: String
    var body: String
}

@Observable class ApplicationData {
    var listOfPosts: [Post] = []
    
    init() {
        Task(priority: .high) {
            await loadJSON()
        }
    }
    func loadJSON() async {
        let session = URLSession.shared
        let webURL = URL(string: "https://jsonplaceholder.typicode.com/posts")
        
        do {
            let (data, response) = try await session.data(from: webURL!)
            if let resp = response as? HTTPURLResponse {
                let status = resp.statusCode
                if status == 200 {
                    let decoder = JSONDecoder()
                    if let posts = try? decoder.decode([Post].self, from: data) {
                        await MainActor.run {
                            listOfPosts = posts
                        }
                    }
                } else {
                    print("Error: (status)")
                }
            }
        } catch {
            print("Error: (error)")
        }
    }
}

我们在第10章 中学过,解码JSON 文档,我们需要定义一个与JSON 值相匹配的结构体。URL https://jsonplaceholder.typicode.com/posts返回一个文章列表,每条包含四个值:表示用户ID的整数、表示文章ID的整数、表示文章标题的字符串和表示内容的字符串。为存储这些值,我们在示例17-20 中定义了一个Post结构体。为能够解码其中的值这个结构体实现了Codable协议,为能使用List视图列出实例实现了Identifiable协议。

文档下载的处理与之前相同。我们获取会话,调用data()方法,使用JSONDecoder将数据解码至Post结构体数组中,将值存储到listOfPosts属性中更新视图。因文档在模型初始化时下载,我们只需要在视图中列出这些值即可。

示例17-21:列出文档中的值

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        VStack {
            List {
                ForEach(appData.listOfPosts) { post in
                    VStack(alignment: .leading) {
                        Text(post.title).bold()
                        Text(post.body)
                    }.padding(5)
                }
            }.listStyle(.plain)
        }.padding()
    }
}

✍️跟我一起做:使用示例17-20中的代码更新ApplicationData.swift文件,用示例17-21 的代码更新ContentView视图。运行应用。应该会在屏幕上看到100条信息。要查看https://jsonplaceholder.typicode.com/posts所返回JSON文件的结构,可在浏览器中直接打开该链接。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》

相关推荐
I烟雨云渊T7 小时前
iOS 门店营收表格功能的实现
ios
明月看潮生13 小时前
青少年编程与数学 01-011 系统软件简介 07 iOS操作系统
ios·青少年编程·操作系统·系统软件
90后的晨仔15 小时前
RxSwift 框架解析
前端·ios
大熊猫侯佩19 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩19 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(四)
数据库·swiftui·apple watch
可爱小仙子19 小时前
ios苹果系统,js 滑动屏幕、锚定无效
前端·javascript·ios
未来猫咪花20 小时前
# Flutter状态管理对比:view_model vs Riverpod
flutter·ios·android studio
咕噜企业签名分发-淼淼1 天前
开发源码搭建一码双端应用分发平台教程:逐步分析注意事项
android·ios
键盘敲没电1 天前
【IOS】GCD学习
学习·ios·objective-c·xcode
SY.ZHOU1 天前
Significant Location Change
macos·ios·cocoa