Categories
Uncategorized

User Authentication with SwiftUI and Firebase

In this tutorial, you will learn how to perform user authentication using Firebase and SwiftUI. You will learn how to easily create user accounts for your app. We’ll also look at how to let users log in and out of their accounts.

The app we create in this tutorial will end up looking like this:

Let’s get right into it!

The Starter Project 🛫

I have already prepared a starter project, which you can download here:

The starter project contains three different views we will work with. A SignUpView, where the user can create an account with his email and a personal password. A SignInView where the user can log in with his credentials. And finally, a HomeView represents the actual content of our app and which we will show as soon as the user has successfully registered or logged in.

Note that the starter project also includes a ViewRouter and a MotherView that allow us to navigate between different views. If you want to know exactly how this works, feel free to check out this tutorial.

Preparing our App for Firebase 🔥

To authenticate users we use Google’s Firebase service. Firebase is a great and simple way to add backend functionality like databases, analytics and even user authentication to SwiftUI apps.

Firebase is free for most features up to a certain level. For example, the free plan allows you to perform 10k user authentications per month. The only thing you need is a Google account.

Let’s register our app for Firebase services. To do this, open the Firebase Dashboard and click “Get Started” and then “Add Project”. 

Name your project as you like and then click “Continue”. If you want, you can also enable Google Analytics, but we don’t need it for our purposes. After you have created the Firebase project, you should see something like this:

To add Firebase to the Xcode project, click on the “iOS+” button in the Firebase Console and follow the instructions.

Copy and paste the bundle ID of your Xcode project (if you don’t have one yet, create one, for example “com.YOURNAME.AuthenticationDemoApp”) and click on “Register App”.  

Then download the generated “GoogleService-Info.plist” file and add it to the Xcode project (simply drag and drop it into the Project navigator). Make sure that you select “Copy items if needed”.

Now we just need to add the Firebase dependencies to our Xcode project. Open the Xcode menubar and select “File” – “Add Packages”. Then enter the URL “https://github.com/firebase/firebase-ios-sdk” into the search field and click on “Add Packages”.

After a few moments, we will be asked which Firebase components we want to install. We need the FirebaseAuth framework to perform user authentication in SwiftUI apps. Therefore, select “FirebaseAuth” and click on “Add Package”.

At this point, we need to initialize Firebase once we run the app. To do this, open the AuthenticationStarter.swift file and add the Firebase library and the following initialization code to it.

import SwiftUI
import Firebase
@main
struct AuthenticationInSwiftUIFinishedApp: App {
    
    @StateObject var viewRouter = ViewRouter()
    
    init() {
        FirebaseApp.configure()
    }
    
    var body: some Scene {
        WindowGroup {
            MotherView().environmentObject(viewRouter)
        }
    }
}

Also add the Firebase library to the SignUpView, SignInView and HomeView.

import SwiftUI
import Firebase
struct SignUpView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}
import SwiftUI
import Firebase
struct SignInView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}
import SwiftUI
import Firebase
struct HomeView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}

In the Firebase Console, we need to enable the authentication feature. To do this, we click on “Authentication” under the “Build” section and then on “Get started”.

We select “Email/Password” as the authentication method, but do not enable “Email link”.

That’s it! We have now set up everything to do user authentication using SwiftUI and Firebase.

Signing Up Users with Email and Password 📧

Let’s start with the SignUpView. We want to know if and when the authentication process is performed. For this, we add a corresponding State property to our SignUpView to which we initially assign false.

import SwiftUI
import Firebase
struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }
}

Next, we add a function that creates a new user account in Firebase using a given username and password. When this function is called we set the signUpProcessing State to true.

import SwiftUI
import Firebase
struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }
    func signUpUser(userEmail: String, userPassword: String) {
        
        signUpProcessing = true
    }
}

To create an account in Firebase we use the createUser method of the FirebaseAuth dependency we installed earlier. 

import SwiftUI
import Firebase
struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }
    func signUpUser(userEmail: String, userPassword: String) {
        
        signUpProcessing = true
        Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
            
        }
    }
}

The value authResult tells us if the signup process returned a result. The error value in the createUser closure throws us any errors, for example if an account already exists under the given email.

Next, we use a guard statement to make sure that no such errors occurred. Otherwise, we terminate the authentication process.

func signUpUser(userEmail: String, userPassword: String) {
    
    signUpProcessing = true
    
    Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
        guard error == nil else {
            signUpProcessing = false
            return
        }
        
    }
}

Next, let’s see if we received a result from Firebase. If this is the case, we end the authentication process and navigate to the .homePage using the viewRouter.

func signUpUser(userEmail: String, userPassword: String) {
    
    signUpProcessing = true
    
    Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
        guard error == nil else {
            signUpProcessing = false
            return
        }
        
        switch authResult {
        case .none:
            print("Could not create account.")
            signUpProcessing = false
        case .some(_):
            print("User created")
            signUpProcessing = false
            viewRouter.currentPage = .homePage
        }
    }
}

We now call the signUpUser function from the “Sign Up” Button using the values from the username and password TextField.

Button(action: {
    signUpUser(userEmail: email, userPassword: password)
}) {
    //...
}

To prevent the user from creating an account without providing a password and email, we disable the “Sign Up” Button if the TextFields are empty. Also, we disable it if the String of the password TextField and that of the confirmationPassword TextField are not identical. 

Button(action: {
    signUpUser(userEmail: email, userPassword: password)
}) {
    //...
}
    .disabled(!signUpProcessing && !email.isEmpty && !password.isEmpty && !passwordConfirmation.isEmpty && password == passwordConfirmation ? false : true)

That’s it! We can now check if this works. Let’s run our app, navigate to the sign up page and use any email and password (with at least six digits). 

When we click on the Sign Up Button, a user account will be created for us on Firebase and we will be navigated to the HomeView.

We can check if this worked on the server-side by opening the console of our Firebase project and going to the Authentication page. Under the “User” tab we can now see the user account we just created!

Next, we want to improve the user experience by displaying a spinning activity indicator while the registration process is running. To do this, we simply add a ProgressView to our VStack when signUpProcessing is false.

var body: some View {
    VStack(spacing: 15) {
        //...
        Button(action: {
            signUpUser(userEmail: email, userPassword: password)
        }) {
            //...
        }
        if signUpProcessing {
            ProgressView()
        }
        Spacer()
        //...
    }
        .padding()
}

We also want to tell the user if any errors have occurred. To do this, we create a new State that holds the error message as a String.

@State var signUpErrorMessage = ""

Inside the guard statement of our signUpUser function, we now use the error property to assign the error description to the State property.

guard error == nil else {
    signUpErrorMessage = error!.localizedDescription
    signUpProcessing = false
    return
}

Finally, we add a Text view with the error message to our VStack, on the condition that one exists.

var body: some View {
    VStack(spacing: 15) {
        //...
        Button(action: {
            signUpUser(userEmail: email, userPassword: password)
        }) {
            //...
        }
        if signUpProcessing {
            ProgressView()
        }
        if !signUpErrorMessage.isEmpty {
            Text("Failed creating account: \(signUpErrorMessage)")
                .foregroundColor(.red)
        }
        Spacer()
        //...
    }
        .padding()
}

Now let’s run our app one more time and use for example an already existing email or a password that is too insecure.

Signing In Users 👤

So far, we know how to successfully create a user account using SwiftUI and Firebase. In the next step, let’s look at how we can enable the user to log in using their credentials.

To do this, we open our SignUpView. Again, we want to know if and when the authentication process is performed. For this, we add a corresponding State property to our SignInView to which we initially assign false. Again, we also add a State holding the potential error message.

@State var signInProcessing = false
@State var signInErrorMessage = ""

Next, we add a function that signs the user in using the username and password. When this is called we set the signUpProcessing State to true.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
}

To sign in the user we use the FirebaseAuth signIn method.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        
    }
}

This function works very similar to the createUser method and also provides us with an authResult and error value. The authResult value tells us if the sign-up process produced a result. The error value in the createUser closure throws us any errors, for example that the user credentials are incorrect.

So again we use a guard statement to make sure that no such errors occurred. Otherwise, we terminate the authentication process and assign the corresponding error description to the signUpErrorMessage State.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        guard error == nil else {
            signInProcessing = false
            signInErrorMessage = error!.localizedDescription
            return
        }
        
    }
}

Next, we check if we have received a result from Firebase. If so, we terminate the authentication process and navigate to the .homePage using the viewRouter. If it fails we set signInProcessing to false.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        guard error == nil else {
            signInProcessing = false
            signInErrorMessage = error!.localizedDescription
            return
        }
        switch authResult {
        case .none:
            print("Could not sign in user.")
            signInProcessing = false
        case .some(_):
            print("User signed in")
            signInProcessing = false
            withAnimation {
                viewRouter.currentPage = .homePage
            }
        }
        
    }
}

We use the “Log In” Button to call this function. Again we want to disable the “Log In” button if the TextFields are empty. 

Button(action: {
    signInUser(userEmail: email, userPassword: password)
}) {
    Text("Log In")
        //...
}
    .disabled(!signInProcessing && !email.isEmpty && !password.isEmpty ? false : true)

As we did with our SignUp view, let’s add a spinning activity indicator to the SignInView while the sign in process is ongoing. Additionally, we show a Text view if the sign in process throws any error.

//...
Button(action: {
    signInUser(userEmail: email, userPassword: password)
}) {
    //...
}
    .disabled(!signInProcessing && !email.isEmpty && !password.isEmpty ? false : true)
if signInProcessing {
    ProgressView()
}
if !signInErrorMessage.isEmpty {
    Text("Failed creating account: \(signInErrorMessage)")
        .foregroundColor(.red)
}
//...

Let’s check if this works by running our app and trying to sign in using the credentials of the account we created earlier.

Signing Out Users 🚪

So the user is now tagged as logged in with his account in Firebase. We also want to provide the user with the ability to log out from the HomeView. To do this, we add the following function to the HomeView.

func signOutUser() {
    let firebaseAuth = Auth.auth()
    do {
      try firebaseAuth.signOut()
    } catch let signOutError as NSError {
      print("Error signing out: %@", signOutError)
    }
    withAnimation {
        viewRouter.currentPage = .signInPage
    }
}

This uses the firebaseAuth.signOut() method to log the user out of their Firebase account again. Once this task is completed we navigate back to the SignInView using the viewRouter.

We add a Button to our HomeView in the navigation bar using the .toolbar modifier and a ToolbarItem view.

NavigationView {
    Text("HomeView")
        .navigationTitle("V24")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Sign Out") {
                    
                }
            }
        }
}

With this Button, we now call the signOutUser method.

NavigationView {
    Text("HomeView")
        .navigationTitle("V24")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Sign Out") {
                    signOutUser()
                }
            }
        }
}

While the sign-out process is still ongoing, we want to display a spinning activity indicator again instead of the “Sign Out” button. To do this, we add a “signOutProcessing” State to our HomeView. If this is true, we show a simple ProgressView instead of the ToolbarItem.

struct HomeView: View {
    
    //...
    
    @State var signOutProcessing = false
    
    var body: some View {
        NavigationView {
            Text("HomeView")
                .navigationTitle("V24")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        if signOutProcessing {
                            ProgressView()
                        } else {
                            Button("Sign Out") {
                                signOutUser()
                            }
                        }
                    }
                }
        }
    }
    
    //...
}

Finally, we just have to set the signOutProcessing State to true when we call the signOutUser function. However, when the firebaseAuth.signOut method fails, we set signOutProcesssing to false again.

func signOutUser() {
    signOutProcessing = true
    let firebaseAuth = Auth.auth()
    do {
      try firebaseAuth.signOut()
    } catch let signOutError as NSError {
      print("Error signing out: %@", signOutError)
        signOutProcessing = false
    }
    withAnimation {
        viewRouter.currentPage = .signInPage
    }
}

Now when we run our app we can also manually log out of the app!

Where to go from here 🎉

I have uploaded the finished project to GitHub. Please note that I removed the GoogleService-Info.plist file. So to run the project you have to add your own first.

That’s it! You now know how to authenticate users in SwiftUI using Firebase. You have learned how to create user accounts, log in and log out users using email and password.

Other user authentication methods (like SMS, Sign in with Google, Sign in with Apple, etc.) can be easily added to your SwiftUI apps. Have a look at the official Firebase documentation. If you need specific help, feel free to let me know in the comments!

Categories
Uncategorized

Mastering Pull to Refresh and Refreshable in SwiftUI

Since iOS 15, Apple allows us to add pull to refresh functionality to our SwiftUI apps with just a few steps. In this article, you will learn how to add pull to refresh functionality to Lists, how to use the .refreshable modifier to perform asynchronous tasks such as network requests and how to make your own custom views refreshable.

Making a List refreshable 🔄

To start with a simple example, let’s create a List that shows ten random emojis.

import SwiftUI
struct EmojiList: View {
    
    @State var emojiSet = [String]()
    
    var body: some View {
        NavigationView {
            List(emojiSet, id: \.self) { emoji in
                Text(emoji)
            }
                .navigationTitle("Random Emojis")
        }
    }
}

To fill our emojiSet with random emojis, we create a function like this:

struct EmojiList: View {
    
    @State var emojiSet = [String]()
    
    var body: some View {
        //...
    }
    
    func createRandomEmojiSet() -> [String] {
        
        var emojiSet = [String]()
        
        for _ in 1...10 {
            let range = [UInt32](0x1F601...0x1F64F)
            let ascii = range[Int(drand48() * (Double(range.count)))]
            let emoji = UnicodeScalar(ascii)?.description
            emojiSet.append(emoji!)
        }
        
        return emojiSet
    }
}

