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!

Categories
Uncategorized

How to use In-App Purchases in SwiftUI apps

Welcome to a new SwiftUI tutorial! Today we will learn how to integrate and use In-App-Purchases in SwiftUI apps to monetize our app. For this, we will write a small app called “Treasure Store” where the user can buy different items like weapons or skins. Don’t worry. We will go through everything step by step and explain it as simple as possible. After this tutorial, you will be able to monetize your SwiftUI app with In-App-Purchases.

Important: To use and test In-App-Purchases in SwiftUI, you need both a physical Apple device and a paid Apple Developer account!

At the end of the tutorial, our app will look like this:

Creating an Xcode project and preparing our ContentView 👩‍🎨

Let’s start with the preparation of our Xcode project. Open Xcode 12 and create a new Xcode project. Select “App” under “iOS” or “Multiplatform” and click on “Next”. As the “Product Name” we choose “Treasure Store”. At this point, you can also enter an Organization Identifier, e.g., your name, which will be used to generate the Bundle Identifier.

Click on “Next” and then create the Xcode project.

We will only be using one main view for our app. For this, we can use the by default generated ContentView.swift file.

The ContentView should contain a List that will later show the available IAP products. Until we have set this up, we will use dummy data.

struct ContentView: View {
    var body: some View {
        List {
            
        }
    }
}

We want to display the product’s name, description, and price for each IAP product that we will later obtain from Apple servers. We also use a Button to start the purchase process later.

List {
    HStack {
        VStack(alignment: .leading) {
            Text("Power Sword")
                .font(.headline)
            Text("Dominate your enemies with this noble weapon")
                .font(.caption2)
        }
        Spacer()
        Button(action: {
            //Purchase particular IAP product
        }) {
            Text("Buy for 1.09 $")
        }
            .foregroundColor(.blue)
    }
}

Your ContentView preview should now look like this:

We determine whether the user has purchased the corresponding product by checking whether the respective IAP product ID (which will also be provided to us by the Apple servers) has a UserDefault value. If this is the case, we want to show the user a “Purchased” Text instead of the “Purchase” Button. So we write:

HStack {
    //...
    Spacer()
    if UserDefaults.standard.bool(forKey: "*ID of IAP Product*") {
        Text("Purchased")
            .foregroundColor(.green)
    } else {
        Button(action: {
            //Purchase particular IAO product
        }) {
            Text("Buy for 1.09 $")
        }
            .foregroundColor(.blue)
    }
}

Next we wrap our List into a NavigationView and add a .navigationTitle to it.

NavigationView {
    List {
        //...
    }
    .navigationTitle("Treasure Store 🏴‍☠️")
    .toolbar(content: {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button(action: {
                //Restore products already purchased
            }) {
                Text("Restore Purchases ")
            }
        }
    })
}

Finally, we want to provide the user with a Button in the navigation bar to restore purchases already made. For this, we use the .toolbarModifier and initialize a ToolbarItem, which we place on the right side.

Great! We are already done with the preparation of the ContentView. The corresponding preview should now look like this:

Creating an App ID 👨‍💻

First, we have to create an App ID in the Apple Developer Center so that Apple’s server can correctly assign our app. This is quite simple. Visit the Apple Developer Center, log into your account if necessary, and go to “Certificates, Identifiers & Profiles”. 

Now open the “Identifiers” tab and create a new ID by clicking on the plus icon. Then select “App IDs”, click on Continue and select “App” again. 

Now it gets crucial. In the box “Description”, you can enter the name of your app. Next, we enter a unique Bundle ID. If your Xcode project had already generated one for you when you created it, it is a good idea to use it. Otherwise, you can also think of your own. Important: The Bundle Identifier of your Xcode project must match the Bundle ID in the Developer Center!

Quickly make sure that “In-App Purchase” is selected under “Capabilities” and click on “Register”. 

Checking pending agreements ✅

Next, we will make the necessary preparations for our In-App Purchases in App Store Connect. In App Store Connect, we can provide and manage content for the App Store.

It is very important that you have completed the necessary forms and accepted the agreements. Otherwise, you will not be able to communicate with the Apple servers.

Visit this link or open App Store Connect and select “Agreements, Tax and Banking”.

If both the agreements for Paid Apps and Free Apps have the status “Active”, you can continue.

Creating an App, Sandbox users, and In-App Purchases in App Store Connect 🌎

Now we can register the Treasure Store App in App Store Connect. Visit this link or open App Store Connect and select “Apps”.

To register a new app, click on the plus icon and choose “New App”.

Now we have to provide the necessary information for the app entry. Select the platform of your app (in our case iOS). Enter “Treasure Store” as the name and select the primary language.

As the Bundle ID, we now choose the one we created in the Developer Center! As the SKU, you can choose any identifier you like. When you are done, click on “Create”.

Next, we create different In-App Purchases products for our SwiftUI app. Open the tab “Manage” under “In-App-Purchases” and click on the plus icon to add a new IAP product.

You can now choose between four different types of In-App Purchases.

In this tutorial, we will only deal with Non-Consumable In-App Purchases, i.e., products purchased only once by the user. With the knowledge you acquire here, you will quickly understand how to deal with the other types of In-App Purchases too.

After clicking “Create”, you will be redirected to a new site. Choose a proper reference name here and enter a unique Product ID. Here you can use a combination of the Bundle ID of your app and the name of your IAP. Also, choose a pricing tier for your product. By the way: as long as you don’t publish your app, you don’t have to provide meta information.

We also provide a title and a description for the IAP as it will be displayed by the App Store.

Repeat these steps for other IAP products you want to offer in the Treasure Store app!

Before we get to the code, we need to create some sandbox text users. Of course, we don’t want to test the IAP with our personal Apple ID and pay real money at the end.

Open App Store Connect and click on “User and Access”. Open the tab “Testers” under “Sandbox”.

Here we create a fictive test user. But make sure you remember the password because you can’t change it later. We also have to enter a real mail address and confirm it. Note that this mail address must not already be connected for a real Apple ID!

Once you’re finished, click on “Invite”.

You can also create a few more sandbox users to test the purchase of a product several times later.

Great, we have now both registered our app in App Store Connect, created various IAP products, and created sandbox user accounts for testing purposes.

Next, we will switch back to our Xcode project.

Activating In-App Purchases in our Xcode project

Back in our Xcode project, we only need to open the “Signing & Capabilities” tab in our Dungeon Store target settings.
We now open the “Capabilities” Library by clicking on the “Plus” button in the Xcode toolbar.

We now search for “In-App-Purchase” and double click on the entry to add the corresponding capability to our app.

We have now made all necessary preparations and can finally start writing the code for embedding the In-App Purchases!

Setting up the StoreManager

We need a place in our SwiftUI app where we can fetch the In-App Purchases products from the Apple servers and start the purchase process for a specific product at the user’s request.

To do this, we create a File-New-File and select “Swift File”. We call this file “StoreManager”. Import the StoreKit framework and create a class with the same name.

import Foundation
import StoreKit

class StoreManager {
    
}

Since we want to notify our ContentView later, as soon as the products are fetched from App Store Connect, we conform the StoreManager to the ObservableObject protocol. If you don’t know what this is all about, please have a look at this tutorial.

class StoreManager: ObservableObject {
    
}

