Categories
Uncategorized

7 Steps to become an iOS Developer

Step 1: Get a Mac & Download Xcode

Screenshot 2019-11-16 at 14.07.02.png

First of all, get yourself a macOS system (in case you don’t already have one). Your device should at least be able to run macOS Catalina. Then download Xcode from the App Store, it’s free. It’s the most crucial software for developing iOS apps!

Step 2: Understand the basics of Xcode 

Screenshot 2019-11-16 at 14.07.02.png

After you downloaded Xcode’s, it’s time to understand its basic structure and concepts. Check out this tutorial, to learn everything you need to know to get started with Xcode. We will build our first mini “Hello World”, help you to decide between using UIKit and SwiftUI (two different approaches for iOS development) and talk through Xcode’s basic interface layout.

Step 3: Learn the basics of programming with Swift

58482ce4cef1014c0b5e4a4c.png

After you understood the basics of Xcode, it’s time for learning Swift. Swift is the most often used programming language and especially great for beginners. We’ve created a free eBook for this, you can download it for free!

Step 4: Build apps with tutorials

Untitled-2.png

Start building basic apps with tutorials, there are plenty of them! We created a bunch of SwiftUI tutorials for getting you started! We recommend you to start with this tutorial. After that you can follow along these tutorials. 

Although our free tutorials are covering almost everything you need to know to get learn iOS development, you maybe would like to take a look at our Mastering SwiftUI Book. In this book, we take you by hand and teach you everything you need to know about developing apps with SwiftUI.

Step 5: Deepen your knowledge about the iOS SDK and frameworks

Word Art.png

Once you’ve done some tutorials, deepen your knowledge in topics you are interested in and you want to learn more of. There are so many frameworks to master in iOS development. For example, if you are interested in Augmented Reality apps, google „ARKit tutorial“. You have the choice!

Step 6: Deepen your Swift knowledge 

swift-social-network-brand-logo-512.png

The more you get into iOS app development, the more Swift skills and concepts are required. If you don’t understand some more advanced Swift concepts look it up in the official Swift language documentation (it’s a really great source) or search & ask somewhere, for example on StackOverflow or write us a message!

Step 7: Develop your own, custom apps

👨‍💻👩‍💻

To really become an iOS developer, its crucial to start developing your own, custom apps as soon as possible. This boosts your learning curve and programming skills tremendously.

Categories
Uncategorized

Custom progress bars in SwiftUI

Have you ever had to implement a certain progress bar into your app, for example, to display an ongoing loading process? Well, in SwiftUI it’s super simple to create these 👇

I also uploaded the progress bars templates to GitHub. So if you want to skip this tutorial, you can just copy and paste the files to your project and initialize the specific bars with multiple customization options.

Simple Progress Bar

Let’s start with creating a simple progress bar like this one:


First, create a new SwiftUI file called SimpleProgressBar.

Create a State property assigned to a CGFloat value. We use this to represent the current progress of our loading process. For example, 0.5 means that 50 percent have loaded so far.

struct SimpleProgressBarDemo: View {
    @State var currentProgress: CGFloat = 0.0

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

The next step to create our progress bar is to replace the default Text view with a ZStack with the leading alignment mode.

var body: some View {
    ZStack(alignment: .leading) {

        }
    }

ZStacks are used for stacking elements on top of each other. Click here to learn more. The first object in our ZStack is the „inner“, static bar. To create this, we use a RoundedRectangle.

ZStack(alignment: .leading) {
            RoundedRectangle(cornerRadius: 20)
        }

We want the bar to be gray, wide and thin.

RoundedRectangle(cornerRadius: 20)
                .foregroundColor(.gray)
                .frame(width: 300, height: 20)

Another, dynamic bar should overlay our static one. Therefore, we place another RoundedRectangle into our ZStack. This rectangle should be as high as the first one but only as wide as it if our currentProgress is 1.0, i.e. 100 percent are loaded. To achieve this, we write:

RoundedRectangle(cornerRadius: 20)
                .foregroundColor(.blue)
                .frame(width: 300*currentProgress, height: 20)

To simulate a loading process, we use a timer that adds 0.1 to our currentProgress every second. We implement a corresponding button that starts the timer.

struct SimpleProgressBarDemo: View {
    
    @State var currentProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack(alignment: .leading) {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.currentProgress += 0.01
                if self.currentProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

By using the withAnimation wrapper, we apply a smooth, default animation to our bar’s progress. Run the view in the live preview and click the button to simulate the loading process.

Circular Progress Bar


First, create a new SwiftUI file called CircularProgressBar. Let’s implement a State for keeping track of the loading progress.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        Text("Hello World!")
    }
}

Replace the default Text view with a ZStack again.

ZStack {
            
        }

The first object in our ZStack is the „inner“, static circle. To create it, declare a Circle object.

ZStack {
            Circle()
        }

We don’t want our circle filled out, we just want it to be a ring. To do this we use the .stroke modifier. Next, we modify the width and height of our circle by using the .frame modifier.

Circle()
    .stroke(Color.gray, lineWidth: 15)
    .frame(width: 200, height: 200)

Another, dynamic circle should overlay the static one. Therefore, place a new Circle view into the ZStack. We use the .stroke and .frame modifier again, but this time we use a different color.

Circle()
     .stroke(Color.blue, lineWidth: 15)
     .frame(width: 200, height: 200)

The circle should only be closed when our circleProgress State is 1.0. For example, if the progress state is 0.5, the ring should only be half long. To achieve this we use the .trim modifier (as the first modifier!) and pass the circleProgress to its end argument.

Circle()
    .trim(from: 0.0, to: circleProgress)
    .stroke(Color.blue, lineWidth: 15)
    .frame(width: 200, height: 200)

Now try to change the value of the circleProgress State. You see that our circular progress bar immediately adapts to it.But we don’t want our circular progress bar to start at the top. Therefore, we spin it by 90 degrees.

Circle()
     .trim(from: 0.0, to: circleProgress)
     .stroke(Color.blue, lineWidth: 15)
     .frame(width: 200, height: 200)
     .rotationEffect(Angle(degrees: -90))

If you want, you can also insert a Text indicating how much percent is already loaded.

ZStack {
            //...
            Text("\(Int(self.circleProgress*100))%")
                .font(.custom("HelveticaNeue", size: 20.0))
        }

To simulate the loading process, we implement a corresponding button that fires a timer again.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.circleProgress += 0.01
                if self.circleProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Tip: Subtract twice the line width from the width and height of the circle to achieve a style like this:

ZStack {
                Circle()
                    //...
                Circle()
                    .trim(from: 0.0, to: circleProgress)
                    .stroke(Color.blue, lineWidth: 15)
                    .frame(width: 200-15*2, height: 200-15*2)
                    .rotationEffect(Angle(degrees: -90))
                //...
            }

Halved-Circular Progress Bar

Creating this cool bar is very similar to what we’ve already done.


As always, we create a State property to know how far the loading process is already. We also replace the default text with a ZStack.

    @State var progress: CGFloat = 0.0
    
    var body: some View {
        ZStack {
            
        }
    }

Again, we start by creating a static circle. Only this time we trim it to the half. We also spin it by 180 degrees. To create a dashed ring we use the .stroke modifier again, but this time we use a different StrokeStyle.

Circle()
                .trim(from: 0.0, to: 0.5)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: -180))

We equip our dynamic circle with the usual .stroke modifier and trim it depending on our loading process. We also spin it by 180 degrees.

ZStack {
            Circle()
                //...
            Circle()
                .trim(from: 0.0, to: progress/2)
                .stroke(Color.blue, lineWidth: 12.0)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: -180))
        }

You can add an according Text again and simulate the loading process with a timer.

struct HalvedCircularBar: View {
    
    @State var progress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .trim(from: 0.0, to: 0.5)
                    .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Circle()
                    .trim(from: 0.0, to: progress/2)
                    .stroke(Color.blue, lineWidth: 12.0)
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Text("\(Int(self.progress*100))%")
                    .font(.custom("HelveticaNeue", size: 20.0))
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.progress += 0.01
                if self.progress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Activity Indicator

Although this is not really a progress bar, we want to have a look at how to create an activity indicator in SwiftUI.


We declare a State again, but this time for keeping track of the current indicator’s position by its degree.

@State var degress = 0.0

As usual, we use a static and a dynamic circle. We trim the dynamic circle by a fixed value. To rotate the whole dynamic circle, we use the .rotationEffect modifier and make it dependent on the value of our degrees State.

Circle()
                .trim(from: 0.0, to: 0.6)
                .stroke(darkBlue, lineWidth: 5.0)
                .frame(width: 120, height: 120)
                .rotationEffect(Angle(degrees: degress))

To present a loading process, we can simply use a repeating timer that starts as soon as the view has loaded. The timer adds a few degrees to our State at fast time intervals. Once our circle has one full time, we set our degree State back to 0.

struct ActivityIndicator: View {
    
    @State var degress = 0.0
    
    var body: some View {
        Circle()
            .trim(from: 0.0, to: 0.6)
            .stroke(darkBlue, lineWidth: 5.0)
            .frame(width: 120, height: 120)
            .rotationEffect(Angle(degrees: degress))
            .onAppear(perform: {self.start()})
    }
    