We want to fill up the emojiSet already the first time our EmojiView appears. For this we simply use the .onAppear modifier.

var body: some View {
    NavigationView {
        List(emojiSet, id: \.self) { emoji in
            Text(emoji)
        }
            .navigationTitle("Random Emojis")
            .onAppear {
                emojiSet = createRandomEmojiSet()
            }
    }
}

Our preview should now look something like this:

We want to allow the user to pull down the list to generate ten new emojis and display them in a refreshed List. 

To add the pull to refresh functionality to our SwiftUI List, simply use the .refreshable modifier. 

List(emojiSet, id: \.self) { emoji in
    Text(emoji)
}
    .refreshable {
    
    }
    //...

However, the SwiftUI pull to refresh functionality only works with Lists (we’ll look at how to make views other than Lists refreshable later).

The .refreshable modifier does the hard work for us and adds the pull gesture and the corresponding spinning activity indicator to our List.

Inside the closure of the .refreshable modifier, we can specify what should happen when the user performs the pull gesture.

In our example, we use the createRandomEmojiSet function to refill our emojiSet and display new random emojis in the List.

List(emojiSet, id: \.self) { emoji in
    Text(emoji)
}
    .refreshable {
        emojiSet = createRandomEmojiSet()
    }
    //...

Now when we do the pull gesture our List will refresh and show us new random emojis!

Pull to Refresh and Networking Requests 🌎

Okay, let’s dive a little deeper into the SwiftUI pull to refresh functionality, exploring how we incorporate networking requests into our pull to refresh workflow.

The following view shows an (initially empty) List of different US states. 

struct RandomStates: View {
    
    @State var states = ["Pull 2 get random states"]
    
    var body: some View {
        NavigationView {
            List(states, id: \.self) { state in
                Text(state)
            }
                .navigationTitle("Random US States 🇺🇸")
        }
    }
}

Unlike the previous example, however, we don’t want to generate these randomly, but rather get them from a web server. At this point, we want to choose an example as simple as possible, so I decided to use the service names.drycodes.com, which is perfect for downloading sample data for educational purposes.

Using this service we can download random states using an appropriate URL in JSON format. The URL “http://names.drycodes.com/10?nameOptions=states&combine=1&separator=space” returned about 10 random states in JSON format. 

The corresponding JSON looks something like this:

To make the JSON usable for our purposes, we need to parse it. For this we need the corresponding data model, which we add to our RandomStates.swift file:

struct StateItem: Decodable, Identifiable {
    let id: Int
    let name: String
}

At this point I won’t go further into how to use JSON in Swift, we have a separate tutorial on that here.

Hint: You may have noticed that we are only dealing with an http and not an https internet address, which means that the data to be transmitted is not encrypted. This does not bother us for our purposes, but unencrypted web traffic must be explicitly allowed. Therefore we add the “App Transport Security Settings” key to our projects Target Properties and set “Allow Arbitrary Loads” to “YES”:

Next, we add a function to our RandomStates struct that downloads the JSON data using a URLRequest and assigns the contained state names to our states array using the data model we defined earlier.

struct RandomStates: View {
    
    @State var states = ["Pull 2 get random states"]
    
    var body: some View {
        //...
    }
    
    func fetchRandomStates() {
        let url = URL(string: "http://names.drycodes.com/10?nameOptions=states&combine=1&separator=space")!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let fetchedStates = try! JSONDecoder().decode([String].self, from: data)
        states = fetchedStates
    }
}

Now let’s use the .refreshable modifier as before to call the fetchRandomStates function.

List(states, id: \.self) { state in
    Text(state)
}
    .refreshable {
        fetchRandomStates()
    }
    //...

However, we now get an error message: “‘async’ call in a function that does not support concurrency”.

What does this mean? 🤔

A short excursion into Swift Concurrency 💡

For this purpose, we need to understand the difference between synchronous and asynchronous functions first.

The system of the device our app runs on uses so-called threads to perform various processes, i.e. to perform a function defined by us. You can imagine threads as working units of the system.

A “normal” function is a synchronous function. A synchronous function blocks the thread until it finishes its work. As long as the thread is blocked, it cannot perform any other task. So a synchronous function is only suitable for fast executable and lightweight tasks.

An asynchronous function, on the other hand, does not block the thread. An asynchronous function can suspend, meaning giving control of the thread to the system that decides what is most important (called awaiting). At some time the system will resume the function (called resuming). This is especially useful for more complex tasks that may take a long time to complete. These include, in particular, networking processes whose speed depends on Internet performance or resource-intensive computations.

One such task is our URLSession. We tell Swift to perform the URLSession asynchronously by marking it with the await keyword. However, this is only possible in an asynchronous context. This means that our fetchRandomStates function must also be marked as asynchronous. And to do this, we mark our function as asynchronously using the async keyword.

func fetchRandomStates() async {
    //...
}

If we want to call our asynchronous fetchRandomStates function, we need to embed it into an asynchronous context. However, the .refreshable modifier already provides us with such an asynchronous environment. Therefore, we only need to use the await keyword to call the fetchRandomStates function.

.refreshable {
    await fetchRandomStates()
}

If we now start a Live preview we can see that we can use the .refreshable modifier to execute a network request and display the resulting data in our List.

Fetching updates using Pull to Refresh ⬇️

Okay, let’s strengthen our knowledge and turn to a more practical example.

The following “ExchangeRateView” is supposed to display different exchange rates, for example: “US Dollar : Jap. Yen” or “Euro : Chin. Yuan”.

import SwiftUI
struct ExchangeRateView: View {
    
    @State var exchangeRateUSDtoYEN = ""
    @State var exchangeRateEURtoCNH = ""
    
    var body: some View {
        NavigationView {
            List {
                ExchangeRateListRow(concurrency: "US-Dollar : Jap. Yen", symbol: "USD", exchangeRate: $exchangeRateUSDtoYEN)
                ExchangeRateListRow(concurrency: "Euro : Chin. Yuan", symbol: "EUR", exchangeRate: $exchangeRateEURtoCNH)
            }
                .listStyle(.grouped)
                .navigationTitle("Forex Rates 💹")
        }
    }
}
struct ExchangeRateListRow: View {
    
    let concurrency: String
    let symbol: String
    
    @Binding var exchangeRate: String
    
    var body: some View {
        HStack {
            Text(concurrency)
                .font(.headline)
            Spacer()
            VStack(alignment: .leading) {
                if let roundedExchangeRate = Float(exchangeRate) {
                    Text("1 \(symbol) =")
                        .font(.footnote)
                        .opacity(0.3)
                    Text(String(format:"%.2f", roundedExchangeRate))
                        .font(.headline)
                        .foregroundColor(.green)
                } else {
                    Text("...")
                        .opacity(0.5)
                }
            }
        }
            .frame(height: 50)
    }
}
struct ExchangeRateView_Previews: PreviewProvider {
    static var previews: some View {
        ExchangeRateView()
    }
}

The corresponding preview looks something like this:

To download the corresponding data we use the free AlphaVantageAPI. You can register for free to get your personal API key. As soon as you have it, store it in a corresponding constant.

let apiKey = "*YOUR APIKEY*"

If you want to learn more about processing and displaying financial data using the AlphavantageAPI and SwiftUI, take a look at our Mastering SwiftUI Interactive Book. In it, we also look in particular at how we can display beautiful graphs of price trends.

For example, the URL “https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=*YOURAPIKEY*” returns the following JSON:

In order to parse this, we need a suitable data model which we add to our ExchangeRateView.swift file.

struct ExchangeRate: Codable {
    let realtimeCurrencyExchangeRate: RealtimeCurrencyExchangeRate
    enum CodingKeys: String, CodingKey {
        case realtimeCurrencyExchangeRate = "Realtime Currency Exchange Rate"
    }
}
struct RealtimeCurrencyExchangeRate: Codable {
    let exchangeRate: String
    enum CodingKeys: String, CodingKey {
        case exchangeRate = "5. Exchange Rate"
    }
}

As before, we can now add an asynchronous function to our ExchangeRateView that returns the exchange rate using a URLRequest.

struct ExchangeRateView: View {
    
    @State var exchangeRateUSDtoYEN = ""
    @State var exchangeRateEURtoCNH = ""
    
    var body: some View {
        //..
    }
    
    func getExchangeRate(apiCallString: String) async -> String {
        let url = URL(string: apiCallString)!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let parsedData = try! JSONDecoder().decode(ExchangeRate.self, from: data)
        let exchangeRate = parsedData.realtimeCurrencyExchangeRate.exchangeRate
        
        return exchangeRate
    }
}

Now we can use the .refreshable modifier again to refresh the latest exchange rates when a pull gesture is performed.

List {
    //...
}
    .refreshable {
        exchangeRateUSDtoYEN = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
        exchangeRateEURtoCNH = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=EUR&to_currency=CNY&apikey=\(apiKey)")
    }
    //…

Let’s do a Live preview to see if this works.

Making custom views refreshable 💡

As already mentioned above, the SwiftUI refreshable logic can be applied to views other than Lists. To understand how this works, let’s create a slightly different version of our ExchangeRateView.

import SwiftUI
struct DetailExchangeRateView: View {
    
    let baseSymbol: String
    let outputSymbol: String
    
    @State var exchangeRate: String
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("1 \(baseSymbol) =")
                    .bold()
                if let roundedExchangeRate = Float(exchangeRate) {
                    Text("\(String(format:"%.2f", roundedExchangeRate)) \(outputSymbol)")
                        .font(.largeTitle)
                        .bold()
                } else {
                    Text("...")
                }
            }
                .frame(height: 50)
                .navigationBarTitle("\(baseSymbol) : \(outputSymbol) 💱")
        }
    }
 
    func getExchangeRate(apiCallString: String) async -> String {
        let url = URL(string: apiCallString)!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let parsedData = try! JSONDecoder().decode(ExchangeRate.self, from: data)
        let exchangeRate = parsedData.realtimeCurrencyExchangeRate.exchangeRate
        
        return exchangeRate
    }
    
}
struct DetailExchangeRateView_Previews: PreviewProvider {
    static var previews: some View {
        DetailExchangeRateView(baseSymbol: "USD", outputSymbol: "YEN", exchangeRate: "")
    }
}

The corresponding preview looks like this:

We want our new DetailExchangeRateView to show the most current exchange rate from US dollars to Japanese Yen. To retrieve the latest exchange rate we use a Button:

Let’s add one to the VStack and embed it into a Group.

VStack(spacing: 20) {
    //...
    Group {
        Button(action: {
            
        }) {
            Text("Update Rate")
        }
    }
}

Next, we outsource this Group as a separate view and name it “RateRefresher”.

struct DetailExchangeRateView: View {
    
    let baseSymbol: String
    let outputSymbol: String
    
    @State var exchangeRate: String
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                //...
                RateRefresher()
            }
                //...
        }
    }
    //…
    
}

struct RateRefresher: View {
    var body: some View {
        Group {
            Button(action: {
                
            }) {
                Text("Update Rate")
            }
        }
    }
}

Now we can add the refresh logic to our outsourced RateRefresher. We do this as usual with the .refreshable modifier.

RateRefresher()
    .refreshable {
        exchangeRate = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
    }

To make our RateRefresher listen to the .refreshable modifier we have to use the refresh @Environment property.

struct RateRefresher: View {
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        //...
    }
}

If we now use this property in our RateRefresher‘s Button, we can asynchronously execute the code contained in the .refreshable closure:

struct RateRefresher: View {
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        Group {
            if let refresh = refresh {
                Button(action: {
                    Task {
                        await refresh()
                    }
                }) {
                    Text("Update Rate")
                }
            }
        }
    }
}

Let’s run a Live preview to see if this works:

While this works, we want a spinning activity indicator to be displayed instead of the Button while the exchange rate is being downloaded. To do this we add a State property to our RateRefresher for keeping track of whether new data is being downloaded. When this is true we show a simple ProgressView instead of the Button.

struct RateRefresher: View {
    
    @State var isLoading = false
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else {
                if let refresh = refresh {
                    //...
                }
            }
        }
    }
}

Once the “Update Rate” Button is clicked we set isLoading to true. Finally, when the exchange rate has been downloaded, we set isLoading back to false, so that the “Update Rate” Button appears again.

Button(action: {
    isLoading = true
    Task {
        await refresh()
        isLoading = false
    }
}) {
    Text("Update Rate")
}

If we now start a Live preview, we will see how a spinning activity indicator is displayed for the time that the latest exchange rate is downloaded.

Finally, we can use the .onAppear closure to automatically retrieve the exchange rate when opening the DetailExchangeRateView.

NavigationView {
    VStack(spacing: 20) {
        //...
    }
        //...
        .onAppear {
            Task {
                exchangeRate = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
            }
        }
}

To provide a better user experience replace the placeholder “…”-Text view with a ProgressView.

if let roundedExchangeRate = Float(exchangeRate) {
    Text("\(String(format:"%.2f", roundedExchangeRate)) \(outputSymbol)")
        .font(.largeTitle)
        .bold()
} else {
    ProgressView()
}

This results in the following Live preview:

Conclusion 🎊

Awesome, you’ve learned a lot about the .refreshable modifier and its use cases. In addition to the classic pull to refresh functionality, we looked at how we perform asynchronous tasks like network requests in our SwiftUI apps and how we apply refreshable logic to views other than Lists.

Here’s the GitHub repo for this project.