We want to initialize the StoreManager as soon as we launch our Treasure Store app. Therefore we switch to the Treasure_StoreApp struct and initialize the StoreManager as a @StateObject.

@main
struct Treasure_StoreApp: App {
    
    @StateObject var storeManager = StoreManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Our ContentView should have access to exactly this storeManager and observe it. Therefore, we also declare a storeManager property in our ContentView

struct ContentView: View {
    
    @StateObject var storeManager: StoreManager
    
    var body: some View {
        //...
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(storeManager: StoreManager())
    }
}

… and pass the storeManager of our Treasure_StoreApp struct to the ContentView:

@main
struct Treasure_StoreApp: App {
    
    @StateObject var storeManager = StoreManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView(storeManager: storeManager)
        }
    }
}

Fetching IAP Products using the SKProductsRequestDelegate protocol 

We want to use our StoreManager to fetch the IAP products we have created in App Store Connect. Once we have received them, we use the information contained in them, such as the product name, description and price, to display the available IAPs to the user in our ContentView‘s List.

To fetch the products, we adapt the NSObject and SKProductsRequestDelegate protocols.

class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate {
    
}

To conform to the SKProducutsRequestDelegate, we add the following function to our class:

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    
}

As soon as we receive a response from App Store Connect, this function is called. 

We now declare an array that will contain the IAP products as SKProduct instances. Since we want to update the observing ContentView every time a new SKProduct is added, we use the @Published property wrapper.

@Published var myProducts = [SKProduct]()

We also need a SKProductsRequest property in our StoreManager, which we will use to start the fetching process. 

var request: SKProductsRequest!

Next we implement a function that sends a request to the Apple servers based on given product IDs. At the same time we use the StoreManager class itself as the delegate of the request, so that the request knows that the didReceive response method should be called as soon as the Apple servers send a response.

func getProducts(productIDs: [String]) {
    print("Start requesting products ...")
    let request = SKProductsRequest(productIdentifiers: Set(productIDs))
    request.delegate = self
    request.start()
}

As said, the didReceive response method is called as soon as we get a response from the Apple servers. As soon as this is the case, we check if the response also contains products.

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Did receive response")
    
    if !response.products.isEmpty {
        
    }
}

If we are sure that we have received products, we can add any of these products to our myProducts array using a for-in loop.

if !response.products.isEmpty {
    for fetchedProduct in response.products {
        DispatchQueue.main.async {
            self.myProducts.append(fetchedProduct)
        }
    }
}

If for some reason our response contains product IDs that are invalid, we want to be notified. We use a corresponding for-in loop for this purpose as well.

for invalidIdentifier in response.invalidProductIdentifiers {
    print("Invalid identifiers found: \(invalidIdentifier)")
}

The overall didReceive response method now looks like this:

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Did receive response")
    
    if !response.products.isEmpty {
        for fetchedProduct in response.products {
            DispatchQueue.main.async {
                self.myProducts.append(fetchedProduct)
            }
        }
    }
    
    for invalidIdentifier in response.invalidProductIdentifiers {
        print("Invalid identifiers found: \(invalidIdentifier)")
    }
}

If it should happen that we get no response from the Apple servers but the request fails, we would like to be notified about this.

Therefore, we add the following method to our StoreManager class:

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Request did fail: \(error)")
}

Updating the App struct and the ContentView 🔄

Once our ContentView is initialized as our app’s root view, we want to start the fetching process. For this purpose, we use the .onAppear modifier for the ContentView instance in our Treasure_StoreApp struct. 

WindowGroup {
    ContentView(storeManager: storeManager)
        .onAppear(perform: {
            storeManager.getProducts(productIDs: )
        })
}

Important: As the productIDs, you must now use the IDs you used for the In-App Purchases you created in App Store Connect earlier!

You can put them into your own array, which you use for the productIDs parameter of the getProducts function.

@main
struct Treasure_StoreApp: App {
    
    let productIDs = [
        //Use your product IDs instead
        "com.BLCKBIRDS.TreasureStore.IAP.PowerSword"
        "com.BLCKBIRDS.TreasureStore.IAP.HealingPotion"
        "com.BLCKBIRDS.TreasureStore.IAP.PirateSkin"
    ]
    
    @StateObject var storeManager = StoreManager ()
    
    var body: some Scene {
        WindowGroup {
            ContentView(storeManager: storeManager)
                .onAppear(perform: {
                    storeManager.getProducts(productIDs: productIDs)
                })
        }
    }
}

The next step is to update our ContentView. The List should use the myProducts array of the storeManager to present one row for each SKProduct instance contained.

List(storeManager.myProducts, id: \.self) { product in
    //...
}

Now we replace the fixed values of the two Text views and the UserDefault key with the values of the respective product.

List(storeManager.myProducts, id: \.self) { product in
    HStack {
        VStack(alignment: .leading) {
            Text(product.localizedTitle)
                .font(.headline)
            Text(product.localizedDescription)
                .font(.caption2)
        }
        Spacer()
        if UserDefaults.standard.bool(forKey: product.productIdentifier) {
            Text ("Purchased")
                .foregroundColor(.green)
        } else {
            Button(action: {
                //Purchase particular ILO product
            }) {
                Text("Buy for \(product.price) $")
            }
                .foregroundColor(.blue)
        }
    }
}

Now let’s run the SwiftUI app on a physical device and see if everything works and the In-App Purchases are being loaded.

Perfect! As soon as the ContentView is launched as the root view, the getProducts function of the storeManager starts to fetch the IAP products of App Store Connect using the given IDs. 

We then use the obtained SKProduct instances to represent them in our List.

Purchase products using the SKPaymentTransactionObserver protocol

Okay, we are now able to fetch the IAP products from App Store Connect. We need to start the purchase process as soon as the user taps on the respective “Purchase” Button.

For this functionality, our StoreManager class has to adopt the SKPaymentTransactionObserver protocol. To conform to this protocol, we have to add the “paymentQueue” method to our StoreManager class.

class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
    //...
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
    }
}

Once we have started a transaction (which we will do in a moment), this function is called every time something changes in the status of the transaction(s) currently processed.

To start a transaction, we implement a new function called “purchaseProducts” which accepts a SKProduct.

func purchaseProduct(product: SKProduct) {
        
        
}

First of all, we make sure that the user can also make payments. This is not the case, for example, if parental control is set up.

func purchaseProduct(product: SKProduct) {
    if SKPaymentQueue.canMakePayments() {
        
    } else {
        print("User can't make payment.")
    }
}

If the user can make payments, we initiate a payment process using the given product and add it to the SKPaymentQueue. The SKPaymentQueue handles all payments to be processed on communicates to the App Store Connect servers.

func purchaseProduct(product: SKProduct) {
    if SKPaymentQueue.canMakePayments() {
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    } else {
        print("User can't make payment.")
    }
}

At the same time, we also have to make sure that something is observing the queue and responding to changes. This should also be our StoreManager. So we go to our Treasure_StoreApp struct and set the storeManager as the observer for the SKPaymentQueue by writing:

ContentView(storeManager: storeManager)
    .onAppear(perform: {
        SKPaymentQueue.default().add(storeManager)
        //...
    })

Next, we add a SKPaymentTransactionState @Published property to our StoreManager class so that we can notify our ContentView each time the status of the processed transaction changes.

