如何在SwiftUI中实现自定义代理?

5

举个例子,我有一个SwitUI ContentView。这是在你创建项目时第一次出现的那个。

import SwiftUI

struct ContentView: View {
   var manager = TestManager()
   var body: some View {
    ZStack{
        Color(.green)
            .edgesIgnoringSafeArea(.all)
        VStack {
            Text("Test Text")

            Button(action:{}) {
                Text("Get number 2")
                    .font(.title)
                    .foregroundColor(.white)
                .padding()
                .overlay(RoundedRectangle(cornerRadius: 30)
                .stroke(Color.white, lineWidth: 5))
                }
           }
       }
   }
}

我有一个TestManager来处理Api调用。我为该类创建了一个委托,其中包含两个函数。
protocol TestManagerDelegate {
    func didCorrectlyComplete(_ testName: TestManager, model: TestModel)
    func didFailWithError(_ error: Error)
}

struct TestManager {

    var delegate: TestManagerDelegate?
    let urlString = "http://numbersapi.com/2/trivia?json"

    func Get(){
        if let url = URL(string: urlString){

            let session = URLSession(configuration: .default)

            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil{
                    self.delegate?.didFailWithError(error!)
                    return
                }

                if let safeData = data{
                    if let parsedData = self.parseJson(safeData){
                        self.delegate?.didCorrectlyComplete(self, model: parsedData)
                    }
                }
            }
            task.resume()
        }
    }

   func parseJson(_ jsonData: Data) -> TestModel?{
       let decoder = JSONDecoder()
       do {
           let decodedData = try decoder.decode(TestModel.self,  from: jsonData)
           let mes = decodedData.message
           let model = TestModel(message: mes)
           return model

       } catch {
           delegate?.didFailWithError(error)
           return nil
       }
     }

  }

这是 testModel 数据类。仅获取返回的 Json 的文本。

struct TestModel :Decodable{
    let text: String
}

如何将TestManager连接到视图并使视图处理委托,就像我们在storyboards中所做的那样?


1
寻找MVVM模式 - 对于SwiftUI而言,它比代理更加自然,也就是说,您可以使用ViewModel作为视图和管理器之间的线索;当然,它也可以扮演代理角色,但对我来说似乎有些冗余。 - Asperi
我查询了MVVM,所以它基本上与我在下面标记为正确的ObservableObject答案一样?虽然不是完全相同的步骤,但足够相似,遵循绑定视图到ViewModel的相同模式? - runemonster
1个回答

5

关于TestModel

Decodable协议(在您的情况下)假定您通过JSON创建包含所有属性的模型结构。当请求http://numbersapi.com/2/trivia?json时,您将获得以下内容:

{
 "text": "2 is the number of stars in a binary star system (a stellar system consisting of two stars orbiting around their center of mass).",
 "number": 2,
 "found": true,
 "type": "trivia"
}

这意味着,你的模型应该长成下面这个样子:
struct TestModel: Decodable {
    let text: String
    let number: Int
    let found: Bool
    let type: String
}

关于委托

在SwiftUI中,这种方法是不可行的。相反,开发人员需要适应Combine框架的功能:属性包装器@ObservedObject@PublishedObservableObject协议。您想将自己的逻辑放入某个结构体中。不幸的是,(目前)ObservableObjectAnyObject协议(即仅类协议)。您需要将您的TestManager重写为类,如下所示:

class TestManager: ObservableObject {
   // ...
}

只有这样,您才能使用@ObservedObject属性包装器在CurrentView中使用它:

struct ContentView: View {
    @ObservedObject var manager = TestManager()
    // ...
}

关于TestManager

现在您的逻辑已经排除了delegate,您需要使用您的TestModel将数据传递给您的CustomView。您可以通过添加带有@Published属性包装器的新属性来修改TestManager

class TestManager: ObservableObject {
    let urlString = "http://numbersapi.com/2/trivia?json"
    // 1
    @Published var model: TestModel?

    func get(){
        if let url = URL(string: urlString){
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { [weak self] (data, response, error) in
                // 2
                DispatchQueue.main.async { 
                    if let safeData = data {
                        if let parsedData = self?.parseJson(safeData) {
                            // 3
                            self?.model = parsedData
                        }
                    }
                }
            }
            task.resume()
        }
    }

    private func parseJson(_ jsonData: Data) -> TestModel? {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(TestModel.self, from: jsonData)
            return decodedData
        } catch {
            return nil
        }
    }
}
  1. 为了能够从外部访问您的模型,例如 ContentView,您需要进行调整。
  2. 对于异步任务,请使用 DispatchQueue.main.async{ },因为在后台线程中发布更改是不允许的;请确保通过像 receive(on:) 这样的操作符在模型更新时从主线程发布值。
  3. 只需使用您解析后的模型即可。

然后在 ContentView 中像这样使用您的 TestManager

struct ContentView: View {
    @ObservedObject var manager = TestManager()
    var body: some View {
        ZStack{
            Color(.green)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
                Button(action:{ self.manager.get() }) {
                    Text("Get number 2")
                        .font(.title)
                        .foregroundColor(.white)
                        .padding()
                        .overlay(RoundedRectangle(cornerRadius: 30)
                            .stroke(Color.white, lineWidth: 5))
                }
            }
        }
    }
}

关于HTTP

您正在使用链接http://numbersapi.com/2/trivia?json,这是苹果不允许的,请改用https,或者将App Transport Security Settings键添加到Info.Plist中,并将Allow Arbitrary Loads参数设置为YES 。但请务必小心,因为http链接根本无法工作。

进一步的步骤

您可以根据上面的描述自己实现错误处理。

完整代码(复制粘贴并执行):

import SwiftUI

struct ContentView: View {
    @ObservedObject var manager = TestManager()
    var body: some View {
        ZStack{
            Color(.green)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
                Button(action:{ self.manager.get() }) {
                    Text("Get number 2")
                        .font(.title)
                        .foregroundColor(.white)
                        .padding()
                        .overlay(RoundedRectangle(cornerRadius: 30)
                            .stroke(Color.white, lineWidth: 5))
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class TestManager: ObservableObject {
    let urlString = "http://numbersapi.com/2/trivia?json"
    @Published var model: TestModel?

    func get(){
        if let url = URL(string: urlString){
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { [weak self] (data, response, error) in
                DispatchQueue.main.async {
                    if let safeData = data {
                        if let parsedData = self?.parseJson(safeData) {
                            self?.model = parsedData
                        }
                    }
                }
            }
            task.resume()
        }
    }

    private func parseJson(_ jsonData: Data) -> TestModel? {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(TestModel.self, from: jsonData)
            return decodedData
        } catch {
            return nil
        }
    }
}

struct TestModel: Decodable {
    let text: String
    let number: Int
    let found: Bool
    let type: String
}

在SwiftUI中,当我们尝试获取数据并将其返回到视图时,使用ObservableObject是我们应该采用的方式吗?还有另一种在SwiftUI中让我们基本上做同样事情的方法吗?或者说这是我们目前拥有的最好的方式吗? - runemonster
1
针对这个问题所描述的需求,使用ObservableObject是唯一的方法。 - Aleksey Potapov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接