Did you like the tutorial and want to learn more about developing iOS apps with SwiftUI? Then make sure to check out our Interactive Mastering SwiftUI Book!

Categories
Uncategorized

Custom progress bars in SwiftUI

Have you ever had to implement a certain progress bar into your app, for example, to display an ongoing loading process? Well, in SwiftUI it’s super simple to create these 👇

I also uploaded the progress bars templates to GitHub. So if you want to skip this tutorial, you can just copy and paste the files to your project and initialize the specific bars with multiple customization options.

Simple Progress Bar

Let’s start with creating a simple progress bar like this one:


First, create a new SwiftUI file called SimpleProgressBar.

Create a State property assigned to a CGFloat value. We use this to represent the current progress of our loading process. For example, 0.5 means that 50 percent have loaded so far.

struct SimpleProgressBarDemo: View {
    @State var currentProgress: CGFloat = 0.0
    var body: some View {
        Text("Hello World")
    }
}

The next step to create our progress bar is to replace the default Text view with a ZStack with the .leading alignment mode.

var body: some View {
    ZStack(alignment: .leading) {
    }
}

ZStacks are used for stacking elements on top of each other. Click here to learn more. The first object in our ZStack is the “inner”, static bar. To create this, we use a RoundedRectangle.

ZStack(alignment: .leading) {
    RoundedRectangle(cornerRadius: 20)
}

We want the bar to be gray, wide and thin.

RoundedRectangle(cornerRadius: 20)
    .foregroundColor(.gray)
    .frame(width: 300, height: 20)

Another, dynamic bar should overlay our static one. Therefore, we place another RoundedRectangle into our ZStack. This rectangle should be as high as the first one but only as wide as it if our currentProgress is 1.0, i.e. 100 percent are loaded. To achieve this, we write:

RoundedRectangle(cornerRadius: 20)
    .foregroundColor(.blue)
    .frame(width: 300*currentProgress, height: 20)

To simulate a loading process, we use a timer that adds 0.1 to our currentProgress every second. We implement a corresponding button that starts the timer.

struct SimpleProgressBarDemo: View {
    
    @State var currentProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack(alignment: .leading) {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.currentProgress += 0.01
                if self.currentProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

By using the withAnimation wrapper, we apply a smooth, default animation to our bar’s progress. Run the view in the live preview and click the button to simulate the loading process.

Circular Progress Bar


First, create a new SwiftUI file called CircularProgressBar. Let’s implement a State for keeping track of the loading progress.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        Text("Hello World!")
    }
}

Replace the default Text view with a ZStack again.

ZStack {
            
}

The first object in our ZStack is the “inner”, static circle. To create it, declare a Circle view.

ZStack {
    Circle()
}

We don’t want our Circle filled out, we just want it to be a ring. To do this we use the .stroke modifier. Next, we modify the width and height of our Circle by using the .frame modifier.

Circle()
    .stroke(Color.gray, lineWidth: 15)
    .frame(width: 200, height: 200)

Another, dynamic circle should overlay the static one. Therefore, place a new Circle view into the ZStack. We use the .stroke and .frame modifier again, but this time we use a different color.

Circle()
     .stroke(Color.blue, lineWidth: 15)
     .frame(width: 200, height: 200)

The Circle should only be closed when our circleProgress State is 1.0. For example, if the progress state is 0.5, the ring should only be half long. To achieve this we use the .trim modifier (as the first modifier!) and pass the circleProgress to its end argument.

Circle()
    .trim(from: 0.0, to: circleProgress)
    .stroke(Color.blue, lineWidth: 15)
    .frame(width: 200, height: 200)

Now try to change the value of the circleProgress State. You see that our circular progress bar immediately adapts to it.But we don’t want our circular progress bar to start at the top. Therefore, we spin it by 90 degrees.

Circle()
    .trim(from: 0.0, to: circleProgress)
    .stroke(Color.blue, lineWidth: 15)
    .frame(width: 200, height: 200)
    .rotationEffect(Angle(degrees: -90))

If you want, you can also insert a Text indicating how much percent is already loaded.

ZStack {
    //...
    Text("\(Int(self.circleProgress*100))%")
        .font(.custom("HelveticaNeue", size: 20.0))
}

To simulate the loading process, we implement a corresponding Button that fires a timer again.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.circleProgress += 0.01
                if self.circleProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Tip: Subtract twice the line width from the width and height of the circle to achieve a style like this:

ZStack {
    Circle()
        //...
    Circle()
        .trim(from: 0.0, to: circleProgress)
        .stroke(Color.blue, lineWidth: 15)
        .frame(width: 200-15*2, height: 200-15*2)
        .rotationEffect(Angle(degrees: -90))
         //...
}

Halved-Circular Progress Bar

Creating this cool bar is very similar to what we’ve already done.


As always, we create a State property to know how far the loading process is already. We also replace the default text with a ZStack.

@State var progress: CGFloat = 0.0
    
var body: some View {
    ZStack {
            
    }
}

Again, we start by creating a static circle. Only this time we trim it to the half. We also spin it by 180 degrees. To create a dashed ring we use the .stroke modifier again, but this time we use a different StrokeStyle.

Circle()
    .trim(from: 0.0, to: 0.5)
    .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
    .frame(width: 200, height: 200)
    .rotationEffect(Angle(degrees: -180))

We equip our dynamic circle with the usual .stroke modifier and trim it depending on our loading process. We also spin it by 180 degrees.

ZStack {
    Circle()
        //...
    Circle()
        .trim(from: 0.0, to: progress/2)
        .stroke(Color.blue, lineWidth: 12.0)
        .frame(width: 200, height: 200)
        .rotationEffect(Angle(degrees: -180))
}

You can add an according Text again and simulate the loading process with a timer.

struct HalvedCircularBar: View {
    
    @State var progress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .trim(from: 0.0, to: 0.5)
                    .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Circle()
                    .trim(from: 0.0, to: progress/2)
                    .stroke(Color.blue, lineWidth: 12.0)
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Text("\(Int(self.progress*100))%")
                    .font(.custom("HelveticaNeue", size: 20.0))
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.progress += 0.01
                if self.progress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Activity Indicator

Although this is not really a progress bar, we want to have a look at how to create an activity indicator in SwiftUI.


We declare a State again, but this time for keeping track of the current indicator’s position by its degree.

@State var degress = 0.0

As usual, we use a static and a dynamic circle. We trim the dynamic circle by a fixed value. To rotate the whole dynamic circle, we use the .rotationEffect modifier and make it dependent on the value of our degrees State.

Circle()
    .trim(from: 0.0, to: 0.6)
    .stroke(darkBlue, lineWidth: 5.0)
    .frame(width: 120, height: 120)
    .rotationEffect(Angle(degrees: degress))

To present a loading process, we can simply use a repeating timer that starts as soon as the view has loaded. The timer adds a few degrees to our State at fast time intervals. Once our circle has one full time, we set our degree State back to 0.

struct ActivityIndicator: View {
    
    @State var degress = 0.0
    
    var body: some View {
        Circle()
            .trim(from: 0.0, to: 0.6)
            .stroke(darkBlue, lineWidth: 5.0)
            .frame(width: 120, height: 120)
            .rotationEffect(Angle(degrees: degress))
            .onAppear(perform: {self.start()})
    }
    
    func start() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
            withAnimation {
                self.degress += 10.0
            }
            if self.degress == 360.0 {
                self.degress = 0.0
            }
        }
    }
}

Conclusion 🎊

Great, we just learned how easy it is to create beautiful progress bars in SwiftUI.

Here’s the corresponding GitHub repository.

Did you like the tutorial and want to learn more about developing iOS apps with SwiftUI? Then make sure to check out our Interactive Mastering SwiftUI Book!

Categories
Uncategorized

SwiftUI – How to create a multi-component picker

Updated for iOS 15

In many SwiftUI apps there are cases where we want to use multi-component pickers using. For example, they are suitable when we want to know the user’s birthday. However, SwiftUI does not provide us with native multi-component pickers. However, this is not a big problem as we can easily build one ourselves in a few steps!

Let’s get started!

Step 1: Our SwiftUI multi-component Picker basically consists of several individual Picker views arranged horizontally. Therefore, we start by creating an ordinary Picker view for our first component. To do this, we declare an appropriate State property and initialize an array that contains the set for the Picker view.

struct ContentView: View {
    
    @State var daySelection = 0
    
    var days = [Int](0..<30)
    
    var body: some View {
        
    }
}

Now we can create the Picker view. To make it appear as a wheel, we use the appropriate .pickerStyle.

struct ContentView: View {
    
    @State var daySelection = 0
    
    var days = [Int](0..<30)
    
    var body: some View {
        Picker(selection: self.$daySelection, label: Text("")) {
            ForEach(0 ..< self.days.count) { index in
                Text("\(self.days[index]) d").tag(index)
            }
        }
            .pickerStyle(.wheel)
    }
}

Step 2: Now we can embed the first Picker view in an HStack with zero spacing and repeat the steps from Step 1 for each additional component. The resulting code looks something like this:

struct ContentView: View {
    
    @State var daySelection = 0
    @State var hourSelection = 0
    @State var minuteSelection = 0
    
    var days = [Int](0..<30)
    var hours = [Int](0..<24)
    var minutes = [Int](0..<60)
    
    var body: some View {
        HStack(spacing: 0) {
            Picker(selection: self.$daySelection, label: Text("")) {
                ForEach(0 ..< self.days.count) { index in
                    Text("\(self.days[index]) d").tag(index)
                }
            }
                .pickerStyle(.wheel)
            Picker(selection: self.$hourSelection, label: Text("")) {
                ForEach(0 ..< self.hours.count) { index in
                    Text("\(self.hours[index]) h").tag(index)
                }
            }
                .pickerStyle(.wheel)
            Picker(selection: self.$minuteSelection, label: Text("")) {
                ForEach(0 ..< self.minutes.count) { index in
                    Text("\(self.minutes[index]) m").tag(index)
                }
            }
                .pickerStyle(.wheel)
        }
    }
}


Step 3: We need to know the superview’s (here: the ContentView) width since we want each component of the Picker to be equally wide. Thus, let’s create a Geometry Reader for getting the ContentView‘s size.

var body: some View {
    GeometryReader { geometry in
        HStack(spacing: 0) {
            //...
        }
    }
}

We can now use the geometry property from the GeometryReader closure to divide each Picker view into thirds. 

HStack(spacing: 0) {
    Picker(selection: self.$daySelection, label: Text("")) {
        ForEach(0 ..< self.days.count) { index in
            Text("\(self.days[index]) d").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
    Picker(selection: self.$hourSelection, label: Text("")) {
        ForEach(0 ..< self.hours.count) { index in
            Text("\(self.hours[index]) h").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
    Picker(selection: self.$minuteSelection, label: Text("")) {
        ForEach(0 ..< self.minutes.count) { index in
            Text("\(self.minutes[index]) m").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
}

This already looks good. However, when we start a Live preview, we notice that the individual Picker views overlap.


Step 4: To prevent overlapping we apply the .compositionGroup and the .clipped modifier to each Picker view.

HStack(spacing: 0) {
    Picker(selection: self.$daySelection, label: Text("")) {
        ForEach(0 ..< self.days.count) { index in
            Text("\(self.days[index]) d").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
    Picker(selection: self.$hourSelection, label: Text("")) {
        ForEach(0 ..< self.hours.count) { index in
            Text("\(self.hours[index]) h").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
    Picker(selection: self.$minuteSelection, label: Text("")) {
        ForEach(0 ..< self.minutes.count) { index in
            Text("\(self.minutes[index]) m").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
}


Let’s run a Live preview again to see if this works:


That’s all! You can now use the State properties for subscripting the selected values out of your data sets. For example, like this:

var body: some View {
    GeometryReader { geometry in
        VStack {
            Spacer()
            HStack(spacing: 0) {
                //...
            }
                //You can also limit the width and height of your multi component picker
                .frame(width: 350, height: 150)
            Text("Remind me in \(daySelection) day(s), \(hourSelection) hour(s) and \(minuteSelection) minute(s)")
                .padding(.top, 40)
            Spacer()
        }
    }
}

And that is how you can create multi component Pickers in SwiftUI!

If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics Book and our other tutorials! Also make sure you follow us on Instagram to not miss any updates, tutorials and tips about SwiftUI and more!

Categories
Uncategorized

How to use AsyncImage in SwiftUI

In this tutorial, you’ll learn everything you need to know about the new AsyncImage in SwiftUI. You learn how to fetch Image data asynchronously in a safe and easy way and how to provide a great user experience by utilizing placeholders and covering errors.

This is how our AsyncImage will look like:

Why and when should we use AsyncImage in SwiftUI ❓

When we build SwiftUI apps, we often face the situation that at some point we need to download images from web servers. For example, if we want to display a photo feed or we want to use images from a specific database.

Until now, we had to build elaborate structures for this and download the required image data using URLSession & Co. while using completion handlers to display it inside an Image view.

However, since iOS 15, SwiftUI provides an AsyncImage view designed to do this job for us and load images from the web asynchronously. Asynchronous means – roughly speaking – that the task of downloading the image is completed in the background, i.e. we don’t need to wait until the task finishes. This greatly simplifies the image loading process in SwiftUI.

Basic structure of an AsyncImage 🌌

AsyncImage comes with a few initializers, the simplest of which takes an optional URL as the parameter. This means you don’t have to force unwrap the URL, and AsyncImage will either display the image if loading was successful or display the default placeholder, which is a gray color. 

To begin, place an instance of AsyncImage in the project you are working with.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: )
    }
}

Next, we provide it with the URL for an image off the web. I’m using this image for my example:

Sample Image Data for SwiftUI AsyncImage

Let’s copy & paste the URL of this image into our AsyncImage.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"))
    }
}

Let’s try fetching the image from the web using the AsyncImage instance by starting a Live preview.

You may have noticed that by default, AsyncImage takes up all the available space. You may want to control how much space it takes up by adding a .frame modifier.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"))
            .frame(height: 240)
    }
}

However, this does not work as you’d expect: The placeholder displays the correct size set by the frame, but when the image loads, it displays in its natural size. This happens because AsyncImage does not know the size of the image it is loading. Thus it after loading, the image may be smaller or bigger than the frame allocated, and will be displayed as such. 

AsyncImage in SwiftUI Framing

Modifying AsyncImages properly and using Placeholder views 🔁

To control how the image is displayed after it is loaded, AsyncImage provides initializers that give access to the loaded image. You can use the init(url:scale:content:placeholder) initializer which provides a content closure that takes an Image instance and returns a View. Thus we can use it to attach modifiers to the resulting image. For example, we could attach a .resizable or a .aspectRatio modifier to size the image properly.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), scale: 2) { image in
            image
              .resizable()
              .aspectRatio(contentMode: .fill)
        } placeholder: {
            Color.gray
        }
            .frame(height: 240)
    }
}

The placeholder parameter allows us to specify a custom View that displays as the placeholder while the image is loading. For example, we can use a simple spinning activity indicator as the placeholder

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), scale: 2) { image in
            image
              .resizable()
              .aspectRatio(contentMode: .fill)
        } placeholder: {
            ProgressView()
                .progressViewStyle(.circular)
        }
            .frame(height: 240)
    }
}

