runemonster
runemonster

Reputation: 209

How do you implement custom delegates in SwiftUI

As an example I have a SwitUI ContentView. The one that comes when you first make the project.

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))
                }
           }
       }
   }
}

I have a TestManager that will handle an Api call. I Made a delegate for the class that has two functions.

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
       }
     }

  }

This is the testModel data class. Only grabbing the text of the Json returned.

struct TestModel :Decodable{
    let text: String
}

How do I connect the TestManager to the view and have the view handle the delegate like how we could do in in storyboards?

Upvotes: 5

Views: 5351

Answers (1)

Aleksey Potapov
Aleksey Potapov

Reputation: 3783

Regarding the TestModel

Decodable protocol (in your context) assumes you to create the model struct with all the properties, that you get via JSON. When requesting http://numbersapi.com/2/trivia?json you'll get something like:

{
 "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"
}

Which means, your model should look like the following:

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

Regarding Delegates

In SwiftUI this approach is not reachable. Instead, developers need to adapt the Combine framework's features: property wrappers @ObservedObject, @Published, and ObservableObject protocol. You want to put your logic into some struct. Bad news, that (currently) ObservableObject is AnyObject protocol (i.e. Class-Only Protocol). You'll need to rewrite your TestManager as class as:

class TestManager: ObservableObject {
   // ...
}

Only then you could use it in your CurrentView using @ObservedObject property wrapper:

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

Regarding the TestManager

Your logic now excludes the delegate as such, and you need to use your TestModel to pass the data to your CustomView. You could modify TestManager by adding new property with @Published property wrapper:

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. To be able to access your model "from outside", in your case the ContentView.
  2. Use DispatchQueue.main.async{ } for async tasks, because Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
  3. Simply use your parsed model.

Then in ContentView use your TestManager like this:

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))
                }
            }
        }
    }
}

Regarding HTTP

You use the link http://numbersapi.com/2/trivia?json which is not allowed by Apple, please, use https instead, or add the App Transport Security Settings key with Allow Arbitrary Loads parameter set to YES into your Info.Plist. But do this very carefully as the http link simply will not work.

Further steps

You could implement the error handling by yourself basing on the description above.

Full code (copy-paste and go):

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
}

Upvotes: 5

Related Questions