< Blogs
Elliott

Elliott

April 17, 2023

Mobile App Basics: iOS Swift UI (Part 2)

In the previous article, we explored basic styling and view building in Swift UI for a mobile app which includes:

  • Importing your own images
  • Using your own fonts
  • Using primitive views (Rectangle, Circle, Text, Button)

Knowing all of this, we can now go ahead and learn how to direct the app user to different Views or functionalities. In this article, we will cover some other topics including:

  • Navigating the app to different views
  • Changing a view's state dynamically

The Main View

For the purposes of our demonstration app, we can let the app user select their path at the ContentView View. For now, we have created our own view called WidgetInfo, and we can link the user to this View with a button click fromContentView.

For the design, let's go with this in the ContentView :

On the trigger for Connect Widget button, we will want to link them to WidgetInfo.

The design for this is simple. We will simply be re-using the knowledge we know from Part 1:

struct ContentView: View {
    var body: some View {
        VStack {
            Image("TERRA")
                .imageScale(.large)
                .foregroundColor(.accentColor)
                .padding([.bottom], 32)
            
            Button {
                // link to Widget Info Page
            } label: {
                Text("Connect Widget")
                    .foregroundColor(Color.white)
                    .font(Font.custom("Poppins-Regular", size: 13))
                    .frame(width: 290, height: 50)
            }.background(
                Rectangle()
                    .fill(Color.darkBackground)
                    .cornerRadius(6.5)
            )
            
            Button {
                // Go to Connect device page
            } label: {
                Text("Connect Device")
                    .foregroundColor(Color.white)
                    .font(Font.custom("Poppins-Regular", size: 13))
                    .frame(width: 290, height: 50)
            }.background(
                Rectangle()
                    .fill(Color.lightBackground)
                    .cornerRadius(6.5)
            ).padding([.top], 16)
            
        }
        .padding()
    }
}

Navigation Stack

The question now is, how do we link the user to WidgetInfo when the button is pressed in our View?

We can actually do this with what SwiftUI calls a NavigationStack (or NavigationView ). To do this, we simply need to wrap our entire View in a navigation stack:

struct ContentView: View {
    var body: some View {
        NavigationStack{
            VStack {
                Image("TERRA")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                    .padding([.bottom], 32)
                
                Button {
                // link to Widget Info Page
                } label: {
                    Text("Connect Widget")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.darkBackground)
                        .cornerRadius(6.5)
                )
                
                Button {
                    // Go to Connect device page
                } label: {
                    Text("Connect Device")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.lightBackground)
                        .cornerRadius(6.5)
                ).padding([.top], 16)
                
            }
            .padding()
        }
    }
}

The next thing we would need to use is a NavigationLink. Lucky for us the views Button and NavigationLink contains similar arguments, and in our case, we can actually just replace Button with NavigationLink. The only thing else we would need to add would be to call our WidgetInfo view when the link is triggered:

struct ContentView: View {
    var body: some View {
        NavigationStack{
            VStack {
                Image("TERRA")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                    .padding([.bottom], 32)
                
                NavigationLink {
                    WidgetInfo()
                } label: {
                    Text("Connect Widget")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.darkBackground)
                        .cornerRadius(6.5)
                )
                
                NavigationLink {
                    // Go to Connect BLE
                } label: {
                    Text("Connect Device")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.lightBackground)
                        .cornerRadius(6.5)
                ).padding([.top], 16)
                
            }
            .padding()
        }
    }
}

Let's see it in action:

The back button is actually automatically added because of NavigationLink.

State Variables

Personally, the back button shown from the navigation stack is not the best in terms of style. Let's create our own:

struct BackButton: View{    
    var body: some View{
        ZStack{
            Circle().fill(Color.white)
            Image(systemName: "arrow.backward").foregroundColor(Color.black)
        }.frame(width: 31, height: 31, alignment: .center)
    }
}

The downside of doing so is that we will need to implement our own logic when we want to go back to the ContentView page.

Luckily, we can make use of a very cool feature of Swift UI: State and Binding variables.

Essentially, you propagate changes from one view to another and dynamically cause changes to views using these.

For our back button, we can introduce a Binding variable which we will pass into from the ContentView as a State variable. We simply want this variable to be a Bool for now that we toggle whenever the user touches this back button. For this we will use onTapGesture():

struct BackButton: View{
    @Binding var toggle: Bool
    
    var body: some View{
        ZStack{
            Circle().fill(Color.white)
            Image(systemName: "arrow.backward").foregroundColor(Color.black)
        }.frame(width: 31, height: 31, alignment: .center)
            .onTapGesture {
                toggle.toggle()
            }
    }
}

We will use the toggle variable to represent: "Do we want to show this view or not". This variable will be controlled by ContentView so let's create a State variable in ContentView :

struct ContentView: View {
    
    // The default state here is false (do not show)
    @State var showWidgetInfo: Bool = false
    