More control using AsyncImagePhase 💡

If there was an error loading the image, AsyncImage simply displays the placeholder you provided. However, you may want to control what is displayed in case there was an error. To handle this, AsyncImage provides another initializer, init(url:scale:transaction:content). Here, the content parameter is not the image itself, rather it is an AsyncImagePhase instance that gives you access to the loading state of the image which can handle by using a switch statement.

By handling the different cases of the phase we can react accordingly. For example, you could add a red Text to display when there was an error loading the image.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg")) { phase in
            switch phase {
            case .empty:
                ProgressView()
                    .progressViewStyle(.circular)
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            case .failure:
                Text("Failed fetching image. Make sure to check your data connection and try again.")
                    .foregroundColor(.red)
            }
        }
            .frame(height: 240)
    }
}

We can also add the @Unknown case to handle phase cases that may be added in future versions.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), transaction: .init(animation: .spring(response: 1.6))) { phase in
            switch phase {
            case .empty:
                ProgressView()
                    .progressViewStyle(.circular)
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            case .failure:
                Text("Failed fetching image. Make sure to check your data connection and try again.")
                    .foregroundColor(.red)
            @unknown default:
                Text("Unknown error. Please try again.")
                    .foregroundColor(.red)
            }
        }
            .frame(height: 240)
    }
}

Using the transaction modifier we can define a custom animation of how our image should appear once it’s fetched.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), transaction: .init(animation: .spring())) { phase in
            //...
        }
            .frame(height: 240)
    }
}

Let’s run our app in the standard simulator to check if our animation works as expected:

Conclusion 🎉

That’s it. You learned everything you need to know about AsyncImage in SwiftUI. You see that with AsyncImage views it’s very easy to fetch and display images from the web.

Did you like the tutorial and want to learn more about developing iOS apps with SwiftUI? Then make sure to check out our Interactive Mastering SwiftUI Book!

Categories
Uncategorized

Charts in SwiftUI – Part 2: Pie Chart

by Ahmed Mgua (@_mgua)

Charts are important visual elements that help present data to users in a meaningful way. Depending on the type of data, you may decide to use any of the common chart types, for example, bar charts, pie charts, and line charts. In this tutorial series, we will go over how to create the different types of charts in SwiftUI apps to help visualize different types of data.

In this part of our “Charts in SwiftUI” series, we’ll create a pie chart in SwiftUI. Pie charts are important for visualising data that exists in pieces that together make a whole. For example, we can use a pie chart for our SwiftUI app to analyse sales in a company per model. The total sales would represent the whole, while the contribution of each model represents a piece of the whole. In simple UI terms, what we’re doing here is creating a circle that can be built piece by piece, with each piece representing a specific value. 

Let’s have a look at the pie chart we’ll be creating in this tutorial: 

Let’s go ahead by creating a new SwiftUI project named “MyPieChart”.

Defining the look of a slice 🍕

To start with, let’s define how a slice of the pie chart should look like. 

To do this, let’s add a New-File-SwiftUI View file to our project and name it “PieChartSlice”.

For creating custom shapes, SwiftUI comes with a dedicated Path type. A Path is used for defining the outline of the shape you want.
When you think about it, to create a piece of a circle, we need to know four things

  • Where the centre of the circle is
  • The radius of the circle
  • The angle at which our piece begins
  • And the angle at which our piece ends

With this information, we would be able to create a piece of a circle that looks like this: 

Let’s go ahead and define such a Shape by adding the following properties to our PieChartSlice view.

struct PieChartSlice: View {
     var center: CGPoint
     var radius: CGFloat
     var startDegree: Double
     var endDegree: Double
     var isTouched:  Bool
     var accentColor:  Color
     var separatorColor: Color
     
     var body: some View {
         //...
     }
 }

There are a few properties on our view here, so let’s go over them. The first four properties have been outlined above. 

The isTouched property will be used to equip our pie slices with some “information highlight” functionality. This enables us to attach modifiers that are only active when the slice has been touched. As we did with the bar chart in the last chapter, we will use the .scaleEffect modifier later to enlarge the slice when being touched. Anyway, feel free to experiment with what works for you. 

The accentColor will be used to fill the slice, and the separatorColor to add a visible outline to our slice.

Let’s initialise a preview for our PieChartSlice by passing some dummy data into to PieChartSlice_Previews struct.

struct PieChartSlice_Previews: PreviewProvider {
     static var previews: some View {
         PieChartSlice(center: CGPoint(x: 100, y: 200), radius: 300, startDegree: 30, endDegree: 80, isTouched: true, accentColor: .orange, separatorColor: .black)
     }
 }

To create the slice itself, we need to create a Path instance first.

struct PieChartSlice: View {
     //...
     
     var path: Path {
         var path = Path()
         //...
     }
     
     var body: some View {
         //...
     }
 }

 A Path comes with a few static methods, three of which are of use to us here:

  • func addArc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool) – used to add an arc of a circle specified with the center, radius and angles where it begins and ends. All the parameters are properties of the view already so we can pass them in. The clockwise parameter determines in which direction our arc is drawn
  • func addLine(to: CGPoint) – used to add a line from the current point in our path to the specified point on the screen. We use it to add a line from the end of the arc we have created to the center of our circle
  • func closeSubpath() – used to close and complete the current subpath we have. We use it to draw a line from the center of the circle to the beginning of the arc, thus completing the slice

Let’s implement these functions into our Path property like this:

var path: Path {
     var path = Path()
     path.addArc(center: center, radius: radius, startAngle: Angle(degrees: startDegree), endAngle: Angle(degrees: endDegree), clockwise: false)
     path.addLine(to: center)
     path.closeSubpath()
     return path
 }

Now we are able to use this path for the body of our PieChartSlice view. Let’s add some modifiers to it to style it properly. As mentioned above we use the .scaleEffect modifier later to enlarge the slice when being touched.

var body: some View {
     path
         .fill(accentColor)
         .overlay(path.stroke(separatorColor, lineWidth: 2))
         .scaleEffect(isTouched ? 1.05 : 1)
         .animation(Animation.spring())
 }

Let’s check our preview simulator to see what we got so far:

Building the pie chart 🎂

Let’s build the pie chart step by step as we did with the bar chart in the last chapter. We’ll use the same data type we used in the last chapter, ChartData, which allows us to have organised data with labels and values. For this purpose, let’s create a new File-New-File-Swift file and name it “ChartData”. Let’s add the “ChartData” struct and an array holding some sample entries.

struct ChartData {
     var label: String
     var value: Double
 }
 
 
 let chartDataSet = [
     ChartData(label: "January 2021", value: 150.32),
     ChartData(label: "February 2021", value: 202.32),
     ChartData(label: "March 2021", value: 390.22),
     ChartData(label: "April 2021", value: 350.0),
     ChartData(label: "May 2021", value: 460.33),
     ChartData(label: "June 2021", value: 320.02),
     ChartData(label: "July 2021", value: 50.98)
 ]

Of course, you can use any type you have in your app which will come with a value and a label of some sort.

At this point, we also want to make sure to use our own custom colours for each PieChartSlice. If you prefer to generate random colours, check the PieChart.swift file in our GitHub project where we commented a way to achieve this. Anyway, let’s add another Swift file to our project and name it “Helper”. Make sure to import the SwiftUI framework.

Let’s implement a Color extension to initialise a certain SwiftUI Color by using the corresponding hex code.

