Reputation: 209
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
Reputation: 3783
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
}
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()
// ...
}
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
}
}
}
ContentView
.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.
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))
}
}
}
}
}
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.
You could implement the error handling by yourself basing on the description above.
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