UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Help debugging why nested array loaded from JSON is not showing

Forums > SwiftUI

I'm working on my first Swift app, and I've run into an issue that I'm not sure how to debug.

I've decided to use JSON files to store the data for my app, and I have one I use for previews. In this case, it's a list with multiple items. The file is loaded using a Bundle extension similar to what was shown in the Moonshot example couse app (with a few additional detailed error messages if something goes wrong).

The source data for the list in question (from the JSON file) is this:

{
    "id": "e644e95f-eb52-4da7-822d-515589767600",
    "name": "Preview List",
    "items": [
            {
                "id": "47a986df-2f45-4558-abd2-de11c91067bf",
                "name": "Cool thing 2"
            },
            {
                "id": "16998a9a-e2fd-4ea4-bdb2-807fae315f6e",
                "name": "Cool thing 3"
            }
    ]
}

The issue I'm having is that on my list detail view, the items aren't showing. There are no errors, and the correct title for that list shows (which comes from the file), but no items. This is the case whether the preview loads "directly" from this view file, or if I navigate to it from the parent (which has a list of lists) -- the items won't show. Here's the code:

struct WishListView: View {
    @Environment(\.dismiss) var dismiss

    let wishList: WishList

    var body: some View {
        ScrollView {
            List(wishList.items) { i in
                Text(i.name)
            }
        }
        .navigationTitle(wishList.name)
    }
}

struct WishListView_Previews: PreviewProvider {
    static var previews: some View {
        let lists: [WishList] = Bundle.main.decode("lists.json")

        return NavigationView {
            WishListView(
                wishList: lists.first { $0.name == "Preview List"} ??
                WishList(id:UUID().uuidString, name:"Preview list not found")
            )
        }
    }
}

EDIT to clarify: The above code displays a screen with a title ("Preview List") and nothing else -- blank.

The view code is really simple, so I think there must be a problem with how the data is being loaded from the file, but I'm not sure how to debug that -- how do I "see"/inspect an instance of an object?

Here's the model for WishList:

struct WishList: Identifiable, Codable {
    struct Item: Identifiable, Codable {
        var id: String
        let name: String
    }

    var id: String
    var name: String
    var items: [Item]

    init(id: String, name: String) {
        let items = [Item]()
        self.init(id: id, name: name, items: items)
    }

    init(id: String, name: String, items: [Item]) {
        self.id = id
        self.name = name
        self.items = items
    }
}

What am I missing?

1      

If the problem is that nothing is showing in your list detail view, please show some code for the list detail view.

Although I just noticed that the sample JSON you provided above consists of a single WishList but you are asking to decode several WishLists here:

let lists: [WishList] = Bundle.main.decode("lists.json")

1      

I did include the WishList detail code, that's what I posted as struct WishListView: View.

And yes, I decode multiple lists from a single file in the preview decode, and if you look just a couple lines down, you'll see I'm returning a NavigationView that contains a single WishList. Hence why I'm able to display the title of that wish list just fine.

1      

What I think @roosterboy is saying that you are trying to decode an Array from a single element, so either the json should have [ ] at begin/end or call it without the [ ]

let lists: WishList = Bundle.main.decode("lists.json")

Lets tidy it all up a bit Add a file called Bundle-Decodable which has a helper (that Paul uses)

extension Bundle {
    func decode<T: Decodable>(
        _ type: T.Type,
        from file: String,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys
    ) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle")
        }

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = dateDecodingStrategy
        decoder.keyDecodingStrategy = keyDecodingStrategy

        // swiftlint:disable line_length
        do {
            return try decoder.decode(T.self, from: data)
        } catch DecodingError.keyNotFound(let key, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' - \(context.debugDescription)")
        } catch DecodingError.typeMismatch(_, let context) {
            fatalError("Failed to decode \(file) from bundle due to type mismatch - \(context.debugDescription)")
        } catch DecodingError.valueNotFound(let type, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing \(type) value = \(context.debugDescription)")
        } catch DecodingError.dataCorrupted(_) {
            fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON.")
        } catch {
            fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
        }
    }
}

Change the json file

[
    {
        "id": "e644e95f-eb52-4da7-822d-515589767600",
        "name": "Preview List",
        "items": [
            {
                "id": "47a986df-2f45-4558-abd2-de11c91067bf",
                "name": "Cool thing 2"
            },
            {
                "id": "16998a9a-e2fd-4ea4-bdb2-807fae315f6e",
                "name": "Cool thing 3"
            }
        ]
    },
    {
        "id": "e635e95f-eb52-4da7-822d-515589767600",
        "name": "New List",
        "items": [
            {
                "id": "47a997df-2f45-4558-abd2-de11c91067bf",
                "name": "Cool thing 4"
            },
            {
                "id": "16878a9a-e2fd-4ea4-bdb2-807fae315f6e",
                "name": "Cool thing 5"
            }
        ]
    }
]