import SwiftUI
extension Color {
     init(hex: String) {
         let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
         var int: UInt64 = 0
         Scanner(string: hex).scanHexInt64(&int)
         let a, r, g, b: UInt64
         switch hex.count {
         case 3: // RGB (12-bit)
             (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
         case 6: // RGB (24-bit)
             (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
         case 8: // ARGB (32-bit)
             (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
         default:
             (a, r, g, b) = (1, 1, 1, 0)
         }
         
         
         self.init(
             .sRGB,
             red: Double(r) / 255,
             green: Double(g) / 255,
             blue:  Double(b) / 255,
             opacity: Double(a) / 255
         )
     }
 }

Now we can add an array where we initialise our preferred slice colours:

let pieColors = [
     Color.init(hex: "#2f4b7c"),
     Color.init(hex: "#003f5c"),
     Color.init(hex: "#665191"),
     Color.init(hex: "#a05195"),
     Color.init(hex: "#d45087"),
     Color.init(hex: "#f95d6a"),
     Color.init(hex: "#ff7c43"),
     Color.init(hex: "#ffa600")
]

This example represents the following color scheme:

Now, let’s create the view holding the actual pie chart by creating a new File-New-File-SwiftUI View and naming it “PieChart”.

Here, we initialise our data set, the title for the chart, the separator color to pass into the slices we create, and an array of accent colors that can be used to fill each slice with a specific color. 

struct PieChart: View {
     
     var title: String
     var data: [ChartData]
     var separatorColor: Color
     var accentColors: [Color]
     
     
     var body: some View {
         
     }
 }

Let’s use a fitting title for our PieChart_Previews struct. To make the spaces between the slices transparent we use the system’s background color as the separatorColor. Finally, we use our chartDataSet and our pieColors we created earlier as the data and accentColors.

struct PieChart_Previews: PreviewProvider {
     static var previews: some View {
         PieChart(title: "MyPieChart", data: chartDataSet, separatorColor: Color(UIColor.systemBackground), accentColors: pieColors)
     }
 }

Next, we add some State properties to keep track of the current value to be shown, the current label to be shown, and the touch location. As we did in the last chapter, we will use the touch location to sense when our pie slices are being touched. 

struct PieChart: View {
     
     var title: String
     var data: [ChartData]
     var separatorColor: Color
     var accentColors: [Color]
     
     @State  private var currentValue = ""
     @State  private var currentLabel = ""
     @State  private var touchLocation: CGPoint = .init(x: -1, y: -1)
     
     var body: some View {
         
     }
 }

In the body of our PieChart view, we need to create a VStack that contains the title, the pie chart itself, and a legend at the bottom. 

The title is straightforward:

VStack {
     Text(title)
         .bold()
         .font(.largeTitle)
 }
     .padding()

The actual chart will be embedded in a ZStack with the views for the current label and value. Furthermore, let’s add a GeometryReader. Later on, we will use this GeometryReader to hold the PieChartSlice instances building the actual pie chart.

VStack {
     Text(title)
         .bold()
         .font(.largeTitle)
     ZStack {
         GeometryReader { geometry in
             //We will insert our actual pie chart here
         }
             .aspectRatio(contentMode: .fit)
         VStack  {
             if !currentLabel.isEmpty   {
                 Text(currentLabel)
                     .font(.caption)
                     .bold()
                     .foregroundColor(.black)
                     .padding(10)
                     .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
             }
             
             if !currentValue.isEmpty {
                 Text("\(currentValue)")
                     .font(.caption)
                     .bold()
                     .foregroundColor(.black)
                     .padding(5)
                     .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
             }
         }
             .padding()
     }
 }
     .padding()
 

Let’s talk about the legend for a moment: In a pie chart, the legend is usually the description of what each color in the chart represents. Therefore, we can iterate over the indices of our data and create an HStack that contains an accent color and the label of the data it represents in our data array. 

VStack {
     Text(title)
         //...
     ZStack {
         //...
     }
     VStack(alignment:   .leading)  {
         ForEach(0..<data.count)   {    i in
             HStack {
                 accentColors[i]
                     .aspectRatio(contentMode: .fit)
                     .padding(10)
                 Text(data[i].label)
                     .font(.caption)
                     .bold()
             }
         }
     }
 }
     .padding()

That’s how your PieChart preview should look so far:

Before we can implement our actual pie chart consisting of the combined PieChartSlice’s, there are a few methods we need to implement first. As mentioned in the beginning, pie charts are used to visualise pieces of data that make a whole when combined. Therefore, we need a function that will create a normalised value for each value in our data array. In this function, we will simply represent the value as a fraction of the total of all values in the data array. Let’s add such a function to our Helper.swift file.

func normalizedValue(index: Int, data: [ChartData]) -> Double {
     var total = 0.0
     data.forEach { data in
         total += data.value
     }
     return data[index].value/total
 }

With that function in place, we can then add a new struct called “PieSlice” to our Helper.swift file. We will use this struct to hold the start degree and end degree of each slice according to the normalized value.

struct PieSlice {
     var startDegree: Double
     var endDegree: Double
}

Back to our PieChart view: We can now calculate the pie slices by adding a computed property holding the PieSlice instances inside an array. 

var pieSlices: [PieSlice] {
 
}

To get the pieSlices we create a temporary “slices” array and cycle through our data

var pieSlices: [PieSlice] {
     var slices = [PieSlice]()
     data.enumerated().forEach {(index, data) in
         
     }
 }

If the temporary slices array is empty, we add a slice with a start angle of 0 degrees. The end angle for the slice can be found by using the normalized value to calculate how many degrees we need to add to the start angle of the slice. If the slice we are calculating angles for is not the first in our array, then the start angle should be equal to the end angle of the previous slice. Finally, we use our temporary slices array to initialise the pieSlices.

var pieSlices: [PieSlice] {
     var slices = [PieSlice]()
     data.enumerated().forEach {(index, data) in
         let value = normalizedValue(index: index, data: self.data)
         if slices.isEmpty    {
             slices.append((.init(startDegree: 0, endDegree: value * 360)))
         } else {
             slices.append(.init(startDegree: slices.last!.endDegree,    endDegree: (value * 360 + slices.last!.endDegree)))
         }
     }
     return slices
 }

Now we can generate our actual pie chart. To do this we put all the PieSlice instances into our ZStack’s GeometryReader that’s currently empty. Since the slices have succeeding start and end angles, they will combine to form a pie chart as below: 

VStack {
     //...
     ZStack {
         GeometryReader { geometry in
             ZStack  {
                 ForEach(0..<self.data.count){ i in
                     PieChartSlice(center: CGPoint(x: geometry.frame(in: .local).midX, y: geometry.frame(in:  .local).midY), radius: geometry.frame(in: .local).width/2, startDegree: pieSlices[i].startDegree, endDegree: pieSlices[i].endDegree, isTouched: false, accentColor: accentColors[i], separatorColor: separatorColor)
                 }
             }
         }
         .aspectRatio(contentMode: .fit)
         VStack  {
             //...
         }
             .padding()
     }
     VStack(alignment:   .leading)  {
         ///...
     }
 }
     .padding()

Note: GeometryReader by default takes up as much space as is available in the view. Using the .aspectRatio modifier we ensure that the GeometryReader takes up just the space the content inside it needs. 

Awesome! That’s how your PieChart preview should look so far:

Adding the Highlight Effect 💡

As we did with the bar chart, we’ll attach a gesture modifier to our GeometryReader’s ZStack holding the PieChartSlice instances so we can sense when the user touches a certain slice.

ZStack  {
     //...
 }
     .gesture(DragGesture(minimumDistance: 0)
             .onChanged({ position in
                 //...
             })
             .onEnded({ _ in
                 //...
             })
     )

However, to make good use of this gesture requires some thinking first. Pie charts are circles. Therefore unlike in a bar chart, where we would only be concerned with the drag position along the x-axis, here, we will need the drag position along both the x and y axes. This will enable us to check whether the touch location is within the circle. 

Now, to check whether a specific slice is touched, we will need to calculate the angle at which the touch location is and find the pie slice in which that angle exists. To do this, add the following function to your Helper.swift file.

func angleAtTouchLocation(inPie pieSize: CGRect, touchLocation: CGPoint) ->  Double?  {
     let dx = touchLocation.x - pieSize.midX
     let dy = touchLocation.y - pieSize.midY
     
     let distanceToCenter = (dx * dx + dy * dy).squareRoot()
     let radius = pieSize.width/2
     guard distanceToCenter <= radius else {
         return nil
     }
     let angleAtTouchLocation = Double(atan2(dy, dx) * (180 / .pi))
     if angleAtTouchLocation < 0 {
         return (180 + angleAtTouchLocation) + 180
     } else {
         return angleAtTouchLocation
     }
 }

In this function, we are passing in a parameter that gives us the width (diameter) of our pie chart. We can then use this diameter to find the radius of the pie chart. The dx and dy values give us the distances of the touch location from the x and y axes respectively, thus we now have three sides of a right-angled triangle, so we can use some pythagorean math here to calculate the exact angle of the touch location as well as concentrate on the values of touchLocation that fall within the circle. The atan() method then returns a value that has its units as radians/degrees so we multiply the result by 180/pi to obtain an actual angle value. 

This angle value will run from -180 to 180 degrees, however, we need to convert it to an angle between 0 and 360 degrees. Therefore, for negative values, we will add the value to 180, before adding the result to 180 again. For example, if the angle we have is -40, we will add -40 to 180, getting 140. We can then add this back to 180, getting 320 degrees. 

We can then use this function in another function we insert into our PieChart struct below its body

func updateCurrentValue(inPie   pieSize:    CGRect)  {
     guard let angle = angleAtTouchLocation(inPie: pieSize, touchLocation: touchLocation)    else    {return}
     let currentIndex = pieSlices.firstIndex(where: { $0.startDegree < angle && $0.endDegree > angle }) ?? -1
     
     currentLabel = data[currentIndex].label
     currentValue = "\(data[currentIndex].value)"
 }

We use this function to update the current values by checking for the slice in which this angle is contained and updating the state properties for the current value and the current label. 

We also add a function to our PieChart struct that resets the values.

func resetValues() {
     currentValue = ""
     currentLabel = ""
     touchLocation = .init(x: -1, y: -1)
 }

Now we can call the updateCurrentValue function in the .onChanged modifier for the DragGesture to update the values accordingly, and call the resetValues function in the .onEnded modifier.

.gesture(DragGesture(minimumDistance: 0)
             .onChanged({ position in
                 let pieSize = geometry.frame(in: .local)
                 touchLocation   =   position.location
                 updateCurrentValue(inPie: pieSize)
             })
             .onEnded({ _ in
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                     withAnimation(Animation.easeOut) {
                         resetValues()
                     }
                 }
             })
 )

If we now start a live preview, we can take a look at the information relating to the touched PieChartSlice.

Furthermore, we can add a third function to our PieChart struct that returns a boolean when the touchLocation corresponds to a given index, and use that to detect when a specific slice has been touched. 

func sliceIsTouched(index: Int, inPie pieSize: CGRect) -> Bool {
     guard let angle =   angleAtTouchLocation(inPie: pieSize, touchLocation: touchLocation) else { return false }
     return pieSlices.firstIndex(where: { $0.startDegree < angle && $0.endDegree > angle }) == index
 }

We can now use this function to highlight the specific PieChartSlice by utilising its isTouched property.

ForEach(0..<self.data.count){ i in
     PieChartSlice(center: CGPoint(x: geometry.frame(in: .local).midX, y: geometry.frame(in:  .local).midY), radius: geometry.frame(in: .local).width/2, startDegree: pieSlices[i].startDegree, endDegree: pieSlices[i].endDegree, isTouched: sliceIsTouched(index: i, inPie: geometry.frame(in:  .local)), accentColor: accentColors[i], separatorColor: separatorColor)
 }

Let’s run another live preview to see if this works:

Conclusion 🎊

That’s it! We now have a functional and easily customizable pie chart ready for use in SwiftUI apps. We’ve seen how powerful Path can be for drawing custom shapes and did some thinking exercises for how we can use the touch location to accomplish the same behaviours as the bar chart we made last time. Feel free to experiment and customise it to your liking! 

In the next part [TBA] of our Charts in SwiftUI series, we’ll get into line charts and see how we can use the Path type to create custom lines. Stay tuned! 

As always, we’ve uploaded the source code for the app to GitHub.

You liked the tutorial, and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!

Categories
Uncategorized

Charts in SwiftUI – Part 1: Bar Chart

by Ahmed Mgua (@_mgua)

Charts are important visual elements that help present data to users in a meaningful way. Depending on the type of data, you may decide to use any of the common chart types, for example, bar charts, pie charts, and line charts. In this tutorial series, we will go over how to create the different types of charts in SwiftUI apps to help visualize different types of data.

Today, we start with creating a bar chart in SwiftUI! Bar charts are the simplest type of charts to create in SwiftUI. In UI terms, a bar chart is simply a row of rectangles each with a height corresponding to the value it represents and a text label showing what the value means.

Here’s a sample of the SwiftUI bar chart we are creating:


After this tutorial, you should be able to implement your own custom bar chart for your SwiftUI apps. Let’s get started by creating a new Xcode App project and naming it, for instance, “MyBarChart”.

Define the look of a bar 🍫

First, we need to define how a chart bar chart cell should look like. For this purpose, create a new File – New – SwiftUI View file and name it “BarChartCell”. Then replace the pre-generated Text view with this:

struct BarChartCell: View {       
                
    var value: Double               
    var barColor: Color       
                         
    var body: some View {       
        RoundedRectangle(cornerRadius: 5)       
            .fill(barColor)       
            .scaleEffect(CGSize(width: 1, height: value), anchor: .bottom)       
                
    }       
}

Let’s go over the code in our BarChartCell. A bar in our bar chart is a simple rounded Rectangle that takes two arguments, a value which is a Double, and a barColor which is a Color. The barColor is used to fill the Rectangle with the color of choice. The value is used to determine the height of the rectangle. We use the .scaleEffect modifier here to adjust the height of the Rectangle based on the value the cell represents, and set the anchor to .bottom. This means that each bar will be anchored to the bottom of the frame in the chart, which is the expected behavior for a bar chart.

Next, feed the BarChartCell_Previews with some sample data. Also, clip the size of the preview to the frame of the BarChartCell.

struct BarChartCell_Previews: PreviewProvider {
     static var previews: some View {
         BarChartCell(value: 3800, barColor: .blue)
             .previewLayout(.sizeThatFits)
     }
 }

And this is how a single BarChartCell should look like:

The basic capabilities of our bar chart 📊

Next, create let’s create the view that actually contains our bar chart by creating a new File-New-File and naming it “BarChart”.

The layout of our chart is simple, we have a vertical stack of the chart title at the top, the current value, and the row of bars showing our values.

Our SwiftUI bar chart should be capable of the following things:

  • The bars can sense when they are touched or dragged along
  • The current value updates itself to the value for the currently touched bar
  • There is a label that shows the label of the currently touched bar and moves to stay below the touched bar, which is replaced by the legend when the bar is no longer being touched.

Let’s start building such bar chart in SwiftUI step-by-step.

First, we need to declare three customization properties and one data set property.

import SwiftUI               
                
struct BarChart: View {    
              
    var title: String                  
    var legend: String                   
    var barColor: Color                 
    var data: [ChartData]        
                         
    var body: some View {                    
      Text("Hello, World!")                   
    }       
}

By using those, we can set the title of the chart and the color of the bars to pass down to our BarChartCell. We also pass the data we are using with this bar chart as an array of “ChartData”.

Preparing our data set 💻

“ChartData” should be a custom type we can create to represent values that can be labeled. Let’s create a new File-New-Swift file called “ChartData” to create this model.

struct ChartData {
     var label: String
     var value: Double
 }

Right below our ChartData struct, we can create an array holding some sample ChartData entries.

let chartDataSet = [
     ChartData(label: "January 2021", value: 340.32),
     ChartData(label: "February 2021", value: 250.0),
     ChartData(label: "March 2021", value: 430.22),
     ChartData(label: "April 2021", value: 350.0),
     ChartData(label: "May 2021", value: 450.0),
     ChartData(label: "June 2021", value: 380.0),
     ChartData(label: "July 2021", value: 365.98)
 ]

We can now feed our BarChart_Previews struct with the necessary information including our sample chartDataSet.

struct BarChart_Previews: PreviewProvider {
     static var previews: some View {
         BarChart(title: "Monthly Sales", legend: "EUR", barColor: .blue, data: chartDataSet)
     }
 }

Let’s jump back to our BarChart view. Next, we insert two @State properties for showing the value and label of the bar being currently touched.

@State private var currentValue = ""
@State private var currentLabel = ""

Let’s move on to the body of the BarChart view.

Here, we need to insert a VStack which contains the Text labels for the chart title and the current value State for each BarChartCell initialized.

var body: some View {
         VStack(alignment: .leading) {
             Text(title)
                 .bold()
                 .font(.largeTitle)
             Text("Current value: (currentValue)")
                 .font(.headline)
         }
             .padding()
}

This is how your BarChart view should look so far:

The VStack should also contain a GeometryReader in which we will render the chart.

VStack(alignment: .leading) {                   
    //…                 
    GeometryReader { geometry in       
                
    }                      
}                               
    .padding()

GeometryReader is a container view that has quite a lot of tricks up its sleeve, but for this tutorial, we will use it to find the size of the frame in which the BarChart is rendered.

Determining the height of each bar 📏

Inside the GeometryReader, we add another VStack which contains the HStack of our cells and the moving label of the currently touched bar.

GeometryReader { geometry in
     VStack {
         HStack {
             //Cells
         }
         if currentLabel.isEmpty {
             Text(legend)
                 .bold()
                 .foregroundColor(.black)
                 .padding(5)
                 .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
         } else {
             Text(currentLabel)
                 .bold()
                 .foregroundColor(.black)
                 .padding(5)
                 .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white).shadow(radius: 3))
                  .animation(.easeIn)
         }
     }
}