    func start() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
            withAnimation {
                self.degress += 10.0
            }
            if self.degress == 360.0 {
                self.degress = 0.0
            }
        }
    }
}

Conclusion 🎊

Great, we just learned how easy it is to create beautiful progress bars in SwiftUI. If you want to see more, 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

New into iOS development – Should I learn SwiftUI or UIKit & Storyboards

A question that many new iOS developers are facing is: Should I start learning SwiftUI first or should I start with building apps the „old“ way using storyboards and the UIKit framework ❓⠀

When you consider working with SwiftUI, you must be aware of the fact, that apps built with SwiftUI are only working on devices running iOS 13+. However, the adoption rate of iOS users is very high, especially when being compared to Android. So it’s legit to say that you only want to support devices meeting this requirement. But the main problem that comes when working with SwiftUI only, is that this framework is still pretty new and rough. That means that there are not as many resources to work with as with storyboards and UIKit. Plus, that there are still some features that SwiftUI itself is not capable of, for example implementing some more complex controls.⠀

Differentiate between UIKit and storyboards ❗️

First, you must distinguish between UIKit and storyboards. UIKit is a framework that defines the core elements of iOS apps, for example UIImages, UILabels, UITextField, just to name a few. Storyboards on the other side are just a tool to arrange and compose your interface by using these elements.

SwiftUI is another way of composing your interface in a way more intuitive way. SwiftUI on its own also defines such elements as UIKit does (Images, Texts etc.), but at the current state not as much as UIKit. However, SwiftUI will definitely be the future of iOS development and the main framework to work with. And it’s super easy to get started with SwiftUI with it and it has a really high learning curve which is especially great for beginners. So starting to learn iOS development by working with SwiftUI can be really motivating!

SwiftUI + UIKit 🚀

To fill the holes of what SwiftUI is currently not capable of, it’s a big advantage to also know the UIKit framework. This framework will still be an important part of the iOS dev world for at least the next few years. It’s also important to be aware that potential employers will most likely demand UIKit knowledge and skills from you, at least for the next couple of years.⠀

Regarding working with storyboards: At this point, it’s totally possible to build apps not using them but SwiftUI. To build the interfaces you can rely only on SwiftUI + UIKit. So although storyboards are not deprecated yet (and likely won’t be until the next few years), there is no requirement to learn to work with these if you are new to iOS development.

Our recommendation 🎊

To sum it up, we can encourage new iOS developers to start with learning SwiftUI instead of working with storyboards. But at this point, it’s a very big advantage to know at least the basics of UIKit in case it’s not possible to implement the wanted app functionality using SwiftUI only. Fortunately, is super easy to interface between SwiftUI and UIKit. In our SwiftUI eBook, we show you how to do this step-by-step!

Also make sure, you check out our free SwiftUI tutorials!

Categories
Uncategorized

Property Wrappers in SwiftUI – Understanding @State, @ObservableObject, @EnvironmentObject etc.

Property wrappers in SwiftUI provide us with variables with a specific logic depending on the type of the property wrapper. This logic is the core of the data flow concept in SwiftUI. To understand how they work and when to use which one, we explain every one step-by-step 👇

@State

State is probably the most frequently used property wrapper in SwiftUI.

You can easily declare a State by putting the @State keyword in front of a variable.

struct ContentView: View {

    @State var myVariable = 1

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

Hint: State properties must always be related to a view in SwiftUI. So make sure you always declare them inside a View struct (but not inside the View’s body)!

So, what does a State property do? Well, you can read out and manipulate data just as you do with regular variables in Swift. But the key difference is that every time the data of a State changes, the related View gets rebuilt.

To see this in action, take a look at this example:

struct ContentView: View {
    
    @State var myInteger = 1
    
    var body: some View {
        VStack {
            Text("\(myInteger)")
            Button(action: {self.myInteger += 1}) {
                Text("Tap me!")
            }
        }
    }

We have an Integer variable wrapped into a State. In the related view, we have a Text object reading out the State’s data and a button. When we tap on the button, the State’s value get’s increased. As we learned, this manipulation causes the whole view to rerender and the Text to eventually displaying the updated value!

If you want to see another example how to use this functionality in practice, we recommend you to read our Login Page tutorial, where we talk about using States for implementing Text Fields.

@Binding

As we learned above, State properties must always relate to a specific View. But sometimes we want to have access to a State property from the outside, for example from child views.

For creating such a reference, we use the @Binding property wrapper. Suppose you want to create a Binding from a child view to the view containing the State property, just declare a variable inside the child view and mark it with the @Binding keyword.

struct ChildView: View {
    
    @Binding var myBinding: String
    
    var body: some View {
        //...
    }
}

You can then create the binding to the State by initialising the child view with referring to the State by using the “$” syntax.

struct ContentView: View {
    
    @State var myBinding = "Hello"
    
    var body: some View {
         ChildView(myBinding: $myBinding)
    }
    
}

You can then use the Binding as you do with States. For example when you update the Binding property, it also causes the State to change its data which then causes the view to rerender as well.

struct ContentView: View {
    
    @State var myInteger = 1
    
    var body: some View {
        VStack {
            Text("\(myInteger)")
            OutsourcedButtonView(myInteger: $myInteger)
        }
    }
    
}

struct OutsourcedButtonView: View {
    
    @Binding var myInteger: Int
    
    var body: some View {
        Button(action: {self.myInteger += 1}) {
            Text("Tap me!")
        }
    }
}

In the example above, the MotherView contains a State property holding an Integer as well as a Text for displaying that data. The ChildView contains a Binding to that State and a Button for increasing the Binding’s property value. When we tap the button, the State’s data get updated through the Binding which causes the MotherView to rerender and eventually displaying the new Integer.

Again, we recommend you to read our Login Page tutorial if you want to see how to implement Bindings in practice.

@ObservableObject and @ObjectObserver

ObservableObjects are similar to State properties which you already know. But instead of (re)rendering a view depending on its assigned data, ObservableObjects are capable of the following things:

● OberservableObjects are wrapping classes not variables

● These classes can contain data, for example, a string assigned to a variable

● We can bind one or multiple views to the ObservableObject (or better said, we can make these views observe the object).

● 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 rerendered, similar to when a State changes

Here’s an example on how to implement such an ObservableObject:

import SwiftUI
import Combine

struct ContentView: View {
    
    @ObservedObject var myObservedObject: MyObservableObject
    
    var body: some View {
        VStack {
            Text("\(myObservedObject.myInteger)")
            Button(action: {self.myObservedObject.increaseInteger()}) {
                Text("Tap me!")
            }
        }
    }
    
}

class MyObservableObject: ObservableObject {
    
    let objectWillChange = PassthroughSubject<MyObservableObject,Never>()

    var myInteger = 1 {
        didSet {
            objectWillChange.send(self)
        }
    }
    
    func increaseInteger() {
        myInteger += 1
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(myObservedObject: MyObservableObject())
    }
}
#endif

Hint: Make sure you import the Combine Framework and conform to the @ObservableObject protocol. If you want to learn how to do this, check out this tutorial!

We can make a view observe such an ObserableObject by creating an @ObservedObject property. The view can then access the ObservableObject and also manipulate its data. In the example above, the View calls the ObservableObject’s increaseInteger function which increases the value of the myInteger variable inside the MyObservableObject. This manipulation causes the observing ContentView to rerender with eventually showing the updated value.

ObservableObjects are great for sharing data to multiple views, as you learn in the How to navigate in SwiftUI tutorial. Give it a try!

@EnvironmentObject

An EnvironmentObject is a property wrapper for a data model which, once initialised, can share data to all view’s of your app. The cool thing is, that an EnvironmentObject is created by supplying a ObservableObject.

Using EnvironmentObjects is suitable for passing data to several views at once without needing an “initialisation chain”. The exact explanation of what I mean by this can be found in this tutorial, where we use an EnvironmentObject to navigate through our SwiftUI app!

Conclusion 🎊

Today we talked about property wrappers in SwiftUI which are crucial for handling the data flow concept used within SwiftUI. Make sure you check out the linked tutorials!

If you want to see more, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Have any questions about this article? Write it in the comments below 👇

Categories
Uncategorized

How to create an onboarding screen in SwiftUI #2 – Implementing the Page Control

Hello and welcome to this tutorial! In this post, we will learn how to create an onboarding screen in SwiftUI. By doing this we learn a lot about interfacing with UIKit in SwiftUI. In the last part, we started creating our onboarding screen by implementing an UIViewController with feeding it with SwiftUI views. In this part, we will create the rest of our UI, including the “Next” button, the texts and the dotted page indicator. By doing this we will learn how to create a Binding between a UIKit object and a SwiftUI view. We also learn how to insert UIViews into SwiftUI.

Here’s what we are going to achieve:

Where we are 🏛

As a little reminder, here’s the architecture we use for creating our onboarding screen.

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

We already implemented the UIPageViewController including the SwiftUI subviews and enabled the swipe functionality.

What’s left is to create a “Next” button for manually going to the next subview of the PageViewController. In addition to that, we want to create two text sections for displaying information depending on the currently shown subview. Eventually, we want to include a Page Control for indicating the user, which page is currently being shown.

Keeping track of the current page 👁

To update the PageViewController when we click on the “Next” button (which we will implement in a moment), we need to keep track of which page of the PageViewController should be currently displayed.

To do this, let’s create a State property which serves as the “source of truth”, meaning that the PageViewController should get updated according to the value of this State.

So let’s insert a currentPageIndex State inside the OnboardingView. Since when the app launches the first page should be presented, we set the initial value to 0:

struct OnboardingView: View {
   //...
    