@Published var transactionState: SKPaymentTransactionState?

As mentioned above, the “paymentQueue” is called every time the status of the processed transaction(s) changes. If this happens, we want to update the @Published transactionState property for each processed transaction, depending on the transactionState of this transaction, and thus notify the ContentView. Therefore we write:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            transactionState = .purchasing
        case .purchased:
            transactionState = .purchased
        case .restored:
            transactionState = .restored
        case .failed, .deferred:
            transactionState = .failed
        default:
            queue.finishTransaction(transaction)
        }
    }
}

If the respective transaction is successfully completed (.purchased), we want to set the corresponding UserDefault key to true and complete the transaction process. Also, we want to do the same if App Store Connect detects that the product has already been purchased and therefore restored (.restored). If the payment fails (.failed, .deffered), we want to print the corresponding error and also end the transaction.

So the complete function is as follows:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            transactionState = .purchasing
        case .purchased:
            UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
            queue.finishTransaction(transaction)
            transactionState = .purchased
        case .restored:
            UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
            queue.finishTransaction(transaction)
            transactionState = .restored
        case .failed, .deferred:
            print("Payment Queue Error: \(String(describing: transaction.error))
                queue.finishTransaction(transaction)
                transactionState = .failed
                default:
                queue.finishTransaction(transaction)
        }
    }
}

Now we just have to define that the “Purchase” Button in our ContentView uses the purchaseProduct function of the storeManager to initialize a transaction.

Button(action: {
    storeManager.purchaseProduct(product: product)
}) {
    Text("Buy for \(product.price) $")
}

Okay, let’s see if this works by running the app on a physical device. Of course, we do not want to use our personal Apple ID for this. Instead, we use a sandbox account. Go to the iOS Settings, open “App Store” and tap on “Sign In” under “Sandbox Account”. Now use the credentials of a sandbox account you created in App Store Connect earlier to log in.

Run the app and wait until all products are loaded. To make an In-App Purchase, we tap on the respective “Purchase” button. In sandboxed mode, it may take a few seconds to start the transaction.

Tip: If your personal Apple account is still used when the transaction is initiated, try to log out of your personal iCloud account in the iOS settings and restart the app.

As soon as you are prompted to do so, use the login data of a created sandbox account to make the purchase.

If all goes well, App Store Connect will send us a message that the purchase was successful. Our ContentView is updated via the @Published transactionState property, and since a value is now stored for the UserDefault key of the corresponding product ID, the respective product is now marked as “Purchased”!

Restore products already purchased

Okay, but what if the user reinstalls the app but has already made an In-App Purchase?

Try to delete the Treasure Store App from your device and then run the app over Xcode to reinstall it. The respective product is no longer marked as “Purchased”!

Restoring purchased products is very easy. Just add the following function to the StoreManager class.

func restoreProducts() {
    print("Restoring products ...")
    SKPaymentQueue.default().restoreCompletedTransactions()
}

The “Restore Purchases” Button of our ContentView must now only call the restoreProducts function.

ToolbarItem(placement: .navigationBarTrailing) {
    Button(action: {
        storeManager.restoreProducts()
    }) {
        Text ("Restore Purchases ")
    }
}

If we now run the app again and tap on the “Restore Purchases” Button in the navigation bar, all IAPs will be restored!

Conclusion 🎊

That’s it! We have really learned a lot. You now know how to build in-app-purchases into your SwiftUI apps to monetize them.

You can download the whole source code here!

We only worked with non-consumable IAPs in this tutorial, but you should be able to apply your knowledge to other types of IAPs. Please have a look at the Apple Docs

If anything is unclear, please leave a comment!

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 navigate between views in SwiftUI by using an @EnvironmentObject

Updated for Xcode 12 and SwiftUI 2.0 ✅

Hello and welcome to this tutorial! In this post, we will talk about how to navigate between views in SwiftUI (not using a navigation view!). A concept that may sounds trivial but by understanding it deeply we can learn a lot about the data flow concepts used in SwiftUI.

This tutorial is an excerpt from our Interactive Mastering SwiftUI Book

In the last part, we learned how to do this by using an ObservableObject and the @StateObject property wrapper. In this part, we look at how to accomplish the same but more efficiently using an @EnvironmentObject. We are also going to apply a nice transition animation.

Here is what we are going to achieve:

Our current progress 👓📚

So we just figured out how to navigate between different views using a ObservableObject. In a nutshell, we created a ViewRouter and bound our MotherView and the ContentViews to it. Then, we update the Page assigned to the ViewRouter’s currentPage property when clicking on the respective ContentViews Buttons. This causes the MotherView to update its body with eventually showing the correct ContentView!

But there is a second, more efficient way for achieving this functionality: using an @EnvironmentObject!

Hint: You can download the current progress here (it’s the “NavigatinInSwiftUIFinishedPart1” folder):

GitHub

Why using an ObservableObject isn’t always the best solution⚠

Okay, we just learned how ObservableObjects and SwiftUI work and how we can utilize them for navigating between views.

Maybe you’re asking yourself: “Why should we do anything else when the existing solution is sufficient?”. Well, it should become clear when looking at our app’s hierarchy logic. The NavigatingInSwiftUIApp struct initialises a ViewRouter instance as a @StateObject and passes it to the root MotherView. In the MotherView, we initialise either ContentViewA or ContentViewB with passing down the ViewRouter instance down to them. 

You see that we follow a strict hierarchy which passes the initialised ViewRouter as a @StateObject downwards to the “lowest” subviews. For our purposes, this not a big deal, but imagine a more complex app with a lot of views containing subviews that in turn contain subviews and so on. Passing down the primary initialised @StateObject down to all subviews could get pretty messy.

In one sentence: Using an ObservableObjects observed by using @StateObjects can become confusing when we work with more complex app hierarchies.

So, what we could do instead, is to initialise the ViewRouter once at the app’s launch in a way that all views of our app hierarchy can be directly bound to this instance, or better said, are observing this instance, with no regard to the app’s hierarchy and no need to passing the ViewRouter downwards the hierarchy manually. The ViewRouter instance would then act like a cloud that flies above our app’s code where all views have access to, without taking care of a proper initialisation chain downwards the app’s view hierarchy.

Doing this is the perfect job for an EnvironmentObject!

What is an EnvironmentObject? 🧐

An EnvironmentObject is a data model that, once initialised, can be used to share information across all view’s of your app. The cool thing is, that an EnvironmentObject is created by supplying an ObservableObject. Thus we can use our ViewRouter as it its for creating an EnvironmentObject!

So, once we defined our ViewRouter as an EnvironmentObject, all views can be bound to it in the same way as a regular ObservableObject but without the need of an initialisation chain downwards the app’s hierarchy.

As said, an EnvironmentObject needs to already be initialised when referring to it the first time. Since our root MotherView will look into the ViewRouter‘s currentPage property first, we need to initialise the EnvironmentObject at the app’s launch. We can then automatically change the data assigned to the EnvironmentObject’s currentPage property from the ContentView’s which then causes the MotherView to rerender its body.

Implementing the ViewRouter as an EnvironmentObject 🤓

Let’s update our app’s code!

First, change the viewRouter‘s property wrapper inside the MotherView from an @StateObject to an @EnvironmentObject.

@EnvironmentObject var viewRouter: ViewRouter