This is how our BarChart looks so far:

Let’s use a ForEach loop, to iterate over the indices of our array of data. This gives us index 0 through the last index of our data array, so we can use that index in a number of ways.

HStack {                
     ForEach(0..<data.count, id: \.self) { i in       
          
     }                        
}

For each index, we will first instantiate a BarChartCell, giving it a normalized value and the barColor we made. This gives us the varying heights of the bars corresponding to their real values.

To obtain the normalized values, insert the following function below your BarChart‘s body:

func normalizedValue(index: Int) -> Double {
         var allValues: [Double]    {
             var values = [Double]()
             for data in data {
                 values.append(data.value)
             }
             return values
         }
         guard let max = allValues.max() else {
             return 1
         }
         if max != 0 {
             return Double(data[index].value)/Double(max)
         } else {
             return 1
         }
}

This function assigns a value of 1 to the maximum value in our array, and then gets the ratio of each remaining value to our maximum. Therefore, in an array of [30, 50, 100, 75, 60], 100 will be returned as 1, while 30 is returned as 30/100, 50 returned as 50/100, and so on.

We can now use our normalizedValue function to set the height of each BarChartCell using the value parameter:

ForEach(0..<data.count, id: \.self) { i in
    BarChartCell(value: normalizedValue(index: i), barColor: barColor)
        .animation(.spring())
        .padding(.top)
}

Awesome, our BarChart is slowly taking shape! Take a look at how the BarChartCell‘s are generated using normalized values:

Highlighting the selected bar 💡

Last but not least, we want to highlight a certain bar once the user touches it. To do this, we first need to determine whether a bar is touched and then provide a suitable visual cue for that.

For this purpose, let’s add another State property for keeping track of the location on the screen currently being touched.

@State private var touchLocation: CGFloat = -1

Next, we need to create a function that will sense a bar being touched by checking that the touchLocation is greater than the current bar’s beginning but less than the next bar’s beginning and return a boolean value. Let’s insert this function below our normalizedValue function.

func barIsTouched(index: Int) -> Bool {
    touchLocation > CGFloat(index)/CGFloat(data.count) && touchLocation < CGFloat(index+1)/CGFloat(data.count)
}

We can then combine this function with fitting view modifiers and use the return boolean value to perform a myriad of visual changes. For example, we can use the .scaleEffect and .opacity modifier to make the bar slightly wider when it is touched. Of course, you can use anything that you feel would be fitting.

BarChartCell(value: normalizedValue(index: i), barColor: barColor)
    .opacity(barIsTouched(index: i) ? 1 : 0.7)
    .scaleEffect(barIsTouched(index: i) ? CGSize(width: 1.05, height: 1) : CGSize(width: 1, height: 1), anchor: .bottom)
    .animation(.spring())
    .padding(.top)

Okay, we know have the capability to know whether a certain bar is touched and to tell this specific BarChartCell to perform some visual changes when being touched.

Detecting the user’s gestures 👉

To make the highlight feature work, we need to know which bar is touched. To do this, we need to use the index of the current bar and the touchLocation State property to determine where on the screen the user is touching. For this purpose, we attach a .gesture modifier to our HStack containing the BarChartCell‘s while using the DragGesture. We use a minimum distance of zero. This means that the drag gesture will be activated for a tap as well.

HStack {                     
    //…                     
}                  
    .gesture(DragGesture(minimumDistance: 0) 
      
    )

The DragGesture comes with two modifiers of interest to us: the .onChanged and .onEnded modifiers. We use them to write code that runs when the drag gesture is activated and when it stops. Both of them come with a closure argument that gives you the position of the drag gesture or where it stopped to use in the closure.

.gesture(DragGesture(minimumDistance: 0)                
    .onChanged({ position in       
         
    })                                       
    .onEnded({ position in                                
                
    })                            
)

This position comes with a location attribute, which we can check to know the location coordinates of the drag gesture on the screen. However, we only want to know about the touches inside the frame where the bar chart is.

Here is where we can see a little bit of the magic of GeometryReader.
The GeometryReader provides us with a frame(in: _) function that reads the size of the frame we give it. We can then use that frame size to determine the touch or drag position along the axis of the given frame, but not anything outside it.

.onChanged({ position in
     let touchPosition = position.location.x/geometry.frame(in: .local).width
 })

For the .onChanged modifier, we need the obtained touchPosition to update the current value of the touchLocation State.

.onChanged({ position in               
    let touchPosition = position.location.x/geometry.frame(in: .local).width       
                        
    touchLocation = touchPosition                                
})

The touchLocation is a value between 0 and 1 with 0 being the left edge of the frame and 1 being the right edge of the frame. We create a function to update the current value, which will in turn update the UI
accordingly. Let’s insert such a function below our barIsTouched function,

func updateCurrentValue()    {
         let index = Int(touchLocation * CGFloat(data.count))
         guard index < data.count && index >= 0 else {
             currentValue = ""
             currentLabel = ""
             return
         }
         currentValue = "\(data[index].value)"
         currentLabel = data[index].label
     }

The updateCurrentValue function uses the value of the touchLocation to find the index of the value in our data array. So for a touchLocation of 0.5, our index will be the halfway index in the data array. We need to be careful here to make sure that the index does not go beyond the first or last index in our data array, so we use a guard statement to keep that in check.

Let’s use this function to complete the .onChanged closure.

.onChanged({ position in
    let touchPosition = position.location.x/geometry.frame(in: .local).width
     touchLocation = touchPosition    
    updateCurrentValue()                                                                
})

For the .onEnded modifier, we can omit the position argument since we will simply reset the states to show that the drag gesture has ended and the visual cues can stop. Let’s set the touchLocation to a negative value here to mean “the user is touching outside the screen”. To make the transition more sensible, we can reset the values after a short delay, say 0.5s.

.onEnded({ _ in                 
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {              
        withAnimation(Animation.easeOut(duration: 0.5)) {        
            touchLocation = -1          
            currentValue  =  ""       
            currentLabel = ""           
        }                 
     }       
})

Let’s outsource the code into its own function as well ..

func resetValues() {
         touchLocation = -1
         currentValue  =  ""
         currentLabel = ""
}

… and call it from the .onEnded closure like this:

.onEnded({ _ in                  
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {               
        withAnimation(Animation.easeOut(duration: 0.5)) {            
            resetValues()              
        }                 
    }       
})

Finally, for a little flair, we can also use the touchLocation to offset the label for the current bar using a new function that takes the width of the frame and uses it to determine the position the label should be displayed. So let’s add this last function to the BarChart struct.

func labelOffset(in width: CGFloat) -> CGFloat {
         let currentIndex = Int(touchLocation * CGFloat(data.count))
         guard currentIndex < data.count && currentIndex >= 0 else {
             return 0
         }
         let cellWidth = width / CGFloat(data.count)
         let actualWidth = width -    cellWidth
         let position = cellWidth * CGFloat(currentIndex) - actualWidth/2
         return position
}

We can then pass in the function to an .offset modifier applied the label Text view:

Text(currentLabel)
    //…
    .offset(x: labelOffset(in: geometry.frame(in: .local).width))
    .animation(.easeIn)

Our SwiftUI bar chart is finally completed. Let’s run a live preview to see if everything works.

Conclusion 🎉

There we have it! We now have a simple bar chart that we can use in our SwiftUI apps, that can adjust according to different frames. Feel free to experiment!

In the next part, we’ll dive into pie charts in SwiftUI and see more of what GeometryReader can do, and start getting into Paths and Shapes in SwiftUI.

We’ve uploaded the source code for the app to GitHub.

You liked the tutorial, and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!

Categories
Uncategorized

How to create a Search Bar with SwiftUI

Hello and welcome to a new SwiftUI tutorial. Today we will learn how to build a fully functional search bar for our SwiftUI apps. We won’t use the UIKit framework for this but build our search bar using native SwiftUI views only.

This is how our finished search bar will look like:

Unfortunately, SwiftUI does not (yet) offer us a native search bar that we can use for our apps. However, this should not prevent us from using them in our SwiftUI apps. With a few tricks we can easily create and customise our own SwiftUI search bar.

Some preparations 💻

Let’s start with creating a new Xcode project and naming it “MyFruits”.

Before we implement our search bar, we need a corresponding list that contains all possible search results. For this purpose, we use the pre-generated ContentView

We put all possible matches into an array, which we initialize inside our ContentView.

struct ContentView: View {
     
     let myFruits = [
         "Apple 🍏", "Banana 🍌", "Blueberry 🫐", "Strawberry 🍓", "Avocado 🥑", "Cherries 🍒", "Mango 🥭", "Watermelon 🍉", "Grapes 🍇", "Lemon 🍋"
     ]
     
     var body: some View {
         Text("Hello, world!")
             .padding()
     }
 }

Now we replace the pre-generated Text view with a List. We feed this List with a ForEach loop that produces one Text view for each String in our myFruits array.

var body: some View {
     List {
         ForEach(myFruits, id: \.self) { fruit in
                 Text(fruit)
             }
     }
         .listStyle(GroupedListStyle())
 }

We also add a navigation bar to our ContentView by wrapping the List in a NavigationView and using the .navigationTitle modifier.

NavigationView {
              List {
                  ForEach(myFruits, id: .self) { fruit in
                      Text(fruit)
                  }
              }
                  .listStyle(GroupedListStyle())
                  .navigationTitle("MyFruits")
          }

That’s it. Our finished ContentView should now look like this:

Creating a custom Search Bar view 🔦

Let’s start by building the interface of our search bar. 

In the end, we want our search bar to look like this:

The background of our tab bar should be a lighter/darker Gray in light/dark mode. To do this, we quickly create a custom color set in our Assets folder and name it “LightGray”.

Back to our ContentView: We want to place our search bar right above the List. Therefore, wrap the List into a VStack like this:

VStack {
     List(myFruits, id: \.self) { fruit in
         ForEach(myFruits, id: \.self) { fruit in
             Text(fruit)
         }
     }
         .listStyle(GroupedListStyle())
         .navigationTitle("MyFruits")
 }

Basically, our search bar should simply consist of a gray background on which we then place an Image view for the “magnifying glass” icon and a TextField. 

For this purpose, we place a ZStack into the VStack. Let’s choose the .leading alignment mode and apply a .frame to it. For the background, we simply use a gray Rectangle.

VStack(alignment: .leading) {
     ZStack {
         Rectangle()
             .foregroundColor(Color("LightGray"))
     }
         .frame(height: 40)
         //...
 }

We now place an HStack on top of the Rectangle. This HStack consists of an Image view with the “magnifyingglass” icon from the SFSymbols app. 

ZStack {
     HStack {
         Image(systemName: "magnifyingglass")
     }
     Rectangle()
         .foregroundColor(.gray)
 }

Next to it, we want to place our TextField. Accordingly, we require a property for our ContentView to be bound to the TextField.

@State var searchText = ""

Now we can initialize our TextField as usual. We also use a .padding to increase the distance between the Image and the TextField and change their .foregroundColor.

HStack {
     Image(systemName: "magnifyingglass")
     TextField("Search ..", text: $searchText)
 }
     .foregroundColor(.gray)
     .padding(.leading, 13)

Finally, we round off the corners of the entire ZStack and add some .padding to all sides again.

ZStack {
     //...
 }
     .frame(height: 40)
     .cornerRadius(13)
     .padding()

And this is what our finished search bar looks like:

Let’s outsource the search bar by CMD-clicking on our ZStack and selecting “Extract subview”. Let’s name the extracted subview “SearchBar”

struct SearchBar: View {
     
     var body: some View {
         ZStack {
             Rectangle()
                 .foregroundColor(Color("LightGray"))
             HStack {
                 Image(systemName: "magnifyingglass")
                 TextField("Search ..", text: $searchText)
             }
             .foregroundColor(.gray)
             .padding(.leading, 13)
         }
             .frame(height: 40)
             .cornerRadius(13)
             .padding()
     }
 }

Let’s add the corresponding searchText Binding to the extracted SearchBar

struct SearchBar: View {
     
     @Binding var searchText: String
     
     var body: some View {
         //...
     }
 }

… and initialise it from the ContentView.

VStack(alignment: .leading) {
     SearchBar(searchText: $searchText)
     //...
 }

Changing our interface while searching 👨‍💻

Once the user taps on the TextField to start searching, we want to change the navigation bar title. We also want to provide a “Cancel” button while searching.

To do this, we need to be aware of when the user starts the search. For this purpose, we add a corresponding State to our ContentView.

@State var searching = false

We pass this on to our SearchBar as a Binding.

@Binding var searching: Bool

Again, we initialize this accordingly in our ContentView.

SearchBar(searchText: $searchText, searching: $searching)

The user starts its search process by tapping the TextField in the SearchBar. We can easily detect this by adding the “onEditingChanged” closure to the TextField. This will be executed as soon as the user starts editing the TextField. As soon as this happens, we change the searching property to true.

TextField("Search ..", text: $searchText) { startedEditing in
     if startedEditing {
         withAnimation {
             searching = true
         }
     }
 }

Consequently, we want to set searching to false as soon as the user taps the return key of the keyboard. To do this, we append the “onCommit” closure to our TextField:


 TextField("Search ..", text: $searchText) { startedEditing in
     if startedEditing {
         withAnimation {
             searching = true
         }
     }
 } onCommit: {
     withAnimation {
         searching = false
     }
 }