   @State var currentPageIndex = 0
   var body: some View {
      //...
    }
}

Depending on this State, the Text objects and the PageViewController should get updated. We’ll see how to accomplish this in a moment.

But first, let’s implement a Button for changing the State’s value when the user taps on it!

Implementing the Button and the Text objects 👷‍♂️

Wrap the PageViewController instance inside the OnboardingView into a VStack and insert a Button object.

var body: some View {
        VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button() {
            }
        }
    }

The button should contain a white arrow with a yellow, round background:

Button() {
    Image(systemName: "arrow.right")
                    .resizable()
                    .foregroundColor(.white)
                    .frame(width: 30, height: 30)
                    .padding()
                    .background(Color.orange)
                    .cornerRadius(30)
            }

If you want, you can outsource the button’s content as an external view like this:

struct OnboardingView: View {
//...
    
var body: some View {
        VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button() {
ButtonContent()
            }
        }
    }
}
struct ButtonContent: View {
var body: some View {
Image(systemName: "arrow.right")
        .resizable()
        .foregroundColor(.white)
        .frame(width: 30, height: 30)
        .padding()
        .background(Color.orange)
        .cornerRadius(30)
    }
}

As said, the currentPageIndex State needs to be updated when the user taps the button. Therefore, let’s write an action that adds one to the State’s value. But be aware that we want to display the first page again when the user reached the last page and taps the button. We can take this into account by writing a suitable if-else statement:

Button(action: {
if self.currentPageIndex+1 == self.subviews.count {
self.currentPageIndex = 0
                } else {
self.currentPageIndex += 1
                }
            }) {
ButtonContent()
            }

To see if this works properly, let’s also insert a (temporary) Text object for showing the current value of the currentPageIndex.

VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button(action: {
if self.currentPageIndex+1 == self.subviews.count {
self.currentPageIndex = 0
                } else {
self.currentPageIndex += 1
                }
            }) {
ButtonContent()
            }
Text("Currently shown page: \(currentPageIndex)")
        }

We can now try out this functionality by running a live preview and tapping on the button. You see, that every time we tap on it, the currentPageIndex State gets updated which triggers the whole view to rerender with updating our Text.

But instead of this temporary Text, we want to display two Text blocks for displaying information depending on the currently shown subview.

So let’s insert two arrays, one for the titles and one for the captions.


struct OnboardingView: View {
//...
    
var titles = ["Take some time out", "Conquer personal hindrances", "Create a peaceful mind"]
var captions =  ["Take your time out and bring awareness into your everyday life", "Meditating helps you dealing with anxiety and other psychic problems", "Regular medidation sessions creates a peaceful inner mind"]
@State var currentPageIndex = 0
var body: some View {
//...
    }
}

Next, insert two Text objects into your OnboardingView, each of it displaying a String of the arrays subscript by the currentPageIndex value. At this point, you can delete the temporary “Current Page” Text object.

VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Text(titles[currentPageIndex])
                .font(.title)
Text(captions[currentPageIndex])
                .font(.subheadline)
                .foregroundColor(.gray)
                .frame(width: 300, height: 50, alignment: .leading)
                .lineLimit(nil)
//...
        }

Your preview should now look as follows:

If you want, you can run the app again and see if the Text’s get updated properly when you tap the button. As you probably already noticed, the PageViewController isn’t updating it’s displayed subview yet. Let’s change this by binding it to our currentPageIndex State!

Bind the PageViewController to the currentPageIndex State

Binding the PageViewController to the currentPageIndex State means that when the State gets updated the PageViewController gets updated too and that when the Binding gets updated the State gets updated. Doing this is pretty straight forward.

Just insert a @Binding property named currentPageIndex into the PageViewController.

struct PageViewController: UIViewControllerRepresentable {
    
    @Binding var currentPageIndex: Int
    
    //...
    
}

Since we can now refer to the currentPageIndex, we can update the updateUIViewController function for showing the subview depending on the currentPageIndex State’s value, which we’ve set to an initial value of 0.

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [viewControllers[currentPageIndex]], direction: .forward, animated: true)
    }

Now we have to update the initialisation of the PageViewController inside the OnboardingView with binding it to the currentPageIndex State of the OnboardingView:

var body: some View {
        VStack {
            PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
                .frame(height: 600)
            //...
        }
    }

Let’s run our app again!

Now every time, we tap the “Next” button, the State gets updated which also updates the currentPageIndex Binding. What then happens is that the PageViewController gets updated which triggers the updateUIViewController function and eventually shows the next subview!

But now try to swipe..you notice that the Text’s don’t get updated! That’s because we’ve not yet told the PageViewController to update the currentPageIndex when the user swipes. Let’s change this!

Update the State when the user swipes 🆕

Updating the currentPageIndex can be done with a delegate pattern. The appropriate method to use is the didFinishAnimating function. Since, as you learned in the last part, the right place to insert delegate methods is the Coordinator subclass, let us conform this class to the UIPageViewControllerDelegate protocol and insert the didFinishAnimating method there:

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        
        //...
        
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.viewControllers.firstIndex(of: visibleViewController)
            {
                parent.currentPageIndex = index
            }
        }
        
    }

The method gets called when the swiping of the user is finished. When that’s the case, we then grab the shown subview and locate its position inside the viewControllers array. Depending on that value, we can then update the currentPageIndex binding which then also updates the currentPageIndex State of the OnboardingView.

Similar as we did with the data source pattern in the last tutorial, let’s assign the Coordinator as the PageViewController’s delegate inside the makeUIViewController function.

func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        
        return pageViewController
    }

Okay, let’s run our app again and see if that works. Great, every time we swipe, the currentPageIndex State of the OnboardingView gets updated trough the updated Binding property of the PageViewController which eventually updates the Text objects of our OnboardingView accordingly!

Inserting an UIPageControl and updating the view’s composition 🖌

Last but not least, we want to have a dotted indicator for showing the user how many pages the PageViewController has and which one is currently being displayed.

Such an indicator is part of the UIKit framework and called UIPageControl, which is an UIView. In the last tutorial, we saw how to insert an UIViewController. Inserting UIViews into SwiftUI views is very similar to do this.

Create a new Swift file called PageControl.swift and make sure the UIKit and SwiftUI frameworks are imported. Then create a struct called PageControl and conform it to the UIViewRepresentable protocol:

import Foundation
import UIKit
import SwiftUI

struct PageControl: UIViewRepresentable {
    
}

Our page control needs to know how many pages the PageViewController has. It also needs to know which page is currently being displayed. So let’s create a property numberOfPages and create a Binding to the currentPageIndex State of the OnboardingView.

struct PageControl: UIViewRepresentable {
    
    var numberOfPages: Int
    
    @Binding var currentPageIndex: Int
    
}

Similar to the UIViewControllerRepresentable protocol, the UIViewRepresentable protocol has two mandatory functions which need to be implemented. The makeUIView and the updateUIView function. The makeUIView function is used for initialising the UIView for the first time. The updateUIView function gets called every time the UIView gets updated.

Let’s start with creating the makeUIView function (make sure you indicate to return an UIPageControl object after the arrow):

 func makeUIView(context: Context) -> UIPageControl {
        
    }

Inside this function, we create an UIPageControl. We set the number of dots by using the built-in numberOfPages property and assigning it to the numberOfPages property we declared above. We then modify the color of the PageControl and return it.

 func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        control.currentPageIndicatorTintColor = UIColor.orange
        control.pageIndicatorTintColor = UIColor.gray

        return control
    }

We then implement the updateUIView function. Every time the currentPageIndex State of the OnboardingView gets updated the currentPageIndex Binding of the PageControl gets too. This then triggers the updateUIView function. So let’s use this function to tell the PageControl which dot of the PageControl should be highlighted when the user goes to another page.

struct PageControl: UIViewRepresentable {
    
    //...
    
    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPageIndex
    }
    
}

Now we just need to initialise a PageControl instance inside our OnboardingView with passing the number of pages and binding it to the currentPageIndex State:

var body: some View {
        VStack {
            //...
            PageControl(numberOfPages: subviews.count, currentPageIndex: $currentPageIndex)
        }
    }

This is a great opportunity for reordering the elements of our UIView and adding some paddings to make it look more nicely. If you want, you can just copy and paste the code below.


var body: some View {
        VStack(alignment: .leading) {
            PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
                .frame(height: 600)
            Group {
                Text(titles[currentPageIndex])
                    .font(.title)
                Text(captions[currentPageIndex])
                .font(.subheadline)
                .foregroundColor(.gray)
                .frame(width: 300, height: 50, alignment: .leading)
                .lineLimit(nil)
            }
                .padding()
            HStack {
                PageControl(numberOfPages: subviews.count, currentPageIndex: $currentPageIndex)
                Spacer()
                Button(action: {
                    if self.currentPageIndex+1 == self.subviews.count {
                        self.currentPageIndex = 0
                    } else {
                        self.currentPageIndex += 1
                    }
                }) {
                    ButtonContent()
                }
            }
                .padding()
        }
    }

