WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Day 60 - Problems formatting or downloading JSON - SOLVED

Forums > 100 Days of SwiftUI

@CJ  

Hi - I'm trying to create the 'Friendface' app on day 60 but cannot seem to pull the JSON data into the app. I used an extension for downloading JSON from Paul's tutorial 'How to Download JSON from the internet....' but although I managed to get it to work with his example struct, I can't with mine. I am assuming there is something wrong with how I've created it? I have tried so many versions: 2 separate structs, nesting one inside the other, used Identifiable, removed identifiable. I just don't know anymore. I may just be completely on the wrong track, I've never tried downloading JSON so just don't where to start.

In terms of the contentView, I've only just created a button to test whether the data will pull through (it won't) and haven't gone any further than that.. Any guidance would be appreciated.

Thanks

extension URLSession {
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

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

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

struct User: Codable, Identifiable {
  let id: UUID
  let isActive: Bool
  let name: String
  let age: Int
  let company: String
  let email: String
  let address: String
  let about: String
  let registered: Date?
  let tags: [String]
  let friends: [FriendData]
  var formattedDate: String {
    registered?.formatted(date: .abbreviated, time: .shortened) ?? "N/A"
  }
  struct FriendData
  : Codable, Identifiable {
    let id: UUID
    let name: String

  }

}

struct ContentView: View {

  //function to decode JSON

  func decode() async {
    do {
      let url1 = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
      let user = try await URLSession.shared.decode(User.self, from: url1)
      print("Downloaded \(user.age)")

    } catch {
      print("Download error: \(error.localizedDescription)")
    }
  }

    var body: some View {
      VStack {
        Button("decode") {
          Task {
            await decode()
          }
        }

      }

    }
}

2      

Hi, There are 3 problems i found, 1- the Type of the decoded user needs to be an array of users since it is what the URL returns.

      let user = try await URLSession.shared.decode([User].self, from: url1)

2- the registered property in the User struct needs to be a String.

3- Since the registered property is a string the formattedDate wont work any more so you need to remove it.

struct User: Codable, Identifiable {
  let id: UUID
  let isActive: Bool
  let name: String
  let age: Int
  let company: String
  let email: String
  let address: String
  let about: String
  let registered: String
  let tags: [String]
  let friends: [FriendData]

  struct FriendData
  : Codable, Identifiable {
    let id: UUID
    let name: String

  }

}

Hope this helps.

2      

OK few thing need to change. Need a array to put the data in

@State private var users = [User]()

Then in the method add dateDecodingStrategy of .iso8601 and change the let user to users = ... and the type to be [User].self not User.self

func decode() async {
  do {
      let url1 = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
      users = try await URLSession.shared.decode([User].self, from: url1, dateDecodingStrategy: .iso8601)
  } catch {
      print("Download error: \(error.localizedDescription)")
  }
}

I change the body to show a list of users

var body: some View {
    List {
        ForEach(users) { user in
            Text(user.name)
        }
    }
    .task { await decode() }
}

The whole file

struct ContentView: View {
    @State private var users = [User]()

    var body: some View {
        List {
            ForEach(users) { user in
                Text(user.name)
            }
        }
        .task { await decode() }
    }

    func decode() async {
        do {
            let url1 = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
            users = try await URLSession.shared.decode([User].self, from: url1, dateDecodingStrategy: .iso8601)
        } catch {
            print("Download error: \(error.localizedDescription)")
        }
    }
}

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

struct User: Codable, Identifiable {
    let id: UUID
    let isActive: Bool
    let name: String
    let age: Int
    let company: String
    let email: String
    let address: String
    let about: String
    let registered: Date?
    let tags: [String]
    let friends: [FriendData]

    var formattedDate: String {
        registered?.formatted(date: .abbreviated, time: .shortened) ?? "N/A"
    }
    struct FriendData: Codable, Identifiable {
        let id: UUID
        let name: String

    }
}

extension URLSession {
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

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

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

PS you made to date optional but there is no need as every one has a registered field

struct User: Codable, Identifiable {
    let id: UUID
    let isActive: Bool
    let name: String
    let age: Int
    let company: String
    let email: String
    let address: String
    let about: String
    let registered: Date
    let tags: [String]
    let friends: [FriendData]