Back to our ContentView. Once the search has started, we want to change the existing .navigationTitle

.navigationTitle(searching ? "searching" : "MyFruits")

We also like to provide a “Cancel” button during the search process. For this, we use the .toolbar modifier. 

VStack(alignment: .leading) {
     //...
         .navigationTitle(searching ? "Searching" : "MyFruits")
         .toolbar {
             if searching {
                 Button("Cancel") {
                     searchText = ""
                     withAnimation {
                         searching = false
                     }
                 }
             }
         }
 }
 

Let’s see if this works:

As you can see, the keyboard disappears when we tap on the return key (because then the “onCommit” closure of our TextField gets executed). However, the keyboard doesn’t disappear when we tap on the “Cancel” Button in our NavigationBar. 

Resigning the keyboard ⌨️

To fix this, we need to find a way to hide the keyboard manually. There is no native SwiftUI feature for this, so again we have to rely on the UIKit.

Just add the following extension to your ContentView.swift file.

extension UIApplication {
      func dismissKeyboard() {
          sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
      }
  }
 

The code is quite complicated. Basically, it says that the “control” that currently commands the keyboard should stop commanding it. See this post if you are interested in the topic further.

We can now add the following line to our “Cancel” Button.

Button("Cancel") {
     searchText = ""
     withAnimation {
         searching = false
         UIApplication.shared.dismissKeyboard()
     }
 }

If we now tap on the “Cancel” Button, the keyboard disappears!

To provide a better user experience, we also want to hide the keyboard while the user is scrolling through the search results.

To do this, we add a .gesture to the List in our ContentView. There are many different gestures available in SwiftUI, for example, the TapGesture or the onLongPressGesture. Here we use a DragGesture.

List(myFruits, id: \.self) { fruit in
     //...
 }
     //...
     .gesture(DragGesture()
          
     )

Once the user performs a drag gesture across the List, we want to dismiss the keyboard. For this purpose, we use the .onChanged modifier:

.gesture(DragGesture()
             .onChanged({ _ in
                 UIApplication.shared.dismissKeyboard()
             })
 )

When we run our app now, the keyboard will also be hidden when swiping over the list while performing a search.

Filtering our List’s results 🕵️‍♀️

Okay, we’re almost there. All we need to do now is implement the actual search functionality. 

While the user is searching we want to display the search results instead of all fruits available. For this, we modify our ForEach loop to only display those Strings in our myFruits array that match the searchText.

List {
     ForEach(myFruits.filter({ (fruit: String) -> Bool in
         return fruit.hasPrefix(searchText) || searchText == ""
     }), id: \.self) { fruit in
         Text(fruit)
     }
 }

Using the filter function we cycle through String in our myFruits array. Only if the particular String has the same initial letters as the searchText (or if there is no searchText at all), we use it for our ForEach loop.

We can now run our app and see if our search bar works!

Conclusion 🎊

Awesome! You have just learned how to build your own search bar using SwiftUI views only. We hope you have fun applying what you’ve learned and implementing your own custom search bars for your SwiftUI apps.

We’ve uploaded the source code for the app to GitHub.

If you want to learn to combine a search bar with a fully-functional To-Do app check out the corresponding chapter in our Interactive Mastering SwiftUI Book. In this you’ll also learn how to build more awesome apps such as a chat messenger and a photo editor with SwiftUI!

Categories
Uncategorized

How to use Google Maps in SwiftUI apps

by Ahmed Mgua (@_mgua)

In this tutorial we’ll create a simple reusable Google Maps view that you can use in your SwiftUI apps. The process is simple but it takes a few steps:

  1. Use CocoaPods to get the Google Maps SDK for our Xcode project
  2. Obtain and set up the API key using the Google Cloud Platform
  3. Create a custom AppDelegate to prepare the API calls
  4. Create a reusable Google Maps view we can use in our SwiftUI app

This tutorial is also suitable for beginners who have not yet worked with CocoaPods.

Disclaimer: Google Maps cannot be directly integrated into SwiftUI views. Therefore, we choose a workaround by using the UIViewRepresentable protocol. Also note that you will need a Google Cloud Platform account. However, for the purposes of this tutorial, a free trial account is sufficient (more on that below).

Installing the GoogleMaps SDK using CocoaPods 🌎

Let’s start creating a new Xcode project. Select “App” under the “Multiplatform” or your preferred platform tab. Name your project “GoogleMapsDemo” and make sure to use SwiftUI as the “Interface” and “Life Cycle” mode. Then go ahead creating your project at your preferred location.

After you created your project quit Xcode by pressing CMD-Q and open your Terminal. Type in the following code:

sudo gem install cocoapods

CocoaPods are written in Ruby, so don’t worry if the code looks foreign to you. Enter your password when prompted. Installing CocoaPods may take a few minutes. Once the installation process is complete, finish the setup by typing in the following code:

pod setup --verbose

This step could take a while as it clones the MasterSpecs repo from CocoaPods. By using the — verbose command we tell the Terminal to return the progress of this process.

Once the setup is complete, we need to navigate to our project folder. You can do this by typing in cd ~/…… completing it with the path to your project folder and pressing enter. You can also type cd and drag and drop your project folder and then press enter.

Once you’re in the project folder, type in this line and press enter.

pod init

This creates a PodFile in your project folder. You can find this PodFile by opening your project folder in the Finder. However, don’t close your Terminal yet. Open the PodFile using the Text Editor.

The code inside the file should look like this:

Uncomment the next line to define a global platform for your project
 platform :ios, '9.0'
 target 'GoogleMapsDemo' do
   # Comment the next line if you don't want to use dynamic frameworks
   use_frameworks!
 # Pods for GoogleMapsDemo
 end

Now we need to specify the pod we would like to install which is the GoogleMaps pod. To do this, add the following line to your PodFile:

Uncomment the next line to define a global platform for your project
 platform :ios, '9.0'
 target 'GoogleMapsDemo' do
   # Comment the next line if you don't want to use dynamic frameworks
   use_frameworks!
 # Pods for GoogleMapsDemo
      pod 'GoogleMaps', '4.1.0'
 end

Next, save and close the PodFile. To install the specified pods we go back to the terminal and enter the following command:

pod install --verbose

Using the –verbose command again the terminal will show us the progress as it downloads the dependencies. Once the installation is complete, you should see a new file with the extension .xcworkspace in your project folder.

Important: From now on, use this file to open your project and not the .xcodeproj file.

Setting up the Google Maps SDK 🗺

With the PodFile set up and the Google Maps SDK installed, we now need to generate our personal API key using the Google Cloud Platform. To do this, go to the Google Cloud Platform. Create a free trial account if not already done yet.

Once you logged into your Google Cloud Platform account, click on the menu button in the top left corner, select “APIs & Services” and choose “Credentials”.

In the credentials section, click on “+ Create Credentials” and select “API Keys”.

Next, select the created API key and click the Edit button.

We now need to setup the restrictions for our API key. This allows us to make calls from our SwiftUI apps using the generated API key. Select iOS apps in the restrictions list.

Then click on “Add Item” and insert the Bundle Identifier of your Xcode project. Finally, click on “Done”

Hint: You can find Bundle Identifier in the “General Tab” of your project file.

At this point, we can go back to the “Credentials” tab and copy the API key to our clipboard.

Then switch from the “Credentials” to the “Dashboard” tab .

Next, click on “Enable APIs and Services” button at the top of the API dashboard. Now the API library opens up. Select “Maps SDK for iOS” and click on the “Activate” button.

Finally, we are ready to use the Google Maps API in our SwiftUI project.

Creating a custom App Delegate 🧑🏽‍💻

Now we go back to our Xcode project and open up our SwiftUIDemoApp.swift file. Let’s insert the API key we copied to our clipboard earlier. Also make sure to import the Google Maps library at the top of the SwiftUIDemoApp.swift file.

import SwiftUI
import GoogleMaps
 
 let APIKey = "INSERT_YOUR_API_KEY_HERE"
 
 @main
 struct SwiftUIDemoApp: App {
     var body: some Scene {
         WindowGroup {
             ContentView()
         }
     }
 }

If you are still working with SwiftUI 1.0, find the didFinishingLaunchingWithOptions function in the AppDelegate class and insert the following code.

GMSServices.provideAPIKey("INSERT_YOUR_API_KEY_HERE")

Inn SwiftUI 2.0 however, apps are initialized using the @main attribute. Thus, we need to create our own, custom App Delegate. We can do this by adding a new class to our SwiftUIDemoApp.swift file that inherits from NSObject and conforms to the UIApplicationDelegate protocol. Inside this class, we implement the didFinishingLaunchingWithOptions function (don’t worry if you don’t know it, just type in “didFinishLa..” and select it in the autocomplete list), inside which we will put our API keys.

class AppDelegate: NSObject, UIApplicationDelegate    {
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
         GMSServices.provideAPIKey(APIKey)
         return true
     }
 }

Next, we need to initialize our custom AppDelegate at the app’s entry point. We do this by adding a UIApplicationDelegateAdaptor property to our @main struct and passing our AppDelegate class.

@main
 struct SwiftUIDemoApp: App {
     
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
     
     var body: some Scene {
         WindowGroup {
             ContentView()
         }
     }
 }

Awesome, we are now all set to create our actual Google Maps view.

Creating the MapView 🧭

Let’s start by creating a new SwiftUI View file and naming it appropriately, for example GoogleMapsView. Again, make sure to import the GoogleMaps library.

import SwiftUI
import GoogleMaps
 
 struct GoogleMapsView: View {
     var body: some View {
         Text("Hello, World!")
     }
 }
 
 struct GoogleMapsView_Previews: PreviewProvider {
     static var previews: some View {
         GoogleMapsView()
     }
 }

The struct that Xcode gives us is a SwiftUI View. However, Google Maps views cannot be inserted into SwiftUI directly. Therefore we need a workaround using the UIViewRepresentable protocol.

Thus, we remove the body of our GoogleMapsView and replace the View conformance with UIViewRepresentable. At this point, you can also delete the GoogleMapsView_Previews struct.

struct GoogleMapsView: UIViewRepresentable {
     
 }

Inside this struct, we need to implement the makeUIView and updateUIView functions to conform to the UIViewRepresentable protocol.

Hint: The makeUIView struct is used to initialise and return the specific UIView. The updateUIView updates the UIView every time it gets called . If you want to dig deeper into the UIViewRepresentable protocol, make sure to check out this tutorial as well.

struct GoogleMapsView: UIViewRepresentable {
     
     func makeUIView(context: Context) -> GMSMapView {
         
     }
     
     func updateUIView(_ uiView: GMSMapView, context: Context) {
         
     }
 }

The updateUIView function doesn’t need to do anything for now, so you can leave that empty. The makeUIView function however, needs to return a GMSMapView instance.

func makeUIView(context: Context) -> GMSMapView {
         let mapView = GMSMapView(frame: CGRect.zero)
         
         return mapView
     }

Next, we need to provide a camera position and the amount of zoom we want the view to show. The camera position is basically the location we want to show in latitude and longitude, and the zoom level sets the span that the display width will show.

For example, we can create an extension on GMSCameraPosition that adds the location for London.

extension GMSCameraPosition  {
     static var london = GMSCameraPosition.camera(withLatitude: 51.507, longitude: 0, zoom: 10)
 }

Finally, use this extension to set the camera in our makeUIView function and pass it to mapView using the camera argument:

 func makeUIView(context: Context) -> GMSMapView {
         let camera = GMSCameraPosition.london
         
         let mapView = GMSMapView(frame: CGRect.zero, camera: camera)
         
         return mapView
     }

We can treat the created GoogleMapsView just like a normal SwiftUI view. For instance, let’s insert GoogleMapsView instance into our ContentView. By using the .edgesIgnoringSafeArea modifier we make sure that the GoogleMapsView covers the whole screen of the device the app runs on.

struct ContentView: View {
     var body: some View {
         GoogleMapsView()
             .edgesIgnoringSafeArea(.all)
     }
 }

Once you’re finished, we can launch our app or start a live preview of our ContentView containing the GoogleMapsView.

Conclusion 🎊

Awesome. You now know how to integrate Google Maps into your SwiftUI app using CocoaPods, the Google Maps SDK and the UIViewRepresentable protocol.

We’ve uploaded the source code for the app to GitHub.

If you want to learn how to use native Apple Maps in SwiftUI check out our Interactive Mastering SwiftUI Book. Here you’ll learn how to interact with Apple Maps, implement location functionality and retrieve images via the Flickr API depending on the user’s location.

Categories
Uncategorized

How to create a custom Tab Bar in SwiftUI

Updated for Xcode 12 and SwiftUI 2.0 ✅

Hello and welcome to a new SwiftUI tutorial! In SwiftUI, it’s super easy to embed your app’s views into a tab bar using a TabView. However, We are limited to the standard tab bar design that Apple provides us with. But the in-house tab bar can get boring and sometimes doesn’t offer the functionality we need for our app. In this tutorial, we will learn how to create our own custom and fully customizable tab bar. By the way: we will not use a UITabBarController for this but implement the navigation logic only with SwiftUI and from scratch.

Our finished tab bar will look like this:

With the knowledge from this article you should be able to create almost any tab bar you can imagine using SwiftUI only!

Let’s get started! After opening Xcode 12 and creating a new “App” under “iOS” or “Multiplatform”, we can begin preparing our custom tab bar. For the purpose of this tutorial, we will use the default ContentView.swift file.