Awesome! Let’s run our app and try everything out!


You can download the whole source code from GitHub.

Conclusion 🎊

That’s it! We’ve successfully created our own onboarding screen in SwiftUI. We learned a lot about interfacing with UIKit and experienced the great power of this synergy. We learned how to implement UIViewControllers and UIViews into SwiftUI views and saw how to embed SwiftUI views into UIKit objects!

If you want to see more, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Have any questions about this article? Write it in the comments below 👇

Categories
Uncategorized

How to create an onboarding screen in SwiftUI #1 – Embedding a UIPageViewController

Hello and welcome to this tutorial! In this post, we will learn how to create an onboarding screen in SwiftUI. By doing this we learn a lot about interfacing with UIKit in SwiftUI. In this part, we will create our onboarding screen by implementing an UIViewController with feeding it with SwiftUI views. By doing this, we will learn how to integrate UIViewControllers into SwiftUI and how to transform SwiftUI Views into UIViewControllers!

Here’s what we are going to achieve:


To get started, create a new Xcode project called “Onboarding” and store it wherever you want. Make sure “Use SwiftUI” is selected. After creating the project, import the images into your Assets.xassets folder and make sure you name them correctly.

Our app’s architecture 🏛

Before diving into the coding part, let’s discuss our onboarding app’s architecture.

It simply consists of one single SwiftUI Content View, called OnboardingView. This content view contains two SwiftUI Text objects and an “arrow” Button. But it also contains a UIPageViewController (which is part of the UIKit), which itself contains three different subviews. The subviews are plain SwiftUI views, but which need to be transformed into UIViewControllers because the embedded UIPageViewControllers only accepts UIViewControllers. In addition to that, the OnboardingView contains an UIPageControl to indicate the current page that’s displayed, which is also part of the UIKit framework.

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

For creating this SwiftUI-UIKit synergy based app, it’s best practice to go from detail to general, meaning that we start with creating our subviews, which we then use to feed our UIPageViewController.

Creating the subviews 🖌

As you saw in the graphic above, the subviews are of a very simple composition. They only consist of one overall image. We could create one single SwiftUI view for every subview but in this example, it’s more efficient to compose a single one and initialise it multiple times with different images.

So let’s create a File-New-File, select SwiftUI view and click on “Next”, name it Subview and then click on “Create”.

As said, we want to dynamically display different images when initialising this view later on. So let’s declare a variable imageString for this purpose.

import SwiftUI

struct Subview: View {
    var imageString: String
    var body: some View {
        //...
    }
}

Next, insert an Image object with passing the imageString variable. To properly frame the image, also add some modifiers.

var body: some View {
        Image(imageString)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .clipped()
    }

Now we need to update our previews struct for showing a sample image.

struct Subview_Previews: PreviewProvider {
    static var previews: some View {
        Subview(imageString: "meditating")
    }
}

This is how your subview should look like now:

That’s all! Theoretically, we could now use this view for creating our subviews.

But as said, we can’t directly use SwiftUI views for feeding a UIPageViewController. Instead, we first need to transform them into UIViewControllers.

Transforming SwiftUI Views into UIViewControllers 🔄

But first, we have to create our onboarding view, especially for holding our UIPageViewController, as you saw in the App’s hierarchy section. To do this, create a new SwiftUI file named OnboardingView.

As mentioned, the OnboardingView will be the place to embed our UIPageViewController in.

Because of this, it’s also the right place to initialise our subviews for passing them to the UIPageViewController (which we will create in a moment). So let’s create an array for storing our subviews:

struct OnboardingView: View {
    
    var subviews = [
        
    ]
    
    var body: some View {
        //...
    }
}

Because our UIPageViewcontroller will only accept UIViewControllers we have to transform our subviews into UIViewControllers. We can simply do that by declaring an UIHostingController for every subview respectively. UIHostingControllers are used for wrapping SwiftUI views in order to use them within the UIKit framework.

struct OnboardingView: View {
    
    var subviews = [
        UIHostingController(rootView: Subview(imageString: "meditating")),
        UIHostingController(rootView: Subview(imageString: "skydiving")),
        UIHostingController(rootView: Subview(imageString: "sitting"))
    ]
    
    var body: some View {
        //...
    }
}

At this point, you can delete the default ContentView.swift file since we won’t need it anymore. But then we have to set our OnboardingView as the root view when the app launches. We do this within the scene function inside the scenedelegate.swift file.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: OnboardingView())
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Great, we successfully prepared our subviews for feeding our UIPageViewController, which we will create now!

Creating the UIPageViewController 🚀

So we just learned how to convert SwiftUI views into UIViewControllers using an UIHostingController. But how can we embed a ViewController of the UIKit framework, like a PageViewController is one, into our SwiftUI OnboardingView?

First of all, let’s create a Swift file called PageViewController. Make sure you import the SwiftUI and UIKit framework. Then create a struct called PageViewController.

import Foundation
import UIKit
import SwiftUI

struct PageViewController {
    
}

We want to work with our subviews in this file. Therefore, declare an appropriate array for holding them, which will get “filled” with the UIHostingControllers when initialising the PageViewController inside the OnboardingView.

struct PageViewController {
    
    var viewControllers: [UIViewController]
    
}

In this file, we will create our UIPageViewController for embedding it into the OnboardingView. For being able to do that, we must conform it to the UIViewControllerRepresentable protocol.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
}

The UIViewControllerRepresentable protocol has three mandatory methods:

  • makeUIViewController: This method is used for creating the UIViewController we want to present.
  • updateUIViewController: This method updates the UIViewController to the latest configuration every time it gets called .
  • makeCoordinator: This method initialises a Coordinator which serves as a kind of a servant for handling delegate and datasource patterns and user inputs. We will talk about this in more detail later.

Let’s start by creating our UIPageViewController by using the makeUIViewController method. Because we want to return a UIPageViewController we have to indicate this behind the arrow like that:

func makeUIViewController(context: Context) -> UIPageViewController {
        
    }

Let’s create the UIPageViewController and return it:

func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)

        return pageViewController
    }

The updateUIViewController will get called every time the UIPageViewController gets updated, for example when the number of content views of it are changing. Because we use a static number of view controllers for our PageViewController, this function will only get called once, namely when the UIPageViewController is being rendered first. When this happens, we have to configure our PageViewController with setting the first view controller of the viewControllers array to display first. We do this by using the setViewController method.

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [viewControllers[0]], direction: .forward, animated: true)
    }

At this point, our PageViewController is ready to be embedded! We can now insert it into our OnboardingView by passing our Subviews. Let’s also add a frame to it.

struct OnboardingView: View {
    
    //...
    
    var body: some View {
        PageViewController(viewControllers: subviews)
            .frame(height: 600)
    }
}

Hint: We will implement the Text objects, the buttons and the Page Control, in the next part of this tutorial.

Great, we successfully implemented an UIPageViewController into a SwiftUI view! But when running the app in live mode, you notice that we are currently not able to swipe back and forth. This is because we didn’t yet implement a datasource pattern for telling our Page View Controller what subview to display when the user swipes back or forth.

Let’s change this by implementing a Coordinator!

Implementing the Coordinator 👷‍♂️

A Coordinator is implemented by creating a subclass inside the PageViewController, which we can then use for implementing the common datasource functions we need for telling our PageViewController which subview to display when the user swipes.

The Coordinator is setup by inserting a Coordinator subclass into the PageViewController with having a parent variable (of the superclass type, in our case that’s PageViewController) and a init function which we need for initialising the Coordinator in our PageViewController.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
    class Coordinator: NSObject {
        
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }
        
    }
    
}

We can now initialise the Coordinator by calling the makeCoordinator method of the UIViewControllerRepresentable protocol.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIPageViewController {
        //...
    }
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        //...
    }
    
    class Coordinator: NSObject {
        
        //...
        
    }
    
}

As said we can use our Coordinator for implementing common Cocoa patterns, like delegates, data source and responding to user input. We want our Coordinator to act as the data source for our UIPageViewController. So let’s conform the Coordinator Subclass to the UIPageViewControllerDataSource protocol.

struct PageViewController: UIViewControllerRepresentable {

//...
    
class Coordinator: NSObject, UIPageViewControllerDataSource {

}

This protocol requires one function for returning the right ViewController after the currently displayed ViewController in our viewControllers array and one for returning the right ViewController before the currently displayed ViewController of our viewControllers array.

So let’s add the viewControllerBefore and viewControllerAfter methods for telling our PageViewController which view controller of the viewControllers array to display when the user swipes.

class Coordinator: NSObject, UIPageViewControllerDataSource {
        
        //...
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            //retrieves the index of the currently displayed view controller
            guard let index = parent.viewControllers.firstIndex(of: viewController) else {
                 return nil
             }
            
            //shows the last view controller when the user swipes back from the first view controller
            if index == 0 {
                return parent.viewControllers.last
            }
            
