SwiftUI ObservableObject create non array @Published

423 views Asked by At

I tried to create an ObservableObject with non array @Published item. However, I still don't know how to do so. I tried to use a ? to do so. But when I display it in view like Text((details.info?.name)!), and it return Thread 1: Swift runtime failure: force unwrapped a nil value I don't know what the problem and how to solve. Is it my method of creating observable object class are correct?

class ShopDetailJSON: ObservableObject {
    
    @Published var info : Info?

    init(){load()}
    
    func load() {

        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data else {
                print("No data in response: \(error?.localizedDescription ?? "Unknown error").")
                return
            }
            if let decodedShopDetails = try? JSONDecoder().decode(ShopDetail.self, from: data) {
                DispatchQueue.main.async {
                    self.info = decodedShopDetails.info!
                }
            } else {
                print("Invalid response from server")
            }
        }.resume()
    }
    
}
struct Info : Codable, Identifiable {
    let contact : String?
    let name : String?
    var id = UUID()

    enum CodingKeys: String, CodingKey {

        case contact = "contact"
        case name = "name"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        contact = try values.decodeIfPresent(String.self, forKey: .contact)
        name = try values.decodeIfPresent(String.self, forKey: .name)
    }

}
struct ShopDetail : Codable {
    let gallery : [Gallery]?
    let info : Info?

    enum CodingKeys: String, CodingKey {

        case gallery = "gallery"
        case info = "info"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        gallery = try values.decodeIfPresent([Gallery].self, forKey: .gallery)
        info = try values.decodeIfPresent(Info.self, forKey: .info)
    }

}

Sample JSON data

{

    "gallery": [],
    "info": {
        "contact": "6012345678",
        "name": "My Salon",
    }
}
1

There are 1 answers

0
New Dev On

This is answer is a bit of a guess as to what happens in your code, but if the JSON data is never null, as you say in the comments, it's likely that you're trying to access a not-yet-updated ShopDetailJSON.info optional property in your view.

First, some clean-up. You don't need to the custom implementation of init(from:) - just conforming to Codable is enough in your case. And if the JSON values aren't optional, no need to make them into an optional type:

struct Info: Codable, Identifiable {
    let contact : String
    let name : String
    var id = UUID()
}

struct ShopDetail: Codable {
    let gallery : [Gallery]
    let info : Info
}

Then, when you get the JSON you wouldn't need to deal with optionals and force-unwrap ! (which should have been avoided anyways):

if let decodedShopDetails = try? JSONDecoder().decode(ShopDetail.self, from: data {
    DispatchQueue.main.async {
        self.info = decodedShopDetails.info // no force-unwrap needed
    }
}

In the view, you need to check that the info property is not nil before accessing its elements.

struct ContentView: View {

   @ObservedObject var details: ShopDetailJSON

   var body: some View {
       Group() {

           // on iOS14
           if let info = details.info {
               Text(info.name)
           }

           // pre iOS14
           // if details.info != nil {
           //    Text(details.info!.name)
           // }
       }
       .onAppear {
           self.details.load()
       }
   }
}