The Day 60 Milestone is a demo app that vacuums up some JSON and displays it in a list in a NavigationView that links to a details page. Nothing super strenuous, the steps were something like this:

import Foundation
struct User: Codable {
var id: String
var isActive: Bool
var name: String
var age: Int
var company: String
var email: String
var address: String
var about: String
var registered: Date
var tags: [String]
var friends: [Friend]
}
struct Friend: Codable {
var id: String
var name: String
}
The date you can see in the JSON is in the ISO-8601 format - and @twostraws gives the hint about using the dateDecodingStrategy for it.
.task {
if users.isEmpty {
await fetchUsers()
}
}
func fetchUsers() async {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
print("Invalid URL")
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let (data, _) = try await URLSession.shared.data(from: url)
do {
let decodedUsers = try decoder.decode([User].self, from: data)
users = decodedUsers
} catch {
print(error)
return
}
} catch {
print(error)
return
}
}
Note the square brackets for [User] - we’re decoding an array of users.
struct ContentView: View {
@State private var users = [User]()
var body: some View {
NavigationView {
List(users, id: \.id) { user in
NavigationLink(destination: UserDetail(user: user)) {
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.isActive ? "Active" : "Not active")
}
}
}
.task {
if users.isEmpty {
await fetchUsers()
}
}
.navigationBarTitle("FriendFace")
}
}
The script departure was wanting to have a profile pic. As described yesterday, I used an AsyncImage for this. I would have liked to have cached the image so it doesn’t re-fetch when you go out of a user and back in (this would have solved the problem of the random image I described yesterday as well) and fiddled around with trying to save a .snapshot of the AsyncImage view - but then decided I should be moving on instead of cracking that particular procrastination nut, especially because of this parting advice from Paul.
Tip: As always, the best way to solve this challenge is to keep it simple – write as little code as you can to solve the challenge, and for you to feel comfortable that it works well.
struct UserDetail: View {
let user: User
var body: some View {
Form {
Section {
HStack {
Text(user.name)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
}
}
AsyncImage(
url: URL(string: "https://randomuser.me/api/portraits/men/\(nameHash).jpg"),
scale: 3
) { image in image
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
Section {
Text("Age: \(user.age)")
Text("Company: \(user.company)")
Text("email: \(user.email)")
Text("Address: \(user.address)")
Text("Registered: \(user.registered, style: .date)")
}
Section(header: Text("Friends")) {
List(user.friends, id: \.id) { friend in
Text(friend.name)
}
}
}
}
var nameHash: Int {
user.name.utf8.reduce(0) { $0 + Int($1) } % 100
}
}