            //show the view controller before the currently displayed view controller
            return parent.viewControllers[index - 1]
            
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            //retrieves the index of the currently displayed view controller
            guard let index = parent.viewControllers.firstIndex(of: viewController) else {
                return nil
            }
            //shows the first view controller when the user swipes further from the last view controller
            if index + 1 == parent.viewControllers.count {
                return parent.viewControllers.first
            }
            //show the view controller after the currently displayed view controller
            return parent.viewControllers[index + 1]
        }
    }

Since our Coordinator subclass now conforms to the UIPageViewControllerDataSource protocol, we can assign it as the data source of our PageViewControllers inside our makeViewController method.

func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        
        pageViewController.dataSource = context.coordinator
        
        return pageViewController
    }

Great! Now every time the user swipes to a new subview, the Coordinator’s viewControllerAfter/viewControllerBefore method gets called and tells the PageViewController to display the previous or next view controller!

We can check if that’s works by running our app/the OnboardingView preview in live mode and swiping. You see, that every time we swipe, the correct subview get displayed to us!

This is what we’ve accomplished so far:


You can download the source code from GitHub (Part1/App Onboarding folder)

Conclusion 🎊

Great, we just learned how to use a UIPageViewController in SwiftUI! Beside that, we also learned how to feed this controller with SwiftUI views by using UIHostingControllers. In the next article, we will finish our app onboarding, by adding the Texts, setting up the dotted page indicator and implementing a button which the user can use for going to the next view of the PageViewController.

If you want to see more, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Have any questions about this article? Write it in the comments below 👇

Categories
Uncategorized

How to navigate between views in SwiftUI by using an @EnvironmentObject

Hello and welcome to this tutorial! In this article series, 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.

In the last part, we learned how to do this by using an @ObservableObject. 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:

Where we are 👓📚

So we just figured out how to navigate between different views using a ObservableObject. In short, we created a ViewRouter and bound our Mother View and the Content Views to it. We then manipulate the ViewRouter’s currentPage property when clicking on the Content Views buttons. After this, the MotherView gets updated, showing the correct Content View!

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 “NavigateInSwiftUIComplete” folder):

GitHub

Why using a ObservableObject is not the best solution⚠

You are probably asking yourself: Why should we do it any other way when our current solution works fine? Well, it should get clear when looking on our app’s hierarchy logic. Our MotherView is the root view which initialises the ViewRouter instance. In the MotherView we also initialise the ContentViewA and ContentViewB with passing the ViewRouter instance as the BindableObject to them.

You see, that we must follow a strict hierarchy which passes the initialised ObservableObject downwards to all subviews. This is currently not a big deal, but imagine a more complex app with a lot of views. We must always be aware to pass the initialised Observable of the root view down to all subviews and to all subviews of the subviews etc., which eventually can get pretty messy.

In one sentence: Using a pure ObservableObject can get problematic when it comes to 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 can be directly bound to this instance, or better said, are observing this instance, with no regard to the app’s hierarchy. The ViewRouter instance would then be like a cloud that flies above our app’s code where all Views have automatically access to, without taking care of a proper initialisation chain downwards the view’s hierarchy.

Doing this is the perfect job for an EnvironmentObject!

What is an EnvironmentObject? 🧐

An EnvironmentObject is a data model which, once initialised, can share data to all view’s of your app. The cool thing is, that an EnvironmentObject is created by supplying a ObservableObject, thus we can use our ViewRouter for creating an EnvironmentObject!

So, once we declared 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 a 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 MotherView as the root view will look into the ViewRouter‘s currentPage property we need to initialise the EnvironmentObject at the app’s launch. We can then automatically change the EnvironmentObject’s currentPage variable from the ContentView’s which then triggers the MotherView to rerender.

Implement the ViewRouter as an EnvironmentObject 🤓

So let’s update our app’s code!

First, change the viewRouter property wrapper inside the MotherView from an @ObservableObject to an @EnvironmentObject.

import SwiftUI

struct MotherView : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
//...
    }
}

The viewRouter property now looks for a ViewRouter-EnvironmentObject. Thus, we need to provide our MotherView_Previews struct with an according instance:

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

As said when launching our app, it must immediately be provided with a ViewRouter instance as the EnvironmentObject, because the MotherView as the root view now refers to such an EnvironmentObject. Therefore, update the scene function inside the SceneDelegage.swift file as follows:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(ViewRouter()))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Great, SwiftUI now creates a ViewRouter instance as an EnvironmentObject when the app launches, to which all views of our app can now be bound.

Next, let’s update our ContentViewA. Change the viewRouter property of it to an EnvironmentObject as well and also update the ContentViewA_Previews struct.

import SwiftUI

struct ContentViewA : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
       //...
    }
}
#if DEBUG
struct ContentViewA_Previews : PreviewProvider {
    static var previews: some View {
        ContentViewA().environmentObject(ViewRouter())
    }
}
#endif

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

Let’s repeat this for the ContentViewB:

import SwiftUI

struct ContentViewB : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        //...
    }
}
#if DEBUG
struct ContentViewB_Previews : PreviewProvider {
    static var previews: some View {
        ContentViewB().environmentObject(ViewRouter())
    }
}
#endif

Since our ContentView’s viewRouter properties are now directly bound to/observing 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 {
        VStack {
            if viewRouter.currentPage == "page1" {
                ContentViewA()
            } else if viewRouter.currentPage == "page2" {
                ContentViewB()
            }
        }
    }
}

And that’s the cool thing: We no more need to initialize the ViewRouter inside our MotherView and pass this instance downwards to ContentView’s which 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!

Add a transition animation 🚀

As a bonus, let’s look on how to add a transition animation when going from “page1” to “page2”.

Doing this in SwiftUI, is pretty straight forward.

Take a look at the willChange method we call within the ViewRouter.swift file when the currentPage get’s updated. As you learned, this triggers the bound MotherView to rerender its body, eventually showing another ContentView, which means navigating to another ContentView. We can simply add an animation functionally to this by wrapping the willChange method into a withAnimation function:

var currentPage: String = "page1" {
        didSet {
            withAnimation() {
                willChange.send(self)
            }
        }
    }

Now we can add a transition animation when showing another Content View.

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

Apple

We want to provide our app with 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 types or create even a custom one (but that’s a topic for another article). For adding a “pop up” transition we choose the .scale transition type.

var body: some View {
        VStack {
            if viewRouter.currentPage == "page1" {
                ContentViewA()
            } else if viewRouter.currentPage == "page2" {
                ContentViewB()
                    .transition(.scale)
            }
        }
    }

To see if that’s works, run your app in the normal simulator:


Cool, 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 why it’s better to use an EnvironmentObject for navigating between views in SwiftUI and how to accomplish this. We also learned how to add a transition animation to the navigation. If you want to see more, 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

How to navigate between views in SwiftUI by using an @ObservableObject

Hello and welcome to this tutorial! In this article, 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.

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

Here is what we are going to achieve :


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 with a grumpy dog and a Button saying “Next”. The other view, called ContentViewB shows an Image with a happy dog and a Button saying “Back”.

You can download the starter project here:

Starter Project

Now we want to connect these two views in a way that when clicking on the buttons we navigate between the views. We could kinda accomplish this using a NavigationView, but for this one, we don’t want to create a Navigation View Hierarchy but 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 holds both Content Views as its subviews. For this purpose, create a new File-New-File-SwiftUI View and call it MotherView. This is the place where we want to show the ContentViewA or ContentViewB depending on which page was selected by the user.

Important: Since our MotherView will “contain” the ContentViews, it must be the default view when the app launches. To set the MotherView as the root view, go into the SceneDelegate.swift file and update the scene function:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: MotherView())
self.window = window
            window.makeKeyAndVisible()
        }
    }

Back to our MotherView: To keep track of the page selection, we first have to declare a State property. At default, we want to see the first page so let’s assign an according string to the State

import SwiftUI

struct MotherView : View {
    @State var 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 current State, we want to either display the ContentViewA or ContentViewB, so let’s implement this logic by inserting an If-Statement inside a VStack.

var body: some View {
        VStack {
            if page == "page1" {
                ContentViewA()
            } else if page == "page2" {
                ContentViewB()
            }
        }
    }

Hint: We have to wrap it into a VStack (or any other Stack) because conditional statements can’t be wrapped directly into the body of a view.

Let’s run our preview simulator and take a look at it. Since our State is currently assigned to “page1”, our first condition is met and the ContentViewA gets displayed to us. Let’s change the page State to “page2” and see what happens. Here we go! Because the State changed, our whole MotherView gets rerendered and the else-if block gets executed, this time showing us ContentViewB. So far, so good.


But we want to enable the user to change this selection by clicking 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 possibility for accessing the MotherView from the outside, meaning that when, for instance, clicking on the “Next” Button of ContentViewA we want to toggle the page State of the MotherView for eventually navigating to ContentViewB.

To achieve this we have to interject a thing called ObservableObject inside our MotherView – ContentViews hierarchy.

Observable Objects ?! 🤯

At this point, you are probably asking yourself: What the heck are Observable Objects?! Well, understanding this can be pretty complicated but don’t worry, we will explain it in detail.

ObservableObjects are similar to State properties which you should already know. But instead of (re)rendering a view depending on it’s assigned data, ObservableObjects are capable of the following things:

  • ObservableObjects can contain data, for example, a string assigned to a variable
  • We can bind views to the ObservableObject (or better said, we can make these views observe the object). 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 a State changes

So how could we use this functionality? Well, we can create a Observable 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. We then tell our MotherView to show the appropriate ContentView depending on the ObservableObject’s currentPage variable.

From the ContentViews we can update the Observable’s variable. This would trigger rerendering of all three views, including the mother view. With this functionality, we can achieve that the Mother View will always show the right Content View 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 our ObservableObject. To do this, create a new Swift file and call it ViewRouter.swift. Make sure you import the SwiftUI and Combine framework since we need them for implementing subscription/notification functionality. Then create a class called ViewRouter conforming to the ObservableObject protocol.

import Foundation
import Combine
import SwiftUI

class ViewRouter: ObservableObject {
    
}

As said, a ObservableObject notifies and updates all of its observing views when a change happens. For doing this we must declare a constant objectWillChange and assign it to a PasstroughObject.

class ViewRouter: ObservableObject {
    
    let objectWillChange = PassthroughSubject<ViewRouter,Never>()
    
}

Hint: The objectWillChange property is a part of the Combine framework and needs to be assigned to a Passthrough Subject. The Passthrough Subject literally passes its data to any view that’s observing (in our case that will be especially our MotherView) whenever the objectWillChange property gets called. The first parameter inside the “<>”s is the data that gets passed, in our case that should be the ViewRouter. The second parameter can be used for setting a rule when an error should be thrown, in our case that should be never the case. For more details, check out this great tutorial by HackingWithSwift!

But what do we mean by, “when a change happens”? As said, the main task of our ViewRouter should be to stay tracked on which page (meaning which Content View) should be shown currently, whether it’s on the launch of the app or when the user’s taps on a Button. For this purpose, we declare a variable called currentPage and assign it to a string “page1” as the default value. This value represents that the first page is selected (we will refer our ContentViewA to the page1 string as you will see later).

class ViewRouter: ObservableObject {
    
    let objectWillChange = PassthroughSubject<ViewRouter,Never>()
    
    var currentPage: String = "page1" 
}

The observers of the ViewRouter, in our case especially the MotherView, should get notified and updated when this value gets changed. So let’s implement a didSet wrapper calling the send method of the objectWillChange property we declared before.

var currentPage: String = "page1" {
        didSet {
            objectWillChange.send(self)
        }
    }

Next, we need to update our MotherView for displaying the right page every time the currentPage gets updated.

Update the MotherView 🔁

To make MotherView to observe the ViewRouter, we can declare a @ObservedObject property, which is used for binding to ObservableObjects. When doing this, we also need to update our MotherView_Previews struct with initialising an instance of the ViewRouter.

struct MotherView : View {
    
    //...
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        //...
    }
}
struct MotherView_Previews : PreviewProvider {
    static var previews: some View {
        MotherView(viewRouter: ViewRouter())
    }
}

In the scenedelegate.swift file we set the MotherView as our start view when the app launches. Thus, not only our preview simulator needs to be provided with a ViewRouter instance but also the “normal” simulator. So switch to the scenedelegate.swift file and update the scene function as follows:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: MotherView(viewRouter: ViewRouter()))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Because we initialised the ViewRouter within our MotherView we can read the currentPage value of the ViewRouter. So let’s show the appropriate Content View depending on the variable’s value!

var body: some View {
        VStack {
            if viewRouter.currentPage == "page1" {
                ContentViewA()
            } else if viewRouter.currentPage == "page2" {
                ContentViewB()
            }
        }
    }

At this point, you can delete the page State of the MotherView, since we won’t need it anymore.

Let’s take a look at the simulator: The MotherView reads the value of the ViewRouter’s currentPage variable and displays the appropriate view. You can proof this by changing the default value of the ViewRouter’s currentPage property to “page2”. Switch back to the MotherView preview simulator and see what happens! The didSet block got executed and the ObservableObject told the MotherView to update its view. (After doing this change it back to page1, since we want ContentViewA to be the default view).

Great, we accomplished a lot so far! We initialised a ViewRouter instance and bound it to the MotherView. Every time the currentPage value of the ViewRouter instance gets updated the MotherView will rerender (since all observing views of a ObservableObject get automatically updated when a change occurs) with showing the right view!

Bind the Content Views to the ViewRouter ⛓

Our MotherView is now able to show the correct Content View depending on the currentPage value of the ViewRouter. But until now, the user is not able to change this value by clicking on the button of the ContentViewA and the ContentViewB, respectively.

Let’s start with our ContentViewA. To enable it to access the currentPage data and manipulate it we have to bind it to the ViewRouter. So let’s create an @ObservedObject property for this.

struct ContentViewA : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        //...
    }
}
struct ContentViewA_Previews : PreviewProvider {
    static var previews: some View {
        ContentViewA(viewRouter: ViewRouter())
    }
}

The ContentViewA should observe ViewRouter instance we created inside the MotherView. So let’s assign this instance to this ObservedObject property inside the MotherView.swift file

struct MotherView : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            if viewRouter.currentPage == "page1" {
                ContentViewA(viewRouter: viewRouter)
            } else if viewRouter.currentPage == "page2" {
                ContentViewB()
            }
        }
    }
}

Great, we now have access to the instance’s currentPage variable. Let’s change this variable’s data to “page2” inside the Button’s action.

import SwiftUI

struct ContentViewA : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            GrumpyDog()
            Button(action: {self.viewRouter.currentPage = "page2"}) {
                NextButtonContent()
            }
        }
    }
}

Okay, let’s try out and see if that works: Run the app in the simulator and click on the “Next” button. Great, we navigate to the ContentViewB!

Here is what happens now when the user taps the Button of the ContentViewA. The ContentViewA changes the currentPage property of the ViewRouter instance to “page2”. Therefore the ViewRouter tells all bound views to rerender, including the MotherView. The MotherView rerenders and checks the currentPage data. Since it’s “page2” the condition for showing the 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 @ObservedObject property as an ViewRouter instance.

struct ContentViewB : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
       //...
    }
}
struct ContentViewB_Previews : PreviewProvider {
    static var previews: some View {
        ContentViewB(viewRouter: ViewRouter())
    }
}

Assign this binding to the initial ViewRouter instance inside the ViewRouter.swift file.

struct MotherView : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            if viewRouter.currentPage == "page1" {
                ContentViewA(viewRouter: viewRouter)
            } else if viewRouter.currentPage == "page2" {
                ContentViewB(viewRouter: viewRouter)
            }
        }
    }
}

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

struct ContentViewB : View {
    
    @ObservedObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            HappyDog()
            Button(action: {self.viewRouter.currentPage = "page1"}) {
                BackButtonContent()
            }
        }
    }
}

We can now navigate forward and backward between our ContentView’s! Try it out in the simulator!


You can download the whole source code here!

Conclusion 🎊

Great, we just learned how to navigate between different views using a ObservableObject. But there exists an alternative, a bit more efficient way, to do this: Using an @EnvironmentObject. EnvironmentObjects provide us with more freedom and independence between 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

How to build a login page in SwiftUI #2 – Outsource bindings and implement State-depending views

This is the second of two parts of our tutorial on how to build a login page just with SwiftUI, for the first one click here.

Welcome to this two-part tutorial series on how to create a full-functional login page by just using SwiftUI. In the last part, we talked about implementing Text Views and learn the basics concept of data flow in SwiftUI by using State properties. In this part, we will extend this knowledge by learning how to create a binding between different views and how to show views dynamically.

Here is what we are going to create:

What you will learn 💡

What you will learn in this part of the tutorial:

  • How to outsource views that are bound to State properties
  • How to represent several views depending on the current State
  • How to move up the view’s content when the keyboard is toggled

How to outsource views depending on a @State property

As said in the last part of this tutorial, outsourcing views that are bound to a State, like our Text Fields are, is a little bit special. Let’s try out to outsource our username Text Field by CMD-clicking on it and selecting Extract subview. Lets call it UsernameTextField.

We get an error saying “Use of unresolved identifier ‘$username'”. This is because when outsourcing a view, it gets wrapped into its own struct. But the UsernameTextField struct doesn’t own such a property which can get bind to the TextField object. To fix this, we need to insert an appropriate property into our outsourced UsernameTextField struct.

But: Instead of creating a State property on its own we want to instead have a property that derives its content from the username State of our ContentView in order to enable data flow between them! To do this, we declare a @Binding property that’s also called username above our UsernameTextField’s body and since we want to derive its content from the username State of the ContentView we don’t assign any data to it. Bindings are used for creating a data connection between a view (here: our outsourced UsernameTextField) and its source of data (here: our username State)

“Use a binding to create a two-way connection between a view and its underlying model.”

Apple
struct UsernameTextField : View {
@Binding var username: String
var body: some View {
return TextField("Username", text: $username)
            .padding()
            .background(lightGreyColor)
            .cornerRadius(5.0)
            .padding(.bottom, 20)
    }
}

To create the “link” between our ContentView and our UsernameTextField we initialise the latter by passing the username State of our UsernameTextField into it! As we did in the last part of this tutorial, we use the dollar-sign syntax again for binding objects.