    var body: some View {
        NavigationStack{
...

With this, we can then manipulate the navigation stack to determine when we want to show WidgetInfo or not by toggling showWidgetInfo. For now, we can simply replace the NavigationLink with a Button to toggle this variable when pressed:

struct ContentView: View {
    
    @State var showWidgetInfo: Bool = false
    
    var body: some View {
        NavigationStack{
            VStack {
              ...
                Button {
                    showWidgetInfo.toggle()
                } label: {
                    Text("Connect Widget")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.darkBackground)
                        .cornerRadius(6.5)
                )
                
              ...
                
            }
        }
    }
}

Finally, the magic here is, we can add a navigationDestination property to our VStack to tell the app where to go when the value of this state variable is set to true:

struct ContentView: View {
    
    @State var showWidgetInfo: Bool = false
    
    var body: some View {
        NavigationStack{
            VStack {
                Image("TERRA")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                    .padding([.bottom], 32)
                
                Button {
                    showWidgetInfo.toggle()
                } label: {
                    Text("Connect Widget")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.darkBackground)
                        .cornerRadius(6.5)
                )
                
                NavigationLink {
                    // Go to Connect BLE
                } label: {
                    Text("Connect Device")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Regular", size: 13))
                        .frame(width: 290, height: 50)
                }.background(
                    Rectangle()
                        .fill(Color.lightBackground)
                        .cornerRadius(6.5)
                ).padding([.top], 16)
                
            }
            .navigationDestination(isPresented: $showWidgetInfo){
               // Go to Widget Info View
             }
            .padding()
        }
    }
}

Before we can let it navigate the user to WidgetInfo, we need to create a Binding variable in WidgetInfo View's implementation, this way we can link our custom back button to toggle this State variable:

struct WidgetInfo: View{
    
    @Binding var showWidgetInfo: Bool
    var body: some View{
    ...
    }
}

We can then pass a Binding reference into WidgetInfo in ContentView as such:

...
.navigationDestination(isPresented: $showWidgetInfo){
    WidgetInfo(showWidgetInfo: $showWidgetInfo)
}

And, in WidgetInfo we will simply pass this variable along to the custom back button view.

Note: Spacer here is a built-in view that simply fills up the entire space in the HStack after the back button. In addition, we can use .navigationBarBackButtonHidden(true) on the entire view to (you guessed it) hide the hideous built-in back button.

struct WidgetInfo: View{
    
    @Binding var showWidgetInfo: Bool
    
    var body: some View{
        VStack{
            HStack{
                BackButton(toggle: $showWidgetInfo )
                Spacer()
            }
            Image("widgetInfo")
            Text("Widget to connect your app to all wearables")
                .multilineTextAlignment(.center)
                .font(Font.custom("Poppins-Bold", size: 20))
                .frame(width: 261, height: 60)
                .padding([.top], 64)
            Text("Unlock the power of health data")
                .multilineTextAlignment(.center)
                .font(Font.custom("Poppins-Regular", size: 16))
                .frame(width: 296, height: 21)
                .padding([.top], 16)
            HStack{
                Circle().frame(width: 12, height: 12)
                Circle().fill(Color.lightGray).frame(width: 12, height: 12)
                    .padding([.leading], 4)
                Circle().fill(Color.lightGray).frame(width: 12, height: 12)
                    .padding([.leading], 4)
            }
            .padding([.top], 40)
            Button(action: {
              //
            }, label: {
                Text("Continue")
                    .font(Font.custom("Poppins-Regular", size: 13))
                    .foregroundColor(Color.white)
                    .frame(width: 169, height: 33)
                    .background(
                        Rectangle()
                            .fill(Color.darkBackground)
                            .cornerRadius(6.5)
                    )
                    .padding([.top], 105)
            }).padding()
        }.navigationBarBackButtonHidden(true)
    }
}

Let's take a look:

and More...

State and Binding variables can be used to do a lot more than a toggle. You can also have a state variable that is an Int for which can increase every time a button is pressed for example. These really help create a dynamic field to the app and gives the user a much better experience.

We will explore some more exciting topics such as Deep Linking in your app and play around with other more interesting built-in abilities of SwiftUI!

More Topics

All Blogs
Team Spotlight
Startup Spotlight
How To
Blog
Podcast
Product Updates
Wearables
See All >
CEO and Founder of Prenuvo - Andrew Lacy

CEO and Founder of Prenuvo - Andrew Lacy

In this podcast with Kyriakos the CEO of Terra, Andrew Lacy shares his journey with Prenuvo which began from a personal health crisis.

Terra APITerra API
August 28, 2024
MedHacks: Using Wearables To Predict Heart Attacks

MedHacks: Using Wearables To Predict Heart Attacks

A few weeks ago we met Vishal, a recent engineering graduate who wanted to use Terra API as part of his MedHacks hackathon project, Cardio Clarity.

Gursukh SembiGursukh Sembi
August 19, 2024
July 2024 updates

July 2024 updates

Teams API adds Kinexon integration & new webhooks. Terra Health Scores now include Respiratory & Stress metrics. Eight Sleep integration returns with enhanced data.

Alex VenetidisAlex Venetidis
August 2, 2024
Vice President of Teamworks - Sean Harrington

Vice President of Teamworks - Sean Harrington

In this podcast with Kyriakos the CEO of Terra, Sean Harrington shares his journey from founding NoteMeal to becoming the VP of Teamworks.

Terra APITerra API
August 2, 2024
Chief Digital Product Officer of Les Mills - Amber Taylor

Chief Digital Product Officer of Les Mills - Amber Taylor

In this podcast with Kyriakos the CEO of Terra, Amber Taylor shares her journey from childhood running to her leadership role at Nike and navigating cultural differences.

Terra APITerra API
August 2, 2024
next ventures
pioneer fund
samsung next
y combinator
general catalyst

Cookie Preferences

Essential CookiesAlways On
Advertisement Cookies
Analytics Cookies

Crunch Time: Embrace the Cookie Monster Within!

We use cookies to enhance your browsing experience and analyse our traffic. By clicking “Accept All”, you consent to our use of cookies according to our Cookie Policy. You can change your mind any time by visiting out cookie policy.