    var formattedDate: String {
        registered.formatted(date: .abbreviated, time: .shortened)
    }
    struct FriendData: Codable, Identifiable {
        let id: UUID
        let name: String

    }
}

3      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

@CJ  

Brilliant! -Thanks @Hectorcrdna and @NigelGee - Of course it is an array -that makes sense now!

2      

@CJ  

Thank you @Obelix and all

Unfortunately, still having some issues with accessing/formatting the data-

although I have managed to pull the JSON data through I seem now to have hit another wall with formatting. The big problem I'm having is with arrays. I don't know either how to iterate over the tags array or access the friends array (which itself is made up of values from a nested struct). Separately, I tried to create a struct for 'friends' but then I could not get this to work either!

The syntax below is wrong but hopefully you can see what I am attempting. Obviously, I don't want this ultimately to appear in content view, I'm just working out how to access all data.


//display each element within each tag array

 ForEach( users.tags, id:\.self) { tag in
        Text(tags)
      }

The other problem is trying to access the friends array which is possibly even more complicated as it is made up of a another struct. I have tried to access/format this both from the nested struct (as in original code above) and also creating a separate one below:

struct FriendData: Codable, Identifiable {
    let id: UUID
    let name: String
}

struct Friends: Codable{
  let friends: [FriendData]
}

This one was presenting problems in two ways. If I use as below -I get error messages -'Friends' required to conform to hashable but I cannot add the hashable protocol without getting another error message about not conforming to equitable!


@State private var friends = [Friends]()

