EdenPolis



FriendFace

Hacking with Swift

Day 60 / 100

Today was a big project, it actually took me two days to finish it but I'm pleased with the results!


It involved:
  • Inspecting JSON file and modeling structs based on my analysis
  • Creating URLSession and fetching JSON from a live server
  • Displaying all users
  • Creating screen with user details
  • Designing the UI



If you want to see my process please keep reading!



Creating FriendFace

Show JSON snippet


                        [
    {
        "id": "eccdf4b8-c9f6-4eeb-8832-28027eb70155",
        "isActive": true,
        "name": "Gale Dyer",
        "age": 28,
        "company": "Cemention",
        "email": "galedyer@cemention.com",
        "address": "652 Gatling Place, Kieler, Arizona, 1705",
        "about": "Laboris ut dolore ullamco officia mollit reprehenderit qui eiusmod anim cillum qui ipsum esse reprehenderit. Deserunt quis consequat ut ex officia aliqua nostrud fugiat Lorem voluptate sunt consequat. Sint exercitation Lorem irure aliquip duis eiusmod enim. Excepteur non deserunt id eiusmod quis ipsum et consequat proident nulla cupidatat tempor aute. Aliquip amet in ut ad ullamco. Eiusmod anim anim officia magna qui exercitation incididunt eu eiusmod irure officia aute enim.",
        "registered": "2014-07-05T04:25:04-01:00",
        "tags": [
            "irure",
            "labore",
            "et",
            "sint",
            "velit",
            "mollit",
            "et"
        ],
        "friends": [
            {
                "id": "1c18ccf0-2647-497b-b7b4-119f982e6292",
                "name": "Daisy Bond"
            },
            {
                "id": "a1ef63f3-0eab-49a8-a13a-e538f6d1c4f9",
                "name": "Tanya Roberson"
            }
        ]
    },]
                    

Inspecting JSON

I began by inspecting the structure of the JSON file. How is the User structured, what proerties it has. How it Friend structured.
Properties for user:
  • id: String
  • isActive: Bool
  • name: String
  • age: Int
  • company: String
  • email:String
  • address: String
  • about: String
  • registered: Date
  • tags: [String]
  • friends: [Friend]
Friend properties:
  • id: String
  • name: String
IMPORTANT: ID of Friend corresponds with the id of the User. I will not be connecting the two in this post.
Day 61 will incorporate SwiftData and I think it will be better to do it there.

Creating User

The process was quite straightforward. I decided to put the Friend struct inside the User to underline the relationship between the two.

I used Codable to be able to crete Swift objects from the JSON file. Identifiable, there's a unique id property. Hashable to allow .navigationDestination to work later.

Every property corresponds to JSON file except for joinedDate. It's easier to use computed property and transform it here. Whenever I need to insert the joineDate I will not have to worry about formatting and my code will be clearer.

Coding keys, the names are matching and I do not need to create init method to decode the JSON file in a custom way. At present the registered property is a Date (in the file it's a string), but we already assigned an empty Date insatnce and will later fill it with decoded date. See section below on fetching the data.

User.swift


                        import Foundation

struct User: Codable, Identifiable, Hashable {
    
    struct Friend: Codable, Identifiable, Hashable {
        var id: String
        var name: String

        enum CodingKeys: String, CodingKey {
            case id
            case name
        }
    }
    
    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 = Date()
    var joinedDate: String {
        registered.formatted(date: .numeric, time: .omitted)
    }
    var tags: [String] = []
    var friends: [Friend] = []
    
    enum CodingKeys: String, CodingKey {
        case id, isActive, name, age, company, email, address, about, registered, tags, friends
    }
    
}
                    

loadData()


                        func loadData() async {

            // get the URL
            guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
            print("Invalid URL")
            return
            }
            
            // fetch the data
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
            // decode data if fetched correctly
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .iso8601
                if let decodedUsers = try? decoder.decode([User].self, from: data) {
                    users = decodedUsers
                }
            } catch {
                print("Invalid data")
            }
        }
                    

Fetching data

I admit that I went into this step quite confidently but got stuck for a bit.

We must use async keyword since data from the internet might take a bit of time to load or user might simply not have internet connection. This ensures the whole app doesn't crash while we wait for this data.

Setting url, getting the data, setting the decoder was straightforward.

I applied .dateDecodingStrategy = .iso8601 this will allows us to assign Date of this format to registered.

Another hickup was what to decode, I initially tried to decode User.self. That was incorrect, it's true that inside JSON there's a User, another User and so on, but overall file is an array of User(s), and my code needs to reflect that.

To check if it's working correctly I created @State private var users = [User]() inside the ContentView() where the loadData() was placed. I made a List for users and placed a Text(user.name) to see if I get anything from my call.

Now, with this in place we can use the loadData() method on List element. .onAppear{loadData()} will not work in this scenario. onApper accepts only synchronous function. That's why we must use .task {await loadData()}, the await keyword is an important element, letting Swift know that this function might sleep.


Users View + UI Design

That's where the fun begins. I wanted to create a separate view displaying all users rather than have it all in ContentView. My reason is quite simple, once I will be using SwiftData in day 61, I want to be able to add functionality to filter users based on isActive property and allow to sort them by date or name. Creating a separate screen allows me to get all the users in ContentView and pass only the correct ones to the dedicated UsersView based on enduser selection.

When it comes to design, I navigated to UsersView and created two random users in the preview. Then, I began looking for inspiration. I found a beautiful background by Josh Withers on Unspash. Once I had the greenish background I went with beige and cool green/beige as another accent tone.

When I comes to layout I wanted it to be flexible, to look stunning both in landscape and portrait. That's why I used LazyVGrid.

I looped over my users and created a VStack with systemImage, that changes based on isActive proeprty, name and age. I left out other properties to not clutter the space with non-essential information.

To make it more obvious where each user ends and where another begins I added a frame with a bit of padding, clipShape (to round the corners), added a bit of shadow for definition and overlay for a subtle border. When adding a background color it's important to use clipShape afterwards, otherwise it will have default square corners and look off.

Another important visual rule is to add .padding(.horizontal) to all the elements inside this view. This will create a small gap between the edges of the phone and your view.

To allow this to work correctly my ContentView consists of NavigationStack, ScrollView, inside UsersView(users: users) with .task described above.


UserView + UI Design

Here is the UserView to display detailed user information.

I used .navigationTitle(user.name) to make it the biggest and most prominent text on the screen, who's profile are you currently look at.

I decided that for visual clarity it's important to add a divider (simple Rectangle), that helps to guide the eyes to different parts of the screen. For even more ease of reading I added .background(.ultraThinMaterial), this ensure maximum readibility on smaller text elements, since the background I used has varied tones and can be a bit distracting.

To ensure this screen is accessible from ContentView I create a @State private var path = [User](), this will allow me to navigate between different screens. Then I added .navigationDestination(for: User.self) { user in UserView(user: user)}, next step is to wrap the VStack created in UsersView in NavigationLink(value: user).

With this code in place the app is completed! It was a fun challenge and I'm very happy with the design. I think it's a step up from my previous work.