Now, the viewRouter property looks for a ViewRouter as an EnvironmentObject instance. Thus, we need to provide our MotherView_Previews struct with such an instance:

struct MotherView_Previews: PreviewProvider {
    static var previews: some View {
        MotherView().environmentObject(ViewRouter())
    }
}

When launching our app, the first and most high view in the app hiearchy must immediately be provided with a ViewRouter instance as an EnvironmentObject. Therefore, we need to pass the @StateObject we initialised in our NavigatingInSwiftUIApp struct to the MotherView as an injected EnvironmentObject like this.

@main
struct NavigatingInSwiftUIApp: App {
    
    @StateObject var viewRouter = ViewRouter()
    
    var body: some Scene {
        WindowGroup {
            MotherView().environmentObject(viewRouter)
        }
    }
}

Great! SwiftUI now creates a ViewRouter instance and injects it to the whole view hierarchy as an EnvironmentObject when the app launches. Now, all views of our app can bound to this EnvironmentObject. 

Next, let’s update our ContentViewA. Change the viewRouter property to an EnvironmentObject …

@EnvironmentObject var viewRouter: ViewRouter

… and update the ContentViewA_Previews struct:

struct ContentViewA_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewA().environmentObject(ViewRouter())
    }
}

Hint: Again, only the ContentViewsA_Previews struct has an own instance of the ViewRouter, but the ContentViewA itself is bound to the instance created at the app’s launch!

Let’s repeat this for ContentViewB…

@EnvironmentObject var viewRouter: ViewRouter

…and its previews struct:

struct ContentViewB_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewB().environmentObject(ViewRouter())
    }
}

Since the viewRouter properties of our ContentViews are now directly bound to the initial ViewRouter instance as an EnvironmentObject, we don’t need to initialise them inside our MotherView anymore. So, let’s update our MotherView:

struct MotherView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        switch viewRouter.currentPage {
            case .page1:
                ContentViewA()
            case .page2:
                ContentViewB()
        }
    }
}

And that’s the cool thing about EnvironmentObjects. We don’t need to pass down the viewRouter of our MotherView downwards to ContentView’s anymore. This can be very efficient, especially for more complex hierarchies.

Great! Let’s run our app and see if that works … perfect, we are still able to navigate between our different views but with a more clean code.

Adding a transition animation 🚀

Before ending this tutorial, let’s take a look at how to add a transition animation when navigating from .page1 to .page2.

Doing this in SwiftUI is pretty straight forward.

Take a look at the ViewRouter’s currentPage property that we manipulate when we tap on the Next/Back Button. As you learned, due to the @Published property wrapper’s functionality, this triggers the bound MotherView to rerender its body with eventually showing another ContentView. We can simply animate this navigation process by wrapping the code that changes the Page assigned to the currentPage into a “withAnimation” statement. Let’s do this for the Button of ContentViewA

Button(action: {
    withAnimation {
        viewRouter.currentPage = .page2
    }
}) {
    NextButtonContent()
}

… and ContentViewB

Button(action: {
    withAnimation {
        viewRouter.currentPage = .page1
    }
}) {
    BackButtonContent()
}

Now, we present a transition animation when navigating to another ContentView. 

“withAnimation(_:_:) – Returns the result of recomputing the view’s body with the provided animation”

Apple

By default, the “withAnimation” statement uses a fade transition style. But instead, we want to show a “pop up” transition when navigating from ContentViewA to ContentViewB. To do this, go into your MotherView.swift file and add a transition modifier when calling the ContentViewB. You can choose between several preset transition styles or create even a custom one (but that’s a topic for another tutorial). For adding a “pop up” transition, we choose the .scale transition type.

switch viewRouter.currentPage {
case .page1:
    ContentViewA()
case .page2:
    ContentViewB()
        .transition(.scale)
}

Hint: Most animations don’t work within the preview simulator. Try running your app in the regular simulator instead.

Awesome! With just a few lines of code, we added a nice transition animation to our app.

You can download the whole source code here!

Conclusion 🎊

That’s it! We learned when and how to use EnvironmentObjects in SwiftUI. We also learned how to add a transition animation to view showing up.

You are now capable of navigating between views in SwiftUI by using two ways – either you put your views into a navigation view hierarchy or you create an external view router as an Observable-/EnvironmentObject.

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 navigate between views in SwiftUI by using an @ObservableObject

Updated for Xcode 12 and SwiftUI 2.0 ✅

Hello and welcome to this tutorial! In this post, we will talk about how to navigate between views in SwiftUI (not using a NavigationView!). A concept that may sounds trivial but by understanding it deeply we can learn a lot about the data flow concepts used in SwiftUI.

This tutorial is an excerpt from our Interactive Mastering SwiftUI Book

In this tutorial, you will learn:

  • How to navigate between views without relying on a navigation view hierarchy
  • More advanced data flow techniques including ObservableObjects, @StateObjects and @EnvironmentObjects
  • Taking a first look at using animations in SwiftUI

In this part, we will learn how to navigate between views using a ObservableObject and @StateObject. In the next part, we look at how to accomplish the same behaviour more efficiently using an @EnvironmentObject and apply some nice navigation animations.

And here is what we are going to achieve at the end of this tutorial:

For learning how to navigate between different views in SwiftUI, it’s appropriate to start with an example that’s not too complex. Supposed we have an app with two different views. ContentViewA shows an Image view with a grumpy dog and a Button reading “Next”. The other view called ContentViewB shows an Image view with a happy dog and a Button reading “Back”.

You can download the starter project here:

Starter Project

We want to connect those views in a way that when we tap on the buttons we navigate back and forth. We could accomplish this using a NavigationView, but in this chapter, we don’t want to use such a navigation view hierarchy. Instead, we want both views to be independent of each other.

So let’s get started!

Creating a Mother View 👩‍👧‍👦

The first step is to create a mother view that hosts both Content Views as its subviews. For this purpose, create a new File-New-File-SwiftUI View and name it MotherView. In this view, we want to show either ContentViewA or ContentViewB depending on where the user navigated to. 

Important: Since our MotherView will “contain” the ContentViews, it must be the root view when the app launches. To set the MotherView as the root view, go into the NavigatinInSwiftUIApp.swift file and replace the ContentViewA inside the WindowGroup with an instance of our MotherView.

@main
struct NavigatingInSwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            MotherView()
        }
    }
}

Back to our MotherView.swift file: To keep track of the selected main view, we need to declare a State property. At default, we want to present ContentViewA as the first “page”. For this purpose, create a new Swift file called Helper.swift and insert the following enum.

enum Page {
    case page1
    case page2
}

Now, we can declare the State property in our MotherView and assign it to the page1 option of our Page enum.

struct MotherView: View {
    
    @State var currentPage: Page = .page1
    
    var body: some View {
        Text("Hello World")
    }
}

State properties are used for displaying views depending on the State’s data. Every time the State gets updated it triggers the view to rerender. If you’re not familiar with the concept of States in SwiftUI, we strongly recommend you to read this tutorial first, since it’s crucial for understanding the following concepts!

Depending on the Page assigned to the currentPage State, we want to either host ContentViewA or ContentViewB. Let’s implement this logic by inserting a switch-statement inside a VStack.

var body: some View {
    switch currentPage {
    case .page1:
        ContentViewA()
    case .page2:
        ContentViewB()
    }
}