 ForEach( friends, id:\.self) { friend in
        Text("\(friends.id)")

If I use the other struct I get a result but it is not the friends data it is the id and name of the 'user':


@State private var friendData = [FriendData]()

 ForEach(friendData) { friend in
        Text("\(friend.id)")
      }

This of course makes sense- as name and id appear twice within the JSON data, so I understand Swift needs to know which id or name I mean. Somehow I need to direct it first to 'friends' and then to id and name - but how? That I cannot work out.

Apologies for the long post. Any help, further reading, pointers to previous lessons would be helpful. I have tried looking at Moonshot but can't quite get the answers I need.

Thanks

2      

There are a number of ways you can do it depending on what you want to display.

Make a computed property in the User struct this will now make the tags array one string(formatted eg add and before the last item)

var tagsList: String {
    self.tags.formatted()
}

Then you can add in the

Text(user.tagsList)
        .foregroundStyle(.secondary)

or use a another ForEach in the first for each

 ForEach(user.friends) { friend in
    Text(friend.name)
        .foregroundColor(.red)
}

So now the body look like this (from previous answer)

struct ContentView: View {
    @State private var users = [User]()

    var body: some View {
        List {
            ForEach(users) { user in
                VStack(alignment: .leading) {
                    Text(user.name)

                    ForEach(user.friends) { friend in // go over the each in array of friends
                        Text(friend.name)
                            .foregroundColor(.red)
                    }

                    Text(user.tagsList) // from user struct
                        .foregroundStyle(.secondary)
                }
            }
        }
        .task { await decode() }
    }

    func decode() async {
        do {
            let url1 = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
            users = try await URLSession.shared.decode([User].self, from: url1, dateDecodingStrategy: .iso8601)
        } catch {
            print("Download error: \(error.localizedDescription)")
        }
    }
}

User struct

struct User: Codable, Identifiable {
    let id: UUID
    let isActive: Bool
    let name: String
    let age: Int
    let company: String
    let email: String
    let address: String
    let about: String
    let registered: Date
    let tags: [String]
    let friends: [FriendData]

    var formattedDate: String {
        registered.formatted(date: .abbreviated, time: .shortened)
    }

    var tagsList: String { // give a formatted string
        self.tags.formatted()
    }

    struct FriendData: Codable, Identifiable {
        let id: UUID
        let name: String

    }
}

2      

@CJ identifies the very lesson @twoStraws wants his students to learn with this Day 60 project! Namely: Data you get from internet sources may contain many rows. And each of those rows of data may, in itself, also contain many rows of data.

Your job as a SwiftUI programmer is to take the internet's data (thank you very much, sir) and consume it into your own application. But once it's in your application, it's YOUR data now. Slice and dice, extract and fold, crumple and dump it into any form you wish to meet your application's needs. @Nigel (above) gives a few examples how to slice and dice your data. Pay attention to Nigel!

It's hard!

But this is more of a logic puzzle than a SwiftUI lesson.

Post-It Notes

Not sure if you're a visual learner, but sometimes a white-board and coloured markers, or a few packs of Post-It notes can help. Or good old pen and paper, if you're old school.

Lay out a few yellow post it notes on your desk, each with the name of a friend. Top-to-bottom you have

[ Adam Zapple    ]      
[ Otto Partz     ]      
[ Mary Otchiband ]       
[ Paula Texx     ]     
[ Bess Tinclass  ]     

To get these names to display on your iPhone app, you picked the right technique: ForEach view builders to loop over each Friend object in your Friends array.

Then next to each friend Post-It, lay out a few blue Post-Its left-to-right, with their tags.

[ Adam Zapple    ][ Gym ][ Carpool ][ Ski Club ]    
[ Otto Partz     ][ HWS+][ Book Club ]      
[ Mary Otchiband ][ HWS+ ][ Gym ]       
[ Paula Texx     ][ Dog Park ][ Volksmarch Club ]     
[ Bess Tinclass  ][ Ski Club ][ Jazz Group ][ Book Club ]     

You're already listing each Friend using a ForEach view factory. How do you layout each Friend's tags?
As @Nigel points out above, you need to use another ForEach view factory.

What's a View Factory?

To help me adopt SwiftUI's declarative nature, I had to stop thinking of ForEach as a looping mechanism. I kept thinking of it as a procedural syntax. Instead, I started thinking "What is the purpose of the ForEach syntax? What works for me it to think the ForEach structure's purpose is to build a number of Views for each item in a collection. So, to me, it became a View Factory.

See -> View Factory
or See -> View Factory
or See -> View Factory

Keep Coding!

2      

@CJ  

Thank you @NigelGee and @Obelix

The computed property idea was helpful -I forget all the things you can do within a struct and use elsewhere in the program.

@Obelix - thank you for your explanation - I'll be honest - I'm still completely stumped by how to access the data in the friends array. I'm not sure whether this was clear or not from my post but, from a technical point of view, I have no idea how to proceed. Currently I can only show data from the 'User' Struct and nothing from the 'Friends'/'FriendsData struct. I have tried nesting the FriendsData both within the User struct and also creating it separately but whatever I do I hit a problem as mentioned above. I just don't know how to find a path to the data. To follow on from your example, I feel that I would love to lay out the post it notes but they seem to be locked in the draw and I can't find the key to get them!

2      

Have you tried putting the code that I did in a project?

User is an array and FriendsData is an array inside the User So to get one User

ForEach(users) { user in //<- one user

So to get one Friend

ForEach(users) { user in
  ForEach(user.friends) { friend in //<- one friend

The only thing in this is the id

Add this to the struct User

static let errorUser = User(id: UUID(), isActive: false, name: "Unable to find friend", age: 0, company: "", email: "", address: "", about: "", registered: Date.now, tags: [], friends: [])

Are you trying to get the details of this one friend?

ContentView

var body: some View {
    NavigationStack {
        List {
            ForEach(users) { user in
                NavigationLink {
                    UserDetailView(users: users, user: user)
                } label: {
                    VStack(alignment: .leading) {
                        Text(user.name)
                    }
                }

            }
        }
    }
    .task { await decode() }
}

UserDetailView

struct UserDetailView: View {
    let users: [User]
    let user: User

    var body: some View {
        VStack(alignment: .leading) {
            Text(user.name)
                .font(.title)
                .padding()

            Section {
                List(user.friends) { friend in
                    let friendDetail = users.first { $0.id == friend.id } ?? .errorUser // <- in case no matches
                    NavigationLink {
                        FriendDetailView(friend: friendDetail)
                    } label: {
                        Text(friend.name)
                    }
                }
                .listStyle(.plain)
            }
        }
    }
}

Friend Detail View

struct FriendDetailView: View {
    let friend: User

    var body: some View {
        VStack {
            Text(friend.name)
                .font(.headline)
            Text(friend.company)
                .foregroundStyle(.secondary)

            Text(friend.about)
                .multilineTextAlignment(.leading)
                .padding()

            Spacer()
        }
    }
}

2      

@Nigel provides a GREAT example.

Here's another that I worked on.

// Simplified for the forum.
// By the way, you're not REQUIRED to decode everything from
// a JSON file. Just grab what you need for testing.
struct Person: Codable, Identifiable {
    let id:   String  // <- String! NOT UUID
    let name: String
    // Declare that each person has an array of Friends
    // Loop over this array to display a person's friends
    let friends: [FriendData]

    // Lucky for us everything here is decodable.
    struct FriendData: Codable, Identifiable {
        let id:   String // <- String. NOT UUID.
        let name: String
    }
}

// Show Details for ONE person
struct PersonView: View {
    let personToShow: Person // provide ONE person object

    var body: some View {
        VStack(alignment: .leading) {
            Text("\(personToShow.name) has \(personToShow.friends.count) friends")
            VStack(alignment: .leading) {
                // View Factory!
                // Build a Text view for each friend in a Person's friend array
                ForEach( personToShow.friends ) { aFriend in Text("👤 \(aFriend.name)") }
            }
        }
    }
}

struct FriendsView: View {
    @State private var personArray = [Person]() // empty to start!
    let friendsURL = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!

    var body: some View {
        VStack {
            HStack {
                Button("Get Data") { Task { await decode() } }.buttonStyle(.borderedProminent)    // load the list
                Button("Clear List") { personArray.removeAll() }.buttonStyle(.borderedProminent)  // remove everyone from the list
            }
            Divider()
            Text("You downloaded: \(personArray.count) people")
            // List all the people you downloaded sorting by first name
            List(personArray.sorted(by: {$0.name < $1.name})) { someone in
                PersonView(personToShow: someone)
            }
        }
    }

    // utility function
    func decode() async {
        do {
            // It may take some time, but eventually
            // the personArray will get populated
            // with a bunch of names
            personArray = try await URLSession.shared.decode([Person].self, from: friendsURL)
        } catch { print("Download error: \(error.localizedDescription)") }
    }
}

2      

Why @Obelix have you made the id: String when they are UUIDs?

2      

@CJ  

Thanks @NigelGee - To answer your question - yes, I was trying to get the data to appear in content view as a test to make sure I could access it. I won't put it there ultimately. I was actually thinking at one point of a ForEach within a ForEach but I had just never seen that done so didn't realise it was possible and so then started going round the houses and getting nowhere. I had to format the id as a string but that now works too. I hadn't got as far as making links to other views yet but many thanks for the examples given.

2      

@CJ  

Thanks @Obelix - I'm going to look at this a bit more in depth to fully understand what is going on - It is helpful to see different ways of accessing/showing the data.

Regarding the ID - I did keep it as UUID but then added this computed property to the struct in order to show in text.

 var idFormatted: String {
      id.hashValue.formatted()
    }

2      

If you need the UUID as a string just add .uuidString

Text(friend.id.uuidString)

2      

@CJ  

Thanks, yes, that's better - my solution was changing UUID to a different value - I have over-complicated this anyway I think as I could just use string interpolation.

2      

@CJ  

Sorry for the late addition to the thread but I have revisited this over a number of days and gradually trying to get to grips with it. One thing I am unclear about from the above example @NigelGee is the link to the DetailView and exactly what is going on in the code below - what are all the user/users relating to?- there are declarations of user/users in both contentview and detail view so it is hard to make out what is referring to what. Paul did something similar in Moonshot and I replayed the tutorial but he doesn't clarify why.

  DetailView (users: users, user: user)

Also why is users declared as a private var in contentView but as a constant in detail view?

Thanks again for your help

2      

users: users

In the User array you have Friends with only id and name however in the UserDetailView created a link to get all the details of friends but needed to use the orginal User struct to get that so passed in the array to this view so it can filter those details to pass to FriendsView [^1]

user: user

The user is the user that will be displayed in UserDetailView

Also why is users declared as a private var in contentView but as a constant in detail view?

In UserDetailView are property that are not mutating but passed from ContentView. In ContentView - users start as an empty array then is "filled" by the json call so need to be mutating (@State private var) [^2]

[^1]: This can be done with new NavagationStack with iOS 16 (i am still trying to get my head around it!)

[^2]: A whole different discussion on if it should be private if you pass it on to a another view!

2      

@CJ  

Thanks @NigelGee - This is helpful. I ended up changing the names of the properties to help me understand which part of the code was referencing which view/property. I get the idea, I think, that the code in Navigation link is like a conduit between two views, pulling through data and matching it up the other end. I won't worry too much about the specifics of properties at this stage, as long as they work, that's the main thing!

Thanks again for your help - much appreciated.

2      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.