struct ContentView : View {
@State var username: String = ""
@State var password: String = ""
var body: some View {
        VStack {
WelcomeText()
UserImage()
UsernameTextField(username: $username)
SecureField("Password", text: $password)
                .padding()
                .background(lightGreyColor)
                .cornerRadius(5.0)
                .padding(.bottom, 20)
Button(action: {print("Button tapped")}) {
LoginButtonContent()
            }
        }
        .padding()
    }
}

Now our outsourced UsernameTextField is connected to the username State of our ContentView!

What happens now when the user enters something is that the entered character gets passed from the UsernameTextField to the username property of the outsourced view which serves as a “bridge” and passes the data itself through the binding to the username State of the ContentView.

As learned in the last part the whole view gets then rerendered. The TextField object reads the content of the username State of the ContentView via accessing the username Binding of the UsernameTextField view and using it as a “data bridge”.

Let’s repeat this process for the Secure Field!

1. Outsource the Secure Field by CMD-clicking on it and selecting extract subview. Let’s call it PasswordSecureField.

2. Insert a Binding property without any content into the extracted PasswordSecureField.

struct PasswordSecureField : View {
    
    @Binding var password: String
    
    var body: some View {
        return SecureField("Password", text: $password)
            .padding()
            .background(lightGreyColor)
            .cornerRadius(5.0)
            .padding(.bottom, 20)
    }
}

3. Link the outsourced PasswordSecureField to the State of the ContentView by initialising it inside the ContentView with binding the password State to it.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            UsernameTextField(username: $username)
            PasswordSecureField(password: $password)
            Button(action: {print("Button tapped")}) {
               LoginButtonContent()
            }
        }
        .padding()
    }
}

Great, we finished outsourcing our Text and Secure Field! Let’s go further with designing our view for a failed and successful authentication!

Designing the view for failed and successful user authentication ✍️↔️

A basic approach in SwiftUI is to set up the interface individually for every relevant situation that can occur during the app’s runtime. In our example there are three cases that can occur:

  • Case 1: The user did nothing but entering it credentials into the Text Fields. Especially he didn’t start the authentication process by clicking on the login button. In this situation we can use the default view we created as it is and don’t need to modify anything.
  • Case 2: The user entered the authentication process by tapping the login button and it failed. In this case, we want to display a text saying that the entered information was wrong.
  • Case 3: The user entered the authentication process by tapping the login button and it succeeded. In this case, we want to display a text saying the login was successful.

How our view should look for Case 1, Case 2 and Case 3

Handling the second and the third case is done by using State properties as well!

Let’s start by handling the second case, meaning that the authentication process did fail. For this purpose, we declare a State property of a boolean type and set its default value to false.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    @State var authenticationDidFail: Bool = false
    
    var body: some View {
        
        //[...]
      
    }
}

When the State’s value becomes true that means that the authentication did fail. If this is the case we want to insert a Text object between the password Secure Field and the login Button. Therefore we write:

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    @State var authenticationDidFail: Bool = false
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            UsernameTextField(username: $username)
            PasswordSecureField(password: $password)
            if authenticationDidFail {
                Text("Information not correct. Try again.")
            }
            Button(action: {print("Button tapped")}) {
               LoginButtonContent()
            }
        }
        .padding()
    }
}

Let’s take a look at the Preview Simulator: Where is our created Text? Answer: Because we set the value of the authenticationDidFail State to true, we currently don’t simulate the case that the authentication did fail. Let’s change this:

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = true
    
    var body: some View {
        
        //[...]
        
    }
}

Now we are simulating a failed authentication and therefore see our inserted Text object! Let’s give it a red color and move it a little bit up:

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        VStack {
            //[...]
            if authenticationDidFail {
                Text("Information not correct. Try again.")
                    .offset(y: -10)
                    .foregroundColor(.red)
            }
            //[...]
    }
}

This is how your Preview Simulator should look like now:

Perfect, we styled our view for a failed authentication (our Case 2). Let’s set the authenticationDidFail State to false again to simulate our default view (Case 1).

Let’s repeat this workflow for a successful authentication (Case 3). Again, we need to declare a State property for handling this case. Let’s set it to true to simulate how the view should look if the login process succeeds:

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = false
    @State var authenticationDidSucceed: Bool = true
    
    var body: some View {
        
        //[...]
        
    }
}

If it succeeds, we want a Text to pop out saying that the login succeeded! But we don’t want this text to be within the VStack but on top of it! How do we achieve this?

Fortunately, it’s really straight forward. Just wrap the VStack itself into a ZStack. All objects inside a ZStack gets stacked on top of each other.

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                WelcomeText()
                UserImage()
                UsernameTextField(username: $username)
                PasswordSecureField(password: $password)
                if authenticationDidFail {
                    Text("Information not correct. Try again.")
                        .offset(y: -10)
                        .foregroundColor(.red)
                }
                Button(action: {print("Button tapped")}) {
                    LoginButtonContent()
                }
                }
                .padding()
        }
    }
}

All objects inside a ZStack get stacked on top of each other.

“ZStack: A view that overlays its children, aligning them in both axes.”

Apple

So let’s add a Text object saying “Login succeeded” to the ZStack by inserting it below the VStack.

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
            if authenticationDidSucceed {
                Text("Login succeeded!")
            }
        }
    }
}

You now see that the Text is positioned on top of our VStack.

Let’s modify this Text by styling the font and adding a green background. Also add a .animation modifier with a default animation. This modifier causes the Text to pop up with a decent animation as we will see later.

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
            if authenticationDidSucceed {
                Text("Login succeeded!")
                    .font(.headline)
                    .frame(width: 250, height: 80)
                    .background(Color.green)
                    .cornerRadius(20.0)
                    .foregroundColor(.white)
                    .animation(Animation.default)
            }
        }
    }
}

This is what your preview should look like now:

We are done with handling our third case, so lets set the authenticationDidSucceed State to false again for simulating our default case!

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = false
    @State var authenticationDidSucceed: Bool = false
    
    var body: some View {
        
        //[...]
        
    }
}

Great! We are done view designing our views that should depend on which State is currently “active”. Let’s hop into implementing our code logic for toggling these States appropriately.

Implementing the logic for toggling the States 🤓

So we just defined how our view should look depending on the different States. But how do we toggle these States?

Pretty simple: When the user taps the login button, we need to compare the entered username and password with the stored credentials.

We do not have stored the correct password and username yet. Let’s change this by inserting them below our lightGreyColor constant

import SwiftUI

let lightGreyColor = Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0)

let storedUsername = "Myusername"
let storedPassword = "Mypassword"

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        //[...]
    }
}

If the entered information is correct we want to set the authenticationDidSucceed property to true which then causes the view to rerender with showing the “Login succeeded” Text. Let’s write this into the action parameter of our button:

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
                Button(action: {
                    if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                    }
                }) {
                    LoginButtonContent()
                }
                }
                .padding()
            //[...]
        }
    }
}

If the entered information is not the same as the stored, we want to instead set the authenticationDidFail State to true!

struct ContentView : View {
//[...]
    
var body: some View {
        ZStack {
            VStack {
//[...]
                Button(action: {
if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                    } else {
                        self.authenticationDidFail = true
                    }
                }) {
LoginButtonContent()
                }
                }
                .padding()
//[...]
        }
    }
}

That’s all! Let’s try it out. Run your app in the “normal simulator”. Try to enter the wrong credentials and hit login! What happens now, is that the code inside the action checks if the credentials are correct. Because that is not the case, it toggles the authenticationDidFail State to be true. The view, therefore, rerenders with showing the “Information not correct” Text.

If you enter the right credentials, the authenticationDidSucceed State gets toggled which causes the view to show up the “Login succeeded” Text! Note how the Text pops out with a smooth animation. This is the one we defined earlier by applying the .animation modifier to the Text object.

Maybe you noticed, that when first entering the wrong and then the right information we see both: The disclaimer that the entered information was wrong and the “Login succeeded” Text.

But we don’t want to see both, instead, we want the disclaimer Text to disappear when the login succeeded. We can simply implement this by updating our action:

struct ContentView : View {
//[...]
    
var body: some View {
        ZStack {
            VStack {
//[...]
                Button(action: {
if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                        self.authenticationDidFail = false
                    } else {
                        self.authenticationDidFail = true
                    }
                }) {
LoginButtonContent()
                }
                }
                .padding()
//[...]
        }
    }
}

This makes sure that the authenticationDidFail State is false when the credentials are correct which guarantees that the disclaimer text is not shown when the successful login text is.

Lets rerun our app, write down wrong credentials and tap the login button. After seeing the “Information not correct” Text enter the right credentials and hit the login button again. See how the “Information not correct” Text disappears and how the “Login succeeded” Text shows up instead!

Prevent the keyboard from hiding the Text View

There is one last thing we have to talk about in order to provide a good user experience: When the user taps on a Text Field and enters his password or username, the toggled keyboard hides the Text Field. This causes the user to not be able to see what he enters, which can be pretty annoying.

We outsourced this topic! To learn how to prevent the keyboard from hiding your view’s content, take a look at this tutorial.

Conclusion 🎊

That’s it! We are finished with creating our login page app in SwiftUI. You learned a lot stuff like using States, Binding and how data flow in SwiftUI works. How to manage outsourced views and data input and little tweaks like how to move content up when the user is writing something!

I uploaded the whole source code to GitHub if you want to check it out!