The Model struct

struct WishList: Identifiable, Codable {
    struct Item: Identifiable, Codable {
        var id: UUID
        let name: String
    }

    var id: UUID
    var name: String
    var items: [Item]
}

Now the ContentView

struct ContentView: View {
    @State private var wishList = Bundle.main.decode([WishList].self, from: "lists.json")

    var body: some View {
        List(wishList) { wish in
            Section(wish.name) {
                ForEach(wish.items) { item in
                    Text(item.name)
                }
            }
        }
    }
}

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

1      

@NigelGee Thanks for trying to help, but I already have it setup that way.

What I think @roosterboy is saying that you are trying to decode an Array from a single element,

No, I'm not. The JSON has an array of lists in it. I only showed the relevant one in my first post in an attempt to keep the post from getting too long. This is not the problem.

I already use that Bundle-Decodable helper, and I already have an array of Wishlists in my file. Removing the brackets from the wishlist load prevents the project from compiling at all, because my JSON is already an array of list objects. And remember, the title of the list is displaying fine. If it wasn't able to load the data, the title wouldn't show. It's the items within the list that won't display.

So for clarity, even though it's going to be really long, here is all the code. This is still exactly what I started with, I haven't made a single change from my initial post that's worked. Starting with my complete JSON file:

[
        {
            "id": "e644e95f-eb52-4da7-822d-515589767641",
            "name": "Empty list",
            "items": [
            ]
        },
        {
            "id": "e644e95f-eb52-4da7-822d-515589767600",
            "name": "Preview List",
            "items": [
                    {
                        "id": "47a986df-2f45-4558-abd2-de11c91067bf",
                        "name": "Cool thing 2"
                    },
                    {
                        "id": "16998a9a-e2fd-4ea4-bdb2-807fae315f6e",
                        "name": "Cool thing 3"
                    }
            ]
        }
]

The complete WishList detail view:

//
//  WishListView.swift
//  WishList

import SwiftUI

struct WishListView: View {
    @Environment(\.dismiss) var dismiss

    let wishList: WishList

    var body: some View {
        ScrollView {
            List(wishList.items) { i in
                Text(i.name)  //THIS IS THE PART THAT'S NOT WORKING! Nothing renders in preview
            }
        }
        .navigationTitle(wishList.name) //This displays fine
    }
}

struct WishListView_Previews: PreviewProvider {
    static var previews: some View {
        let lists: [WishList] = Bundle.main.decode("lists.json")

        return NavigationView {
            WishListView(
                wishList: lists.first { $0.name == "Preview List"} ??
                WishList(id:UUID().uuidString, name:"Preview list not found")
            )
        }
    }
}

The Bundle-Decodable extension I'm using (tinkered with a few different variations):

//
//  Bundle-Decodable.swift
//  Wish List
//
//  Created by Matthew Rogers on 11/8/22.
//

import Foundation

extension Bundle {
    func decode<T: Codable>(_ file: String) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        let formatter = DateFormatter()
        formatter.dateFormat = "y-MM-dd"
        decoder.dateDecodingStrategy = .formatted(formatter)

//        guard let loaded = try? decoder.decode(T.self, from: data) else {
//            fatalError("Failed to decode \(file) from bundle")
//        }

        do {
            let loaded = try decoder.decode(T.self, from: data)
            return loaded
        } catch let DecodingError.dataCorrupted(context) {
            fatalError("Decoding error: \(context)")
        } catch let DecodingError.keyNotFound(key, context) {
            fatalError("Key '\(key)' not found: \(context.debugDescription)")
//            print("codingPath:", context.codingPath)
        } catch let DecodingError.valueNotFound(value, context) {
            fatalError("Value '\(value)' not found: \(context.debugDescription)")
//            print("codingPath:", context.codingPath)
        } catch let DecodingError.typeMismatch(type, context)  {
            fatalError("Type '\(type)' mismatch: \(context.debugDescription)")
//            print("codingPath:", context.codingPath)
        } catch {
            print("error: ", error)
        }

        fatalError("Something weng wrong decoding file: \(file)")
    }
}

It all compiles/runs without error, but the preview shows this:

1      

Take the ScrollView out. List have scroll allready. This will solve the issue!

PS when { something in the naming convention is i is for interger so you should name it singluar of the array eg item

var body: some View {
    List(wishList.items) { item in
        Text(item.name)  
    }
    .navigationTitle(wishList.name) 
}

1      

Take the ScrollView out. List have scroll allready. This will solve the issue!

OMG. Thank you. That did it. facepalm I knew it was probably something simple I was overlooking. I'm just not very familar with SwiftUI behaviors yet. Much thanks.

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.