Let’s run our preview simulator and take a look at it. Since our State is currently assigned to .page1, our first switch case is met and the ContentViewA gets hosted. Let’s change the page State to .page2 and see what happens.

Here we go! Because the State changed, our whole MotherView gets updated and the switch block gets executed again, this time showing us ContentViewB. So far, so good.

But we want to let the user change the currentPage State by tapping on the Buttons inside of ContentViewA and ContentViewB, respectively. Note that the Buttons are not part of the MotherView itself, so we need to create a “bridge” for accessing the MotherView’s currentPage Statefrom the outside; meaning that when, for instance, tapping on the “Next” Button of ContentViewA, we alter the Page assigned to the currentPage State of the MotherView for eventually navigating to ContentViewB.

We can achieve this by interjecting something called an ObservableObject into our MotherView – ContentViews hierarchy. 

ObservableObjects ?! 🤯

At this point, you are probably asking yourself: “What the heck are ObservableObjects?!”. Well, understanding this can be pretty tough but don’t worry, we will explain it to you in a simple way.

ObservableObjects are similar to State properties which you should already know. But instead of just rerendering at the body of the related view when the data assigned to the State changes, ObservableObjects are capable of the following things: 

  • Instead of variables, ObservableObjects are classes that can contain data, for example, a String assigned to a variable 
  • We can bind multiple views to the ObservableObject (in other words: we can make these views observe the ObservableObject). The observing views can access and manipulate the data inside the ObservableObject
  • When a change happens to the ObservableObject’s data, all observing views get automatically notified and rerendered similar to when the value assigned to a State changes

So, how could we use this functionality? Well, we can create an ObservableObject class that contains a variable indicating the current Page that should be displayed. Then we can bind our MotherView, our ContentViewA, and our ContentViewB to it. Then, we can tell our MotherView to show the corresponding ContentView depending on the Page assigned to the ObservableObject’s variable.

From the Buttons inside the ContentViews, we can update the Page assigned to the ObservableObject’s variable. This would cause all three observing views to update their bodies, including the MotherView. With this functionality, we can achieve that the MotherView will present show the correct ContentView depending on the selected Page!

Honestly, this seems a little bit abstract, but it should become clearer when applying the concept to our app.

Let’s create such an ObservableObject. To do this, create a new Swift file and call it ViewRouter.swift. Make sure you import the SwiftUI framework. Then create a class called ViewRouter conforming to the ObservableObject protocol.

import SwiftUI

class ViewRouter: ObservableObject {
    
}

As said, an ObservableObject notifies and causes all of its observing views to update themselves when a change happens. But what exactly do we mean with “when a change happens”? As said, the main task of our ViewRouter should be to stay tracked on which Page (meaning which ContentView) should be currently shown – whether it’s on the launch of the app or when the user taps on a specific Button. For this purpose, we declare a variable called currentPage inside our ViewRouter class and assign .page1 to it as its default value.

class ViewRouter: ObservableObject {
    
    var currentPage: Page = .page1
    
}

The views that will observe the ViewRouter, especially the MotherView, should get notified and updated when the Page assigned to the currentPage changes. 

To do this, we use the @Published property wrapper.

@Published var currentPage: Page = .page1

The @Published property wrapper works very similarly to the @State property wrapper. Every time the value assigned to the wrapped property changes, every observing view rerenders. In our case, we want our MotherView to observe the ViewRouter and to navigate to the right Page depending on the currentPage’s updated value.

Updating the MotherView 🔁

To make the MotherView observe the ViewRouter, we need to declare a @StateObject property, which is used for binding views to ObservableObjects.

struct MotherView: View {
    
    @State var currentPage: Page = .page1
    
    @StateObject var viewRouter: ViewRouter

    var body: some View {
        //...
    }
}

When doing this, we also need to provide our MotherView_Previews struct with an instance of the ViewRouter.

struct MotherView_Previews: PreviewProvider {
    static var previews: some View {
        MotherView(viewRouter: ViewRouter())
    }
}

In the NavigatinInSwiftUIApp.swift file, we defined the MotherView as the root view when the app launches. Thus, not only does our preview simulator need to be provided with a ViewRouter instance, but also the actual App hierarchy when the app gets executed on a real device or in the regular simulator.

So, go to the NavigatingInSwiftUIApp file and declare a @StateObject property. Then, pass the initialised @StateObject to the viewRouter of our MotherView.

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

Our MotherView router is now able to observe and access the viewRouter‘s OberservableObject. So, let’s show the corresponding ContentView depending on the Page assigned to the viewRouter‘s currentPage property.

var body: some View {
    switch viewRouter.currentPage {
    case .page1:
        ContentViewA()
    case .page2:
        ContentViewB()
    }
}

You can delete the currentPage State of the MotherView, since we won’t need it anymore.

Let’s take a look at the simulator of our MotherView: The MotherView reads the value of the ViewRouter’s currentPage variable and hosts the corresponding ContentView. You can proof this by changing the default value assigned to the ViewRouter’s currentPage property to .page2. Go back to the MotherView preview simulator and see what happens! The @Published property of our ObservableObject told the MotherView to update its body.

Because we want ContentViewA to be the default view, assign .page1 to the currentPage property again.

Great! We accomplished a lot so far! We initialised a ViewRouter instance and bound it to the MotherView by using a @StateObject. Every time the values assigned to the currentPage property of the ViewRouter instance gets updated, the MotherView will rerender its body with eventually showing the correct ContentView!

Bind the ContentViews to the ViewRouter ⛓

Our MotherView is now able to show the correct ContentView depending on the Page assigned to the currentPage property of the ViewRouter. But until now, the user is not able to change this value by tapping on the respective Button of ContentViewA and ContentViewB.

Let’s start with ContentViewA. To let it access the currentPage and manipulate its value we have to bind it to the ViewRouter. So, let’s create an @StateObject again. 

struct ContentViewA: View {
    
    @StateObject var viewRouter: ViewRouter
    
    var body: some View {
        //...
    }
}

We need to update the ContentViewA_Previews struct again.

struct ContentViewA_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewA(viewRouter: ViewRouter())
    }
}

ContentViewA should observe the ViewRouter instance we created inside the NavigatingInSwiftUI struct and passed to the MotherView. So, let’s assign our new @StateObject to this instance when initialising ContentViewA in our MotherView.

switch viewRouter.currentPage {
case .page1:
    ContentViewA(viewRouter: viewRouter)
case .page2:
    ContentViewB()
}

Great! Now we have access to the currentPage property of our viewRouter. Use the Button’s action closure to assign .page2 to it when tapping on the “Next” Button.

Button(action: {
    viewRouter.currentPage = .page2
}) {
    NextButtonContent()
}

Okay, let’s see if that works: Run the app in the regular simulator or start a Live preview of the MotherView and tap on the “Next” button. Great, we successfully navigate to ContentViewB! 

This is what happens when the user taps on the “Next” Button of ContentViewA: ContentViewA changes the Page assigned to the currentPage property of the viewRouter to .page2. Therefore, the viewRouter tells all bound views to rerender their bodies, including the MotherView. The MotherView updates its body and checks the currentPage‘s value. Because it’s .page2 now, the case for showing ContentViewB is met and we eventually navigate to it!

To be able to navigate back to ContentViewA, repeat this implementation process for ContentViewB:

