Solving “FriendFace” 100DaysOfSwiftUI Challenge (Day 60, 61)
I’ve been having a lot of fun learning about SwiftUI following 100DaysOfSwiftUI by @twostraws of Hacking With Swift so I thought sharing my process of figuring out this particular challenge would be fun as well.
NOTE: I am not a pro at this, I’ve just been following the course and wanted to share my progress, maybe get some tips from other students on how to make it better or help someone else struggling to make this app work.
The challenge
At this part of the course, we’re supposed know about networking and Core Data to successfully finish the task. To begin, we have a URL to get json with some dummy data we need to parse and display in the UI.
The second part is to use Core Data to save the results of the network call so the app can be used offline.
Details of the challenge
- Part 1 — Getting JSON from the web and displaying it
- Part 2 — Saving the retrieved data using Core Data
Before we start, this is the end product you can find on my GitHub Repo:
Creating the models
Let’s create a new project in Xcode and begin the work.
If you’ve been following the Hacking With Swift course on SwiftUI, you will know that you don’t want to tick the Core Data box when creating a new project for this challenge because later we will do our own configuration for that so I left the checkbox unchecked like in the screenshot below:
Now that we have our project ready we can check out the json provided by Paul in his challenge. There are lots of tools for viewing json so that it makes sense to the human eye. For example there’s an extension for Google Chrome called “JSON Formatter” that works well for me. You can choose whichever tool you like as long as it makes it easy to see the structure of the json and property types. If you have good suggestions for tools like this, hit me up on twitter @viktormauzer or comment below.
In this one we see that there is an array of users:
JSON Formatter helpfully tells us that there is an array of 100 items and those will be our “Users”. If we expand this array we get this:
This means that we have an array of 100 objects and each of those objects has properties, one of those properties is an array of strings and there is also a property that is an array of friends which is an object itself with its own properties.
This part can get tricky because data structures can be very deep with some json files and it is very important to carefully look over the data and see what you’re dealing with before writing any code.
That said, we are ready to make our data models.
Go back to your Xcode project and create a new file by pressing Command + N and choose just a normal Swift File template like this:
This is going to be our “User” data model where we will be creating a struct that will mimic our User object from json. That is why the User struct must conform to the “Codable” protocol and in this particular case it will conform to the “Identifiable” protocol too because it has an “id” property so we can use ForEach later in code to display all users in a list.
A couple of things to say about this code:
- the “registered” property is set as a “Date” type we will configure later
- we have a special computed property “formattedDate” which will be used to display date in the UI
- the “friends” property is an array of “Friend”, and to make the code compile we need another file just like before so Command + N, swift file template and another struct called “Friend” like this:
I like to separate these into their own files for organisation. Make sure to carefully name these properties because they match the property names in the JSON.
Part One — Making a network call using async/await
For this part we will go to the ContentView.swift file and refactor later. My approach was to create a function that does the networking and returns an array of users, then assign the result to the state variable I called “users”. This is the code snippet that shows all users’ names in a list:
A few things to consider:
- getUsers() function is marked “async” because retrieving data might take some time and it is called in the .task {} modifier on the List itself
- the url is force unwrapped because the app would not work anyway if it isn’t 100% correct so we make sure it is since it’s hard-coded
- httpMethod is “GET” because we are retrieving data from the internet
- dateDecodingStrategy is set to .iso8601 because that is the format in the JSON and also Paul actually says to use it in the challenge tips
- getUsers() function returns an optional because there is a possibility of an error while parsing the JSON or processing the request. Extensive error handling is not going to be covered in this post since this is only a practice app for now
- I am ignoring the SwiftUI Preview here because of the networking and just using the simulator to view the app
User details
This is nothing we haven’t seen in this course so far a few times so I’ll be quick. We create a new SwiftUI View file called “DetailView”:
Here is where we will load more data about each user and user a navigation link to display the DetailView. Here is a very simple DetailView:
We have a “user” property of a type “User” which we pass through from the ContentView when we link to DetailView from the NavigationLink. This is the body of ContentView after this change:
This is the networking part done! :) First part of this challenge is over. Of course you can take it to another level with fancy UI and more details of each user etc, but for now, we will continue with the second part which is all to do with Core Data.
Part Two — Core Data
To use Core Data in this project, first we need to create a new Core Data Model so use Command + N and look for Core Data -> Data Model and name it “FriendFace”:
Next, we need to create two entities
- CachedUser
- CachedFriend
Let’s analyse this screenshot by numbered items:
- To create an Entity, press “Add Entity”
- Rename Entities accordingly (CachedUser and CachedFriend) because we want to keep our structs “Friend” and “User”
- Add all the properties you intend to store in persistent storage with appropriate types: (note: age in this case is: Integer16 which we will address later in code)
- Create relationship for both entities — the user will have a relationship called “friends” with a destination “CachedFriend” and inverse “user” which comes from the relationship on “CachedFriend” which is “user”, destination “CachedUser” and inverse “friends”
- “CachedUser” will have a type: “To Many” and “CachedFriend” will have a type: “To One”
After that, select both entities and go to Editor->Create NSManagedObject Subclass…
Click next a couple of times and make sure both entities are ticked and that you select your project folder when asked where to save the files.
This should create 4 files for you.
We needed this approach because we are going to create some computed properties to handle optionals and most importantly the NSSet of “friends” which we need to convert to Set so we can use it in SwiftUI
First, open the file called: CachedFriend+CoreDataProperties.swift and there will be your CachedFriend extension with properties id, name and user as a relationship. This is where we want to get rid of optionals by making computed properties (in this case just for the name) so we end up with the extension that looks like this:
Only one addition to the template here and this is the computed property: wrappedName. We just did nil coalescing to “Unknown”
Next, open the file called: CachedUser+CoreDataProperties.swift which has the other extension for CachedUser. We will do the same with optional properties here as well:
Similar thing, more properties. However, the last one is public var friendsArray which takes the NSSet, does type casting to Set<CachedFriend>. Then it sorts it and returns it so you have a public property “friendsArray” you can use in ForEach in SwiftUI. It’s a bit of jumping through hoops it seems, but it is worth it.
Next, do Command + N and create a new empty Swift File and call it DataController.swift
Then create a class called DataController and make sure it is an ObservableObject and that you don’t forget to import CoreData as well. This is where we’ll make an initialiser the persistent container like this:
The name of the persistent container is the name of your CoreData model file, in this case “FriendFace”.
Now we need to initialise the Data Controller and to do that, go to the FriendFaceApp.swift (the file generated automatically when you create a SwiftUI Xcode project) and initialise it like this:
We use StateObject property wrapper to initialise the data controller and then put it in the environment so we can use it in views later.
Now we’re ready to start using CoreData in this project.
Saving retrieved data to the database using CoreData
The only thing left to do is to fix our ContentView and our DetailView so that they load data from persistent storage, rather than from the internet every time.
First, we need to get the managed object context from the environment and make a fetch request to have it ready for use when saving and retrieving data from the database.
Next, add logic to the .task {} modifier for saving data and change the list to iterate over cachedUsers rather than just users fetched from the internet. Also, we need to change property names to wrapped property names to avoid optionals (that’s why we did all that work with the extensions before)
Now, your entire ContentView can look like this:
The main job is done in the .task {} modifier. First thing to note is we put everything inside a condition “cachedUsers.isEmpty” to not do all the work again every time the list is rendered.
Then we loop through the user array and create a new cachedUser each time the loop goes around populating its fields (this is where we use the Int16 to match the type in CoreData for the “age” property). We also have a loop within a loop to do the same for each users’ friends. To make the relationship work, for each friend we set the user property to the “newUser” created in each outer loop iteration. At last, we save using try? moc.save().
You might notice the await MainActor.run {} where all this CoreData work is done. This is the bit I struggled on. I hope it’s done properly, since the app works, but if you have more experience using MainActor, I would love feedback on this. Feel free to comment or tweet me @viktormauzer.
One more thing to do…
We need to fix the DetailView to show correct data. This is how it looks like now:
Changes are simple — the user property now is the CachedUser type rather than User like before.
We now use wrapped properties here as well, just like in ContentView because we made those computed properties in the CachedUser extension before to get rid of optionals.
Also, the friendsArray is now used in ForEach loop to display friends of each user.
Conclusion
That is basically it. The bare bones of this app. I took it a bit further styling wise in my GitHub Repo for this project, but you can do much more than that. This post was about the logic parts which gave me a few days of trouble, but I hope I did it well enough. :)
If you did this differently, I would love to see how and maybe we can discuss. Hit me up on twitter @viktormauzer or comment and I’m looking forward to more input on this challenge.
A big thanks to Paul Hudson for providing all this knowledge and challenges. If you want to learn anything Swift, HackingWithSwift is the place to be.
If you want to check out the full code with improvements and a bit of refactoring, download it from GitHub here.
See you in the next one!