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!