< 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 >
Strava Pulls the Plug on their API: What This Means for Developers

Strava Pulls the Plug on their API: What This Means for Developers

Strava discontinued their API service, changing the ecosystem of third-party apps that have relied on their platform. How can developers react to this?

Terra APITerra API
November 21, 2024
Alternatives to the latest changes in the Strava API

Alternatives to the latest changes in the Strava API

Strava just introduced big changes to their API program. These changes will basically kill off a lot of apps. Use Terra API instead to avoid this

Kyriakos EleftheriouKyriakos Eleftheriou
November 19, 2024
Cycling Legend, Investor, and Podcaster - Lance Armstrong

Cycling Legend, Investor, and Podcaster - Lance Armstrong

In this podcast, Kyriakos the CEO of Terra interviews Lance Armstrong about his journey as a young athlete, cycling champion, and successful investor and podcaster.

Terra APITerra API
November 8, 2024
Founder of Don’t Die - Bryan Johnson

Founder of Don’t Die - Bryan Johnson

In this podcast, Bryan Johnson shares his personal story of how he began his journey to becoming the the world's most measured human.

Terra APITerra API
October 25, 2024
CEO and Co-Founder of Veri - Anttoni Aniebonam

CEO and Co-Founder of Veri - Anttoni Aniebonam

In this podcast with Kyriakos the CEO of Terra, Anttoni Aniebonam shares his journey founding Veri, and his decision in the acquisition by Oura to further his vision.

Terra APITerra API
September 27, 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.