I hope you enjoyed this tutorial. Make sure you subscribe to our newsletter to not miss any updates, a lot of stuff about SwiftUI is coming. Also visit us on Facebook and Instagram. If you have any questions do not hesitate to contact us or leave a comment below!

Categories
Uncategorized

How to build a login page in SwiftUI #1 – Mastering Text Fields and understanding @State

This is the first of two parts of our tutorial on how to build a login page just with SwiftUI, here is the second one!

Welcome to this two-part tutorial series on how to create a full-functional login page by just using SwiftUI. In this part, we will compose a more complex UI and implement features such a Text Fields and Buttons and get to know the basic data flow concepts of SwiftUI.

Here is what we are going to create:

What you will learn 💡

What you will learn in this part of the tutorial:

  • How to compose a complex interface in SwiftUI
  • How to use Text Fields
  • How to use Secure Fields for more user data privacy
  • Understanding data flow in SwiftUI including @State properties
  • How to implement Buttons in SwiftUI

Project creation 🆕

Let’s start with creating a new project for our app. Open Xcode 11 and create a new Xcode project. Then select Single View App and click on Next. Give the project a suitable name and make sure “Use SwiftUI” is selected. Then click on Create.

We want to display the user’s profile image in the app, so let’s import a sample image into the Assets folder. Make sure you name the file appropriately.

We will also use a certain grey color multiple times. So before starting with creating the UI, copy the variable below and insert it above your ContentView struct inside your ContentView.swift file.

import SwiftUI

let lightGreyColor = Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0)

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

Start creating the UI ✍️

Let’s start creating the interface of our login page! As you saw in the demo video, all elements are aligned vertically, so let’s delete the sample Text object and insert a VStack instead. In this stack, we put in all the objects we need for our UI. These objects automatically get stacked, you guessed it, vertically.

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

Let’s start with the “Welcome!” text at the top of our login page. For this, just insert a Text object containing a “Welcome!” string. We also apply some modifiers to it including a .padding modifier for extending the space between the Text and the Image below it which we will create in a moment.

struct ContentView : View {
    var body: some View {
        VStack {
            Text("Welcome!")
                .font(.largeTitle)
                .fontWeight(.semibold)
                .padding(.bottom, 20)
        }
    }
}

To outsource the created Text view, CMD-click on the Text object and select “Extract subview”. Let’s name this view WelcomeText.

Under your WelcomeText place an Image object. The Image should display the profile image of the user. To do this, enter the name of the image which we imported into the Asset folder earlier.

struct ContentView : View {
    var body: some View {
        VStack {
            WelcomeText()
            Image("userImage")
        }
    }
}

We also add some modifiers for scaling the Image and making it round (Check this Instagram post for more details!). Again, we also want a bottom padding.

struct ContentView : View {
    var body: some View {
        VStack {
            WelcomeText()
            Image("userImage")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 150, height: 150)
                .clipped()
                .cornerRadius(150)
                .padding(.bottom, 75)
        }
    }
}

We outsource this view too and call it UserImage.

This is how your preview should look now:

Understanding @State and mastering Text Fields 🤓

Now comes the first one of our two Text Fields. The Text fields enable the user to write down their credentials in order to log in. To create the first one, insert a TextField object into your ContentView’s body. For now, we need two parameters for our TextField: the text binding and the title parameter, which provides us with a placeholder when the TextField is empty.

import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
            WelcomeText()
            UserImage()
            TextField(title:, text:)
        }
    }

Creating a TextField without feeding the necessary parameters causes errors. We will get rid of these in a moment! But first of all, let’s talk about these parameters:

The second parameter accepts a binding. What does this mean?

In a nutshell, SwiftUI needs a property which it can use to store the information that the user enters. This property then processes the information and displays it again to the user.

Let’s create such a property by declaring a variable above our ContentView’s body called username which accepts a String. By default, i.e. when the user does not have anything entered, this variable should contain an empty String.

struct ContentView : View {

	var username: String = “"
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            TextField(title:, text:)
        }
    }
}

But: Currently, this property is only able to read and store the information which an object such as the created TextField passes to it. But it must also be able to automatically render our view depending on its stored content. To do this, mark the variable as a @State.

struct ContentView : View {

	@State var username: String = “"
    var body: some View {
        
        VStack {
            //...
        }
    }
}

A @State property is connected to the view. A @State property is permanently being read by the view. That means that every time the @State property gets changed/updated, the view gets rerendered and eventually displays the content depending on the @State’s data.

To make this more clear, take a look at our TextField example. The TextField is connected to the username State property which contains an empty string as default. When the user enters a character, for example, the letter “A”, this character is passed to the @State property that is connected to the TextView. Because the State gets updated from an empty String to a String “A” it automatically triggers rerendering the view. The TextField then updates itself by reading out the content of the State property, which is now the string “A” and displays it to the User. This is how the user eventually sees what he has entered!

Every time the State gets updated, it causes the view to rerender. Every time the view (re)renders, the State’s content gets read

“State: A persistent value of a given type, through which a view reads and monitors the value.”

Apple

Back to our TextView. Since we created an appropriate State property, we can bind it to our TextField. To bind objects to a State property we have to insert the properties name with a dollar sign.

struct ContentView : View {

	@State var username: String = “"
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            TextField(title: , text: $username)
        }
    }
}

When the TextField is empty (because the String of the State is empty, like it is at default) we don’t want the TextField to display nothing. Instead, we want to display a placeholder. We can do this by passing a Text object as the text argument. We can then delete the parameter’s name.

import SwiftUI

struct ContentView : View {
    
    @State var username: String = ""
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            TextField("Username", text: $username)
        }
    }
}

The TextField itself doesn’t have any border or background by default. To change this we have to add some modifiers to it. We also add a .padding modifier again for extending the space between this TextField and the one we will create in a moment.

struct ContentView : View {
    
    @State var username: String = ""
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            TextField("Username", text: $username)
                .padding()
                .background(lightGreyColor)
                .cornerRadius(5.0)
                .padding(.bottom, 20)
        }
    }
}

Great, we are finished with setting up our first Text View! Outsourcing objects which are connected to a State is a little bit special. Thus, we will do this later.

Next, we want a similar TextField for the user’s password. We could use a “normal” TextField object again. But we want to provide our login form with a little bit of privacy by changing the fields content to dots when the user enters his password.

Doing this is really simple, instead of inserting a TextField object we insert an object called SecureField. The SecureField needs the same arguments as a TextField. Therefore we create another @State property called password and bind it to the SecureField. We also insert an appropriate placeholder. For styling the SecureField like the TextField, just copy & paste the modifiers of the TextField to the SecureField.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            TextField("Username", text: $username)
                .padding()
                .background(lightGreyColor)
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            SecureField("Password", text: $password)
                .padding()
                .background(lightGreyColor)
                .cornerRadius(5.0)
                .padding(.bottom, 20)
        }
    }
}

Also, add an overall .padding modifier to the VStack itself for the fields not touching the edge of the screen.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
           //[…]        
		}
        .padding()
    }
}

This is what your preview should look like now:

Finish the UI by creating the Login button 🛠

Last but not least, let’s insert the Login Button. As described here, to create a button we start with creating the content inside the button. This should be a Text containing “LOGIN”. To modify the Text’s style and insert a green background apply the following modifiers to it.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
	    //[…]
            Text("LOGIN")
                .font(.headline)
                .foregroundColor(.white)
                .padding()
                .frame(width: 220, height: 60)
                .background(Color.green)
                .cornerRadius(15.0)
        }
            .padding()
    }
}

Next, outsource this Text object and call it LoginButtonContent. Now we need to wrap the LoginButtonContent view into a Button. The button needs to know what to do when the user taps it. For now, we just want to print something out (for testing purposes). We will implement our authentication logic in the next part of this tutorial.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
			//[…]
            Button(action: {print("Button tapped")}) {
               LoginButtonContent()
            }
        }
        .padding()
    }
}

Conclusion 🎊

Let’s see where we are. To check out the full functionality, run the app in the “real” simulator, not the preview one. We can now use the text fields to type in the user’s credentials! Note how the password entered by the user gets invisible.

Hint: If you want to use the simulator keyboard and it doesn’t open automatically when clicking on the text fields, click on the simulator and choose on Hardware -> Keyboard -> Toggle Software Keyboard in the macOS toolbar. If you then click on a TextField, the keyboard should open automatically.


This is a great point to remember how the State data flow works: If you enter something it gets passed to the @State property. Because the State’s content gets updated, it automatically triggers rerendering the view. Then, the TextField reads out the State’s content and displays it to the user. This cycle gets executed every time the user enters something.

I uploaded to project’s progress to GitHub if you want to check it out!

Great, we accomplished a lot so far! We composed a complex UI containing a TextField, a SecureField and a Button. We also learned what states are and how the basics of data flow in SwiftUI!

In the next part we will implement our apps functionality including user authentication and displaying different views depending on whether the authentication was successful or not. We also apply some minor improvements like moving the content up when the keyboard opens.

I hope you enjoyed this tutorial. Make sure you subscribe to our newsletter to not miss any updates, a lot of stuff about SwiftUI is coming. Also visit us on Facebook and Instagram. If you have any questions do not hesitate to contact us or leave a comment below!