We will need three different colors for our tab bar. Let’s put them in our Assets.xcassets folder right now. For our app, we need one color for the big “plus” icon of the tab bar, one for the background of the tab bar, and one for the selected tab bar icon (each one with respect to dark and light mode).

Preparing our ContentView 👨‍💻

By using our tab bar, we will later be able to jump between two different views, a “Home” view, and a “Settings” view. By default, we want to present the “Home” view. This view should simply consist of a Text view reading “Home”. For this, we can replace the String in the default “Hello Word” Text view. We also remove the .padding modifier.

import SwiftUI
 struct ContentView: View {
     var body: some View {
         Text("Home")
     }
 }

We want to place the tab bar at the bottom of the ContentView. Therefore, we have to wrap our Text view into a VStack. To make sure that the Text is always centered, we add two Spacer views.

VStack {
     Spacer()
     Text("Home")
     Spacer()
 }

We almost finished preparing our ContentView. Finally, we want to have the capability to know the width and height of the ContentView depending on the particular device the app runs on. We need to know this to adjust the size of the tab bar dynamically. For this purpose, we wrap our VStack into a GeometryReader.

GeometryReader { geometry in
     VStack {
         Spacer()
         Text("Home")
         Spacer()
     }
 }

The GeometryReader reads out its parent view’s size dimensions. In our case, that should be the ContentView as the overall super-view, the one that covers the entire screen.

Note: Don’t worry if the Text view is not centered anymore after embedding the VStack into the GeometryReader. We will fix this in a moment.

We finished our preparations. Now it’s time to actually design our custom tab bar!

Composing our Tab Bar 🎨🖌

Our tab bar should contain five different icons arranged horizontally. Therefore, we insert an HStack into our VStack below the last Spacer view.

VStack {
     Spacer()
     Text("Home")
     Spacer()
     HStack {
         
     }
 }

Next, we can set our HStack to be always one-eighth of the ContentView‘s height. We also add a white background with a smooth shadow effect to it.

HStack {
     
 }
     .frame(width: geometry.size.width, height: geometry.size.height/8)
     .background(Color("TabBarBackground").shadow(radius: 2))

Each tab bar icon should consist of an Image view and a Text. The icon for the Image view is taken from the SF Symbols catalog. For the first icon we use the “homekit” symbol.

HStack {
     VStack {
         Image(systemName: "homekit")
             .resizable()
             .aspectRatio(contentMode: .fit)
             //Since we have five icons, we want everyone to be one-fifth of the ContentView's width
             .frame(width: geometry.size.width/5, height: geometry.size.height/28)
             .padding(.top, 10)
         Text("Home")
             .font(.footnote)
         Spacer()
     }
 }

Your preview should now look like this:

To make it reusable, outsource CMD-click on the VStack and select “Extract subview”. Call the subview “TabBarIcon”. We need to derive the width, height, symbol name tab name of each TabBarIcon from the ContentView that hosts the particular TabBarIcon instance. So let’s add these properties to the TabBarIcon view and replace the fixed values.

struct TabBarIcon: View {
     
     let width, height: CGFloat
     let systemIconName, tabName: String
     
     
     var body: some View {
         VStack {
             Image(systemName: systemIconName)
                 .resizable()
                 .aspectRatio(contentMode: .fit)
                 .frame(width: width, height: height)
                 .padding(.top, 10)
             Text(tabName)
                 .font(.footnote)
             Spacer()
         }
     }
 }

Next, we need to pass those values to the initalised TabBarIcon inside our ContentView‘s HStack:

HStack {
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "homekit", tabName: "Home")
 }

Let’s add four more TabBarIcons to our ContentView!

HStack {
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "homekit", tabName: "Home")
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "heart", tabName: "liked")
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "plus", tabName: "Add")
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "waveform", tabName: "Records")
     TabBarIcon(width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "person.crop.circle", tabName: "Account")
 }

Our tab bar slowly takes shape. However, it is (depending on the device the app runs on) slightly away from the bottom edge. This is because, by default, a SwiftUI view’s content stays inside the so-called safe area. This prevents our tab bar from reaching the lower edge of our screen. But we can explicitly tell our SwiftUI view to exceed the safe area’s lower boundary by adding the following modifier to the overall VStack:

GeometryReader { geometry in
     VStack {
         //...
     }
         .edgesIgnoringSafeArea(.bottom)
 }

Finally, we increase the spacing between our TabBarIcons by adding some negative horizontal .padding to each one.

struct TabBarIcon: View {
     
     //...
     
     
     var body: some View {
         VStack {
             //...
         }
             .padding(.horizontal, -4)
     }
 }

Next, we want to replace the “Plus”-TabBarIcon with a slightly shifted plus button. We create this by inserting a ZStack between the two icons. The first view inside this ZStack should simply be a white Circle:

ZStack {
     Circle()
         .foregroundColor(.white)
         .frame(width: geometry.size.width/7, height: geometry.size.width/7)
         .shadow(radius: 4)
 }

On top of this Circle, we stack our actual plus icon:

ZStack {
     Circle()
         //…
     Image(systemName: "plus.circle.fill")
         .resizable()
         .aspectRatio(contentMode: .fit)
         .frame(width: geometry.size.width/7-6 , height: geometry.size.width/7-6)
         .foregroundColor(Color("DarkPurple"))
 }

To ensure that the whole plus tab bar icon is placed slightly above the tab view, we apply the .offset modifier to our ZStack. Our ZStack should be shifted upwards by half the height of the tab bar. Since our tab view is one-eight as high as the screen, we write:

ZStack {
     //…
 }
     .offset(y: -geometry.size.height/8/2)

Awesome, we finished designing our own custom tab bar. It’s simple as that! Your preview should now look like this:

Implementing the navigation logic ⛓

Now it’s time to write the code for the navigation logic. How to navigate between views independently in SwiftUI is shown in detail in this tutorial. Therefore, we’ll keep it brief in the following.

To be able to switch back and forth between the different views, we need a “manager” that tells our ContentView which view it should display. For this purpose, we create a new File-New-File and select Swift file. We call this file “ViewRouter”. 

Next, we import the SwiftUI framework and create a class named ViewRouter that conforms to the ObservableObject protocol.

import SwiftUI

class ViewRouter: ObservableObject {
   
}

The user should be able switch between four tabs. To represent those tabs in our ViewRouter, we prepare a corresponding enum which we place right below our ViewRouter class.

enum Page {
     case home
     case liked
     case records
     case user
 }

Within our ViewRouter, we need a variable to keep the observing view(s) updated about which Page should currently be displayed. Thus, we declare a variable currentPage. By default, we want to display the .home tab.

class ViewRouter: ObservableObject {
     
     @Published var currentPage: Page = .home
     
 }

With the @Published property wrapper, we notify all observing views to update themselves whenever the Page assigned to the currentView variable changes.

That’s it! We can now use a ViewRouter as a @StateObject within our ContentView.

struct ContentView: View {
     
     @StateObject var viewRouter: ViewRouter
     
     var body: some View {
         //...
     }
 }
 
 struct ContentView_Previews: PreviewProvider {
     static var previews: some View {
         ContentView(viewRouter: ViewRouter())
     }
 }

We create the actual ViewRouter instance in our App struct and pass it to the observing ContentView like this:

@main
 struct CustomTabBarTempApp: App {
     
     @StateObject var viewRouter = ViewRouter()
     
     var body: some Scene {
         WindowGroup {
             ContentView(viewRouter: viewRouter)
         }
     }
 }

As said, make sure you read this tutorial if you don’t really understand the navigation logic we are using!

Depending on the viewRouter’s currentView variable, we want to show an appropriate Text view as a placeholder. Therefore, we replace our current Text view within the two Spacers with the following switch-statement:

Spacer()
 switch viewRouter.currentPage {
 case .home:
     Text("Home")
 case .liked:
     Text("Liked")
 case .records:
     Text("Records")
 case .user:
     Text("User")
 }
 Spacer()

To be able to switch between the different tabs, we need to access the viewRouter from each TabBarIcon. To know which Page is assigned to the particular TabBarIcon instance we also add a property to our TabBarIcon struct.

struct TabBarIcon: View {
     
     @StateObject var viewRouter: ViewRouter
     let assignedPage: Page
     
     //...
     
     
     var body: some View {
         //...
     }
 }

Now we need to update the TabBarIcon initializations in our ContentView.

HStack {
     TabBarIcon(viewRouter: viewRouter, assignedPage: .home, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "homekit", tabName: "Home")
     TabBarIcon(viewRouter: viewRouter, assignedPage: .liked, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "heart", tabName: "Liked")
     ZStack {
         //…
     }
         .offset(y: -geometry.size.height/8/2)
     TabBarIcon(viewRouter: viewRouter, assignedPage: .records, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "waveform", tabName: "Records")
     TabBarIcon(viewRouter: viewRouter, assignedPage: .user, width: geometry.size.width/5, height: geometry.size.height/28, systemIconName: "person.crop.circle", tabName: "Account")
 }

We can enable the user to navigate between the different tabs by adding a tap gesture to the VStack in the TabBarIcon view struct that assigns the viewRouter’s currentView variable to the assignedPage.

struct TabBarIcon: View {
     
     //...
     
     
     var body: some View {
         VStack {
             //...
         }
             //...
             .onTapGesture {
                 viewRouter.currentPage = assignedPage
             }
     }
 }

The @Published property wrapper’s functionality causes our observing ContentView to rebuild itself with eventually showing us the corresponding Text view!

Awesome, we’re now able to jump between the different TabBarIcons by tapping on them!

To indicate to the user which view is currently shown, we can conditionally highlight the corresponding TabBarIcon. For example, when our “Home” view is currently being shown, we want the corresponding TabBarIcon with its Image and Text view to be darkened. Otherwise, we want it to be gray (and vice versa for dark mode). We can achieve this by adding the following modifier to it:

struct TabBarIcon: View {
     
     //...
     
     
     var body: some View {
         VStack {
             //...
         }
             //...
             .foregroundColor(viewRouter.currentPage == assignedPage ? Color("TabBarHighlight") : .gray)
     }
 }

Awesome, our tab bar now indicates which view is currently being shown!

Creating the pop-up menu ➕

Finally, we want to display a cool pop-up menu when the user taps the plus sign icon.

For keeping track of whether the menu should be displayed, we add a corresponding @State property to our ContentView, right below our viewRouter @StateObject.

@State var showPopUp = false

When this State is true, we want to display the menu on top of our tab bar (we will offset its position in a moment). Therefore, we wrap our HStack into a ZStack.

ZStack {
     HStack {
         //…
     }
         //…
 }

For the menu itself, you can add the following struct right below your ContentViews_Previews struct:

struct PlusMenu: View {
   
  let widthAndHeight: CGFloat
   
  var body: some View {
    HStack(spacing: 50) {
      ZStack {
        Circle()
          .foregroundColor(Color("DarkPurple"))
          .frame(width: widthAndHeight, height: widthAndHeight)
        Image(systemName: "record.circle")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .padding(15)
          .frame(width: widthAndHeight, height: widthAndHeight)
          .foregroundColor(.white)
      }
      ZStack {
        Circle()
          .foregroundColor(Color("DarkPurple"))
          .frame(width: widthAndHeight, height: widthAndHeight)
        Image(systemName: "folder")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .padding(15)
          .frame(width: widthAndHeight, height: widthAndHeight)
          .foregroundColor(.white)
      }
    }
  }
}

If you want to learn how to create floating menus in SwiftUI, take a look at this tutorial!

Depending on the showPopUp State, we can now initialize the PlusMenu on top of our tab bar.

ZStack {
     if showPopUp {
         PlusMenu(widthAndHeight: geometry.size.width/7)
     }
     HStack {
         //...
     }
         //...
 }

We position the PlusMenu above our tab bar by using the .offset modifier again.

if showPopUp {
     PlusMenu(widthAndHeight: geometry.size.width/7)
         .offset(y: -geometry.size.height/6)
 }

We want to toggle the State by tapping on the plus icon. For this purpose, we apply a tap gesture to the ZStack containing the Circle and Image view.

ZStack {
     //…
 }
     .offset(y: -geometry.size.height/8/2)
     .onTapGesture {
         showPopUp.toggle()
     }

When we now tap on the plus icon of our tab bar, the floating menu gets displayed! We can animate this by wrapping the toggle statement into a withAnimation clause.

.onTapGesture {
     withAnimation {
         showPopUp.toggle()
     }
 }

To replace the default fade animation with a catchier one, we can add the .transition modifier to our PlusMenu’s view while choosing the .scale option:

var body: some View {
     HStack(spacing: 50) {
         //...
     }
         .transition(.scale)
 }

While running a live preview, tap the plus icon again to see how it looks!

Hint: Probably your live preview doesn’t display the animation correctly. In this case, run your app in the regular simulator.

Additionally, we want to rotate our plus icon when tapping on it. To do this, add the following modifier to the Image view inside the corresponding ZStack.

Image(systemName: "plus.circle.fill")
     //…
     .rotationEffect(Angle(degrees: showPopUp ? 90 : 0))

Depending on whether the plus menu is being shown, this modifier rotates the plus icon by 90 degrees.

Our whole plus menu animation should now look like this:

Conclusion 🎊

Awesome, we finished creating our own custom tab bar in SwiftUI! We’ve learned how to design a tab bar’s UI, how to implement a proper navigation logic, and how to animate a cool pop-up menu. With this knowledge, you should be able to create your own custom tab bar!

We’ve uploaded the whole source code of this app to GitHub.

You liked the tutorial, and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!