Last Thursday I officially joined Apple’s App Developer Program so although I’m anxious to start rolling out some of the apps that I’ve started putting together for now I’m still developing my skills.
I’ve been learning iOS app development through Paul Hudson’s wonderful 100 Days of SwiftUI and I just finished the Day 60 Challenge — Friend Face. I’m posting my work here in order to share my process and solidify my thinking.
I started by identifying the fields I would need from the JSON sample data. As you can see, the fields for each user include:
- id
- isActive
- name
- age
- company
- address
- about
- registered
- tags
- friends
The “tags” and “friends” fields are both embedded in arrays and I’ve incorporated these into the model. I’ve also added a closure that makes a computed property which formats the date. Even though friends are included as an array in the User struct, I made a separate struct to define a single Friend. This could have been made into a separate file for organization’s sake but it’s small so I just kept them together.
// | |
// User.swift | |
// Friend Face | |
// | |
// Created by Jeff Milner on 2025-04-11. | |
// | |
import Foundation | |
struct User: Codable, Identifiable { | |
var id: UUID | |
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] | |
var formattedDate: String { | |
registered.formatted(date: .abbreviated, time: .omitted) | |
} | |
} | |
struct Friend: Codable, Identifiable { | |
var id: UUID | |
var name: String | |
} | |
// this holds the JSON data | |
class UsersData: ObservableObject { | |
@Published var users = [User]() | |
} |
I wanted to be able to click on a given user’s friends and have the friend profile open up in a new view but I needed to be able to access the list of users on more than one view so I used the @EnvironmentObject variable to access the data from both ContentView and UserDetailView.
Friend Face – ContentView.swift
// | |
// ContentView.swift | |
// FriendFace | |
// | |
// Created by Jeff Milner on 2025-04-11. | |
// | |
import SwiftUI | |
struct ContentView: View { | |
// Environment Object allows data to be shared with multiple views. Add the @EnvironmentObject var to each view that needs access. In this case the variable holds all the JSON data so that the user list can be accessed inside of UserDetailView. | |
// | |
// More on EnvironmentObject: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views | |
@EnvironmentObject var usersData: UsersData | |
var body: some View { | |
NavigationView { | |
//Loop through users in a list | |
List(usersData.users) { user in | |
NavigationLink { | |
//each user gets a link to UserDetailView with their details | |
UserDetailView(user: user) | |
} label: { | |
HStack { | |
//Using nil coalescing show a green graphic if user is active and gray if not. | |
Image(systemName: "person.circle") | |
.foregroundColor(user.isActive ? .green : .gray) | |
} | |
//In each iteration through the list show user's name | |
Text(user.name) | |
} | |
} | |
//NavigationTitle must be after a view but inside the NavigationView's {}. | |
.navigationTitle("Friend Face") | |
} | |
//This task will allow the views to continue while loading the data. | |
// Details at: https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-codable-data-with-urlsession-and-swiftui | |
.task { | |
if let retrievedUsers = await getUsers() { | |
usersData.users = retrievedUsers | |
} | |
} | |
} | |
// This function connects to the web to get the JSON data and puts it in an optional array of the type "User" (from struct in User.swift). There are three steps: | |
func getUsers() async -> [User]? { | |
// Step 1. set the url to the location of the JSON | |
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else { | |
print ("Invalid URL") | |
return nil | |
} | |
// Step 2. Fetch the data from the URL using Swift's built in URLRequest | |
var request = URLRequest(url: url) | |
request.httpMethod = "GET" | |
request.addValue("application/json", forHTTPHeaderField: "Content-Type") | |
let decoder = JSONDecoder() | |
decoder.dateDecodingStrategy = .iso8601 | |
do { | |
// this will return a tuple with JSON data as well meta data that is discarded because of the underscore (we don't need it) . | |
let (data, _) = try await URLSession.shared.data(for: request) | |
// Step 3. Decode the data into decodedData struct | |
if let decodedData = try? decoder.decode([User].self, from: data) { | |
return decodedData | |
} | |
} catch { | |
// if the data value above is not able to be set for any reason print the error | |
print("Invalid data") | |
} | |
return nil | |
} | |
} |
I used something like Text(“user.[field]”) for each of the fields except for user.registered which I substituted in the computed value that is stored in user.formattedDate so that I would get a date which is easier to read.
Friend Face – UserDetailView.swift
// | |
// UserDetailView.swift | |
// Friend Face | |
// | |
// Created by Jeff Milner on 2025-04-11. | |
// | |
import SwiftUI | |
struct UserDetailView: View { | |
let user: User | |
//this @EnvironmentObject allows access to the users list that was pulled earlier | |
@EnvironmentObject var usersData: UsersData | |
var body: some View { | |
List { | |
Section("User Details") { | |
Text(user.name) | |
.font(.headline) | |
Text("Registered: \(user.formattedDate)") | |
Text("Age: \(user.age)") | |
Text("Email: \(user.email)") | |
Text("Address: \(user.address)") | |
Text("Works for: \(user.company)") | |
} | |
Section("About") { | |
Text(user.about) | |
} | |
Section("Friends") { | |
ForEach(user.friends) { friend in | |
NavigationLink { | |
// goes through the list of users and the first time friend.id == user.id (written as $0.id) set friendUser as the user, and open friendUser in a new copy of the view. | |
if let friendUser = usersData.users.first(where: { $0.id == friend.id }) { | |
UserDetailView(user: friendUser) // Recursive view | |
} else { | |
Text("Friend not found") | |
} | |
} label: { | |
Text(friend.name) | |
} | |
} | |
} | |
} | |
.navigationTitle(user.name) | |
} | |
} | |
#Preview { | |
let mockData = UsersData() | |
let exampleUser = User( | |
id: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!, | |
isActive: true, | |
name: "Jenny Doe", | |
age: 29, | |
company: "Acme Inc.", | |
email: "jenny@example.com", | |
address: "123 Main St", | |
about: "Loves coding", | |
registered: .now, | |
tags: ["swift"], | |
friends: [ | |
Friend(id: UUID(uuidString: "D621E1F8-C36C-495A-93FC-0C247A3E6E5A")!, name: "Joe Somebody"), | |
Friend(id: UUID(uuidString: "F621E1F8-C36C-495A-93FC-0C247A3E6E5B")!, name: "Andrea Doe") | |
] | |
) | |
mockData.users = [exampleUser] | |
return UserDetailView(user: exampleUser) | |
.environmentObject(mockData) | |
} |
Friend Face – Friend_FaceApp.swift
// | |
// Friend_FaceApp.swift | |
// Friend Face | |
// | |
// Created by Jeff Milner on 2025-04-11. | |
// | |
import SwiftUI | |
@main | |
struct Friend_FaceApp: App { | |
@StateObject private var usersData = UsersData() | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
.environmentObject(usersData) | |
} | |
} | |
} |
And with that I have a working app that runs off of live data from the Internet.
I’ve uploaded the code for Day 60 on GitHub.