Declare a @StateObject property as a ViewRouter instance…

struct ContentViewB: View {
    
    @StateObject var viewRouter: ViewRouter
    
    var body: some View {
        //...
    }
}

…and update the related previews struct:

struct ContentViewB_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewB(viewRouter: ViewRouter())
    }
}

Assign this viewRouter property to the initial ViewRouter instance passed by the NavigatingInSwiftUIApp struct to the MotherView.

switch viewRouter.currentPage {
case .page1:
    ContentViewA(viewRouter: viewRouter)
case .page2:
    ContentViewB(viewRouter: viewRouter)
}

Then, update the Button’s action parameter for showing the first page again:

Button(action: {
    viewRouter.currentPage = .page1
}) {
    BackButtonContent()
}

We can now navigate independently between our ContentViews!

Conclusion 🎊

We just figured out how to navigate between different views using an @ObservableObject. We created a ViewRouter and bound our MotherView and the ContentViews to it. Then, we achieved to manipulate the ViewRouter’s currentPage property when clicking on the ContentViews Buttons. Due to the @Published property wrapper’s functionality, this causes the MotherView to rebuild its body with eventually hosting the correct ContentView! 

You can download the whole source code here!

But often, there is an alternative, more efficient way to do this: Using an @EnvironmentObject. EnvironmentObjects provide us with more freedom and independence within our app’s view hierarchy. You will see what we mean by that when reading the second part of this tutorial ! We will also talk about adding animated transitions, so make sure you check it out!

Categories
Uncategorized

Core Data and SwiftUI 2.0 – Saving, retrieving, updating and deleting persistent data

Updated for Xcode 12.0 and SwiftUI 2.0 ✅

Hello, and welcome to a new tutorial! Today we will learn how to use the Core Data framework with SwiftUI to store and manage persistent data. The integration of Core Data into SwiftUI projects is surprisingly easy. By creating a useful app for a small pizza restaurant, we will talk through all basic CRUD operations (Create, Read, Update, and Delete Data) used in Core Data.

In this tutorial, we will explore:

  • How Core Data and SwiftUI work together
  • Creating and updating Core Data objects
  • How to update views when stored data gets updated
  • Using SwiftUI property wrappers for fetching Core Data objects

We will create a simple app for a pizza restaurant that waiters can use to take and manage orders.

The finished app will look like this:

Setting up Core Data using SwiftUI and Xcode 12

To get started, open Xcode 12 and create a new “App” under “Multiplatform” or “iOS”. You can name your project however you want, for instance, “PizzaRestaurant”. But make sure to use SwiftUI as the “Interface” mode and SwiftUI App as the “Life Cycle” mode. Also, make sure that you check the “Use Core Data” box. This will automatically set up the initial Core Data implementation for our app!

Make sure you “reset” the ContentView by removing the generated code from it since we won’t need it for our app.

import SwiftUI
import CoreData

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

In the project navigator, you can spot the “.xcdatamodeld” file Xcode created for us. In this file, we set up and manage the Entities of our Core Data data model for our SwiftUI app. If you are not familiar with the concept of Entities: You can think of an entity as a class, and an attribute, as a property of that class. The only Entity we need for our app is for holding the different orders. Delete the default “Item” Entity and create a new one by clicking on the large plus button at the bottom and then double-click on the created Entity to rename it to “Order”.

We need to know the following information about each order: The type of pizza the customer ordered, how many slices he wants to eat, and the number of the table the customer is sitting at. Each order should also have a unique “id” and a “status” attribute for keeping track of whether the order is already completed. For the id, we use the UUID type (this automatically creates a unique id for us). For the numberOfSclices we select Integer16 and for the rest String.

How Core Data works in SwiftUI and Xcode 12

That’s it! We just finished setting up a basic Core Data model for holding the orders for our pizza restaurant app. Wasn’t that easy? Let’s take a look at how CoreData was implemented into our SwiftUI project by checking the “Use CoreData” box earlier. 

To look behind the scenes, open the PizzaRestaurantApp.swift file. You already know that the App struct primarily handles booting up the initial view, which is the ContentView by default. Because we checked “Use CoreData” when creating our project earlier, Xcode created a property called persistenceController and applied an important modifier to the launched ContentView.

Let’s take a look at persistenceController property first.

let persistenceController = PersistenceController.shared

This property is assigned to a PersistenceController. We can find this PersistenceController in the Persistence.swift file. The PersistenceController struct contained in this file includes various properties.

Let us briefly review the most important ones. The preview property allows us to use the CoreData functionality inside preview simulators. 

Note: Since we have deleted the default “Item” Entity and created a new one called “Order”, we have to make a quick adjustment here. Delete the existing for-in loop and insert the following loops instead.

static var preview: PersistenceController = {
        //...
        for _ in 0..<10 {
            let newItem = Order(context: viewContext)
            newItem.status = "pending"
            newItem.id = UUID()
            newItem.tableNumber = "12"
            newItem.pizzaType = "Margherita"
            newItem.numberOfSlices = 4
        }
        //...
    }()

The container property is the heart of the PersistenceController, which performs many different operations for us in the background when we store and call data. Most importantly, the container allows us to access the so-called viewContext, which serves as in an in-memory scratchpad where objects are created, fetched, updated, deleted, and saved back to the persistent store of the device where the app runs on.

The container gets initialized within the PersistenceController’s init function. In this, the container property gets assigned to an NSPersistentContainer instance. We need to use the name of our “.xcdatamodeld” file, which is “Shared” (or *YourAppName* when you created a mere iOS App project), as the “name” argument.

init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "PizzaRestaurant")
        //...
    }

Now, let’s take a look at the .environment modifier applied to the ContentView in our PizzaRestaurantApp struct.

ContentView()
    .environment(\.managedObjectContext, persistenceController.container.viewContext)

What does this .environment modifier do? Before our ContentView gets launched as the root view, it feeds the environment’s managedObjectContext key with the viewContext we just talked about.

The “environment” is where system-wide settings are saved, for instance, Calendar, Locale, ColorScheme, and now, also the viewContext contained in the persistenceController’s container property. Each of these settings has its own key; in our case, it’s the .managedObjectContext key.

Now, every view in our app can use the viewContext as a “scratchpad” to retrieve, update, and store objects. We simply need to use the managedObjectContext environment key for accessing it, as you will see later on.

Don’t worry if you are not familiar with this. The only thing you need to remember is that we can use the managedObjectContext for fetching and saving our orders. You’ll see how easy this is in a moment.

But first, we make a small adjustment to our CoreData data model.

Customizing our data model 🛠

Let’s hold on a second and reconsider choosing String as the status attribute’s type. Each order’s status should only be “Pending”, “Preparing” and “Completed”. Wouldn’t be using an enum the better choice for this? Unfortunately, we can’t create and use an enum inside the .xcdatamodeld file itself. But as said, by creating and designing the Order entity, Core Data created a corresponding class under the hood. We can access and modify this class by clicking on the Order entity, going to the Xcode toolbar, and selecting Editor-“Create NSObjectManagedSubclass”. 

After creating the subclass, Xcode generated two files for us. The Order+CoreDataClass.swift file holds the class itself, and the Order+CoreDataProperties.swift contains its properties inside an extension. 

