Mobile App Basics: iOS Swift UI (Part 2)

Elliott
Terra
Published in
6 min readApr 17, 2023

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!

--

--