After we created our data model’s subclass, we need to tell Xcode that the data model is no longer defined by the visual builder in our “.xcdatamodeld” file only, but manually defined by the corresponding subclass we just created. To do this, open the “.xcdatamodeld” file, click on the Order entity and open the data model inspector. Then choose “Manual/None” as the Codegen mode.

At this point, we can remove the question marks from the String-type properties since we don’t want them to be Optionals. Xcode should also create another extension adopting the Identifiable protocol (this will make it easier for us to use Order instances inside the ContentView’s List later). Since we declared an id property, we already conform to this protocol.

extension Order: Identifiable {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Order> {
        return NSFetchRequest<Order>(entityName: "Order")
    }

    @NSManaged public var pizzaType: String
    @NSManaged public var numberOfSlices: Int16
    @NSManaged public var id: UUID?
    @NSManaged public var tableNumber: String
    @NSManaged public var status: String

}

extension Order : Identifiable {

}

Below the Order extension we can declare our Status enum with the three different cases.

enum Status: String {
    case pending = "Pending"
    case preparing = "Preparing"
    case completed = "Completed"
}

If we now try to use the Status enum as the status’ data type, we will get an error.

You see that @NSManagedObject properties can’t be used with enums directly. But how else can we save the status of an order in Core Data? Here’s a workaround: We go ahead with using our NSManaged status property but not of our Status type. Instead, it should be a String again. Next, we add another regular variable called “orderStatus”. Because it’s not an NSManaged property, it can be of the type Status. We assign a setter and getter to our orderStatus. When this property is set, it will also set the NSManaged property accordingly. Using a getter, we try to convert the status string to a Status case when retrieving it.

extension Order {

    //...
    @NSManaged public var status: String
    
    var orderStatus: Status {
        set {
            status = newValue.rawValue
        }
        get {
            Status(rawValue: status) ?? .pending
        }
    }

}

Awesome, we finalized the Core Data model for our SwiftUI app!

Composing our UI 🎨

Important: Before moving on with composing our ContentView, we need to make sure that its preview can access the view Context as well. Otherwise the SwiftUI preview will fail when why try to implement CoreData functionality inside it. To do this, we use the viewContext of our PersistenceController and assign it to the environment’s managedObjectContext key just as we did in our App struct.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

Now our ContentView preview is able to manage CoreData requests!

The ContentView of our pizza restaurant app should contain a list of all orders already taken which the corresponding waiter can manage. Since we can’t store any data yet, we are using only a test list for now.

struct ContentView: View {
    var body: some View {
        List {
            Text("Sample order")
        }
    }
}

We also want to add a navigation bar to our app. To do so, we wrap our List into a NavigationView and use the .navigationBarTitle modifier.

NavigationView {
    List {
        Text("Sample order")
    }
        .navigationTitle("My Orders")
}

The navigation bar should contain a button the waiter can use to add a new order.

List {
    Text("Sample order")
}
    .navigationTitle("My Orders")
    .navigationBarItems(trailing: Button(action: {
        print("Open order sheet")
    }, label: {
        Image(systemName: "plus.circle")
            .imageScale(.large)
    }))

The preview canvas should look like this so far:

When we tap on the Button, we want to open a second view. For this, we create a new SwiftUI file and name it “OrderSheet. We want to display the OrderSheet as a modal view. To do this, we add a State to our ContentView to control when the OrderSheet should be displayed.

struct ContentView: View {
@State var showOrderSheet = false
    var body: some View {
       //...
    }
}

To display the OrderSheet as a modal view, we use the .sheet modifier.

List {
    Text("Sample order")
}
    //...
    .sheet(isPresented: $showOrderSheet) {
        OrderSheet()
    }

Whenever the showOrderSheet State is true the OrderSheet overlays the ContentView. Now we can toggle the showOrderSheet State from our navigation bar button.

.navigationBarItems(trailing: Button(action: {
    showOrderSheet = true
}, label: {
    Image(systemName: "plus.circle")
        .imageScale(.large)
}))

For our OrderSheet view’s body, we’ll be using the Form view to embed the user controls in, for example, a Picker with the different pizza options available. To represent the number of slices that the customer wishes to order, we use a Stepper. Finally, we use a TextField where the user can select the table number for the order.

Finally, we want to save the data after the user taps on the “Add Order” button.

For the OrderSheet’s UI, you can use copy & paste the following code:

struct OrderSheet: View {
       
    let pizzaTypes = ["Pizza Margherita", "Greek Pizza", "Pizza Supreme", "Pizza California", "New York Pizza"]
    
    @State var selectedPizzaIndex = 1
    @State var numberOfSlices = 1
    @State var tableNumber = ""
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Pizza Details")) {
                    Picker(selection: $selectedPizzaIndex, label: Text("Pizza Type")) {
                        ForEach(0 ..< pizzaTypes.count) {
                                Text(self.pizzaTypes[$0]).tag($0)
                        }
                    }
                    
                    Stepper("\(numberOfSlices) Slices", value: $numberOfSlices, in: 1...12)
                }
                
                Section(header: Text("Table")) {
                    TextField("Table Number", text: $tableNumber)
                        .keyboardType(.numberPad)
                    
                }
                
                Button(action: {
                    print("Save the order!")
                }) {
                    Text("Add Order")
                }
            }
                .navigationTitle("Add Order")
        }
    }
}

The OrderSheet’s preview should now look like this:

Saving data using Core Data and SwiftUI 🆕

Great, we’re done composing our PizzaRestaurant app’s interface, but nothing gets saved and persisted yet. To change this, we need to access to the viewContext first to persistently save a created order. Since, as we saw in the beginning, the managed object context is injected in our environment, we can simply access it by using the @Environment property wrapper inside our OrderSheet above its States.

@Environment(\.managedObjectContext) private var viewContext

Now that our OrderSheet has access to the device’s “scratchpad” we are ready to create an Order instance when tapping on the “Add Order” Button. But first, we want to make sure that the tableNumber String is not empty by using a guard statement.

Button(action: {
    guard self.tableNumber != "" else {return}
    let newOrder = Order(context: viewContext)
    newOrder.pizzaType = self.pizzaTypes[self.selectedPizzaIndex]
    newOrder.orderStatus = .pending
    newOrder.tableNumber = self.tableNumber
    newOrder.numberOfSlices = Int16(self.numberOfSlices)
    newOrder.id = UUID()
}) {
    Text("Add Order")
}

Then we’re trying to save the created order. If that fails, we print the corresponding error.

Button(action: {
    //...
    newOrder.id = UUID()
    do {
        try viewContext.save()
        print("Order saved.")
    } catch {
        print(error.localizedDescription)
    }
}) {
    Text("Add Order")
}

After the new order got saved, we want to close the OrderSheet modal view. We can do this by adding the following @Environment property to our OrderSheet.

@Environment (\.presentationMode) var presentationMode

By referring to this property we can manually close the modal view:

do {
    try viewContext.save()
    print("Order saved.")
    presentationMode.wrappedValue.dismiss()
} catch {
    print(error.localizedDescription)
}

Okay, let’s run our app to see if that works. Note that the preview canvas isn’t able to simulate CoreData’s functionality. Therefore, we need to run the app in the regular simulator. Click on the navigation bar button and fill out the OrderSheet form. Then click on “Add Order”. We saved the created order and dismissed the OrderSheet. However, our ContentView’s List is still displaying its sample row.

Fetching and displaying stored orders 📖

To change this, our ContentView needs to read out the saved orders. Achieving this functionality is quite simple by using a @FetchRequest property. But first, our ContentView itself requires access to the viewContext. We do this by using the @Environment property again. Below the ContentView’s @Environment property, insert the following properties:

@Environment(\.managedObjectContext) private var viewContext

@FetchRequest(entity: Order.entity(), sortDescriptors: [], predicate: NSPredicate(format: "status != %@", Status.completed.rawValue))

var orders: FetchedResults<Order>

The @FetchRequest permanently reads out the persistent storage for fetching stored orders from it. With the “predicate” argument, we filter out all orders already completed since we don’t want them to display in our ContentView’s List. The @FetchRequest then passes the retrieved orders to the orders property. Whenever we save a new order, the @FetchRequest will notice and add it to the orders data set. Similar to the State functionality, this causes the ContentView to renew its body.

Now we’re ready to display the fetched data inside our List, like this:

List {
    ForEach(orders) { order in
        HStack {
            VStack(alignment: .leading) {
                Text("\(order.pizzaType) - \(order.numberOfSlices) slices")
                    .font(.headline)
                Text("Table \(order.tableNumber)")
                    .font(.subheadline)
            }
            Spacer()
            Button(action: {print("Update order")}) {
                Text(order.orderStatus == .pending ? "Prepare" : "Complete")
                    .foregroundColor(.blue)
            }
        }
        .frame(height: 50)
    }
}
    .listStyle(PlainListStyle())
    //...

Hint: The reason we use a ForEach loop inside the List instead of inserting the orders data set in the List itself will become clear when deleting orders.

When we run our app again, we see that our @FetchRequest successfully retrieves the just saved order from the persistent storage.

Hint: Also, our ContentView preview shows us different orders. However, these are not the ones that are located in the device’s persistent storage, e.g., the simulator. Rather, they are generated for test purposes by the preview property of our PersistenceController. Can you remember when we adapted the corresponding code at the beginning? The resulting sample Order instances are now used by the preview simulator, which itself has no persistent storage.

Updating Core Data entries 🔄

The Button on the right side of each row can be used to update the particular Order’s status. When we add a new Order, its status is .pending. Therefore the Button reads “Prepare”. When the user taps on the Button we want to update the status to .preparing, and the Button should read “Complete”. When the user taps again, we want the Order’s status to be .completed, which causes the @FetchRequest to filter the Order out.

To implement this functionality, we add the following function below our ContentView’s body.

func updateOrder(order: Order) {
        let newStatus = order.orderStatus == .pending ? Status.preparing : .completed
        viewContext.performAndWait {
            order.orderStatus = newStatus
            try? viewContext.save()
        }
    }

We can call the updateOrder function from our row’s button with passing the particular order instance:

Button(action: {
    updateOrder(order: order)
}) {
    Text(order.orderStatus == .pending ? "Prepare" : "Complete")
        .foregroundColor(.blue)
}

Now we can run the app and tap on the “Prepare” button to mark the currently pending order as prepared. If we click on “Complete”, the Order will be filtered out and eventually removed from our List.

Deleting orders from the persistent storage 🗑

Deleting stored data is almost as simple as updating it. All we have to do is to delete the specific Order from the viewContext. Then, since the @FetchRequest will automatically detect that the Order was deleted, it will update our ContentView accordingly and remove the row from the table with a nice default animation.

To let the user delete rows, we add the .onDelete modifier to the ForEach loop. We can’t apply this modifier to Lists. This is why we inserted a ForEach loop inside the List.

List {
    ForEach(orders) { order in
        //...
    }
        .onDelete { indexSet in
            for index in indexSet {
                viewContext.delete(orders[index])
            }
            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
}

The .onDelete modifier detects the row(s) the user wants to delete by swiping and uses there index/indices to remove the corresponding Order entries from the viewContext.

If we run the application now, we can see that we can easily delete the order by swiping a row.

Conclusion 🎊

That’s it. We finished our small pizza restaurant app! You learned how to use Core Data in SwiftUI to store data persistently. We talked through all basic CRUD operations: Creating, reading, updating, and deleting data. We also understood what a managedObjectContext is and how we can fetch stored data by using SwiftUI’s @FetchRequest.

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


If you liked this tutorial, feel free to check out our Mastering SwiftUI eBook. In this book, we also created a To-do app by using the mentioned Core Data functionalities!

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, check out our other tutorials! Also, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials, and tips about SwiftUI and more!

Categories
Uncategorized

SwiftUI – How to create a Tab Bar

Updated for Xcode 12.0 and SwiftUI 2.0 ✅

Tab bars allow the user to switch between different views of your app quickly. You see them in almost every iOS app. Fortunately, creating a tab bar with SwiftUI is very simple, using a TabView instance 🚀

And this is how it works:

Step 1: Prepare the SwiftUI views your tab bar will contain later. Create a SwiftUI view file for each tab view of the tab bar. To keep your project organized, you can put them in a separate group.


Step 2: Next, we create a new SwiftUI view file called “HostingTabBar”. This HostingTabBar should be the entrance/root view when our app gets launched. For this purpose, we change the SwiftUI App Lifecycle in our *YourAppName*App struct.

@main
struct TabBarApp: App {
    var body: some Scene {
        WindowGroup {
            HostingTabBar()
        }
    }
}

Step 3: Next, we add an enum to our HostingTabBar, which we use to represent our tab bar’s different views. We also declare a State property that we can use to monitor and programmatically control, which Tab is currently shown.

struct HostingTabBar: View {
    
    private enum Tab: Hashable {
        case home
        case explore
        case user
        case settings
    }
    
    @State private var selectedTab: Tab = .home
    
    var body: some View {
        //...
    }
}

Step 4: Now we can add a TabView instance to our HostingTabBar. For this, we bind the selectedTab State to the initialized TabView. 

var body: some View {
    TabView(selection: $selectedTab) {
        
    }
}

Step 5: Next, initialize the different tab views. Wrap them into the TabView closure and assign a unique .tag to each tab view. With the help of each tab view’s .tag, we can control which tab view should be open and move between them programmatically.

TabView(selection: $selectedTab) {
    HomeView()
        .tag(0)
    ExploreView()
        .tag(1)
    UserView()
        .tag(2)
    SettingsView()
        .tag(3)
}

Step 6: Using the .tabItem modifier, you can now design the individual tab items. We can construct a tab item using a Text view, an Image view, or a combination of both.

TabView(selection: $selectedTab) {
    HomeView()
        .tag(0)
        .tabItem {
            Text("Home")
            Image(systemName: "house.fill")
        }
    ExploreView()
        .tag(1)
        .tabItem {
            Text("Explore")
            Image(systemName: "magnifyingglass")
        }
    UserView()
        .tag(2)
        .tabItem {
            Text("User")
            Image(systemName: "person.crop.circle")
        }
    SettingsView()
        .tag(3)
        .tabItem {
            Text("Settings")
            Image(systemName: "gear")
        }
}

If you now run the app and tap on a tab item, you can move between the tab views.

You can programmatically navigate between the tab views by changing the Tab assigned to the selectedTab State (for example, by using a Button).

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics eBook and our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials, and tips about SwiftUI and more!