How to Build a Form UI with SwiftUI

How to Build a Form UI with SwiftUI

How to Build a Form UI with SwiftUI. After playing around SwiftUI for a week, I really enjoy building UI using this brand new framework. With SwiftUI, it makes UI development a breeze and allows you to write much less code.

In the introductory tutorial, we gave you an overview of SwiftUI and walked you through how to build a simple user interface. After exploring the framework for around a week, I really enjoy developing user interface with SwiftUI, even though it’s still buggy at this stage. The framework provides developers with a new way to develop a UI, allowing you to build the same UI with much less code.

The declarative syntax may be new to some developers. It’ll take some time to get used to it. But just like the time when we switched from Objective-C to Swift, you’ll enjoy writing the UI code in the declarative syntax once you manage it. You’ll feel more natural to describe the app layout you want. Together with the new instant preview feature, that lets you write code and preview the visual changes at real-time, the whole development experience is faster and more interactive.

Previously, you learned how to build a table view app and handle navigation. Today, I’d like to show you to build a simple form using SwiftUI. Below is the screen you’ll work on.

Editor’s note: The sample screen is a screen of our FoodPin app that you will learn to build in our beginner course.

Prerequisites of Using SwiftUI and Design Canvas

To follow this tutorial, you will need to have Xcode 11 installed on your Mac. Since the design canvas, that is the instant preview feature, can only work on macOS Catalina. You have to upgrade your macOS to macOS Catalina if you haven’t. At the time of this writing, both Xcode 11 and macOS Catalina (aka macOS 10.15) are currently in beta.

I also assume you have some basic knowledge of Swift. If not, you can check out this free starter guide to learn the basics.

Creating a New Project

We’ll build this screen from scratch. First, create a new project in Xcode 11 using the Single View Application template and name it FormDemo (or whatever name you like). Please make sure you enable the Use SwiftUI option.

Designing the Text Fields

We’ll begin with the implementation of the text fields and the label placing right above each of the text fields. To create a label, use the Text component and write the code like this:

Text("NAME").font(.headline)

We set the label’s value to NAME and change its font type to headline. Like UIKit, the SwiftUI framework comes with a built-in text field component. To create a text field with a placeholder, you can write the code like this:

TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))

To place the label above the text field, you can use a VStack to arrange both components. Your final code should be like this:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
        }
    }
}

In the design canvas, it should show you the preview of the changes in a simulator (depending on your selected model in Xcode).

If you compare the current design of the text fields with the final screen I showed you earlier, we need to make a few changes:

  • Change the background color to light gray
  • Make the text field rounded corner
  • Also, we need to add some paddings

So, update the code like this:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
        }
    }
}

In the code, we updated the TextField by adding paddings for all sides (.padding(.all)) and changing its background color. We also set the corner radius to 5.0 to round the corners. Now your text field should look like this:

Both the text field and the label are too close to the left and right edges. To fix that, we can add some horizontal spaces for the vertical stack (VStack) using the padding property. So, update the code like this:

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            .
            .
            .
        }
        .padding(.horizontal, 15)
    }
}

Once changed, the text field should look better:

Extracting the Text Field for Reuse

As you can see in the final design, all text fields share the same layout and design. Instead of duplicating the code we just wrote for other text fields, it’s better to extract the code to create a separate view for reuse.

Let’s name the struct LabelTextField and move the VStack code block to this struct like this:

struct LabelTextField : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("NAME")
                .font(.headline)
            TextField(.constant(""), placeholder: Text("Fill in the restaurant name"))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
        }
        .padding(.horizontal, 15)
    }
}

For reuse purpose, we’ll further modify the struct to accept two variables: label and placeholder. The LabelTextField should now look like this:

struct LabelTextField : View {
    var label: String
    var placeHolder: String
 
    var body: some View {
 
        VStack(alignment: .leading) {
            Text(label)
                .font(.headline)
            TextField(.constant(""), placeholder: Text(placeHolder))
                .padding(.all)
                .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0), cornerRadius: 5.0)
            }
            .padding(.horizontal, 15)
    }
}

Now going back the ContentView. You can create a text field by constructing a LabelTextFieldwith the specified label name and place holder like below:

struct ContentView : View {
    var body: some View {
        LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
    }
}

The preview should still show you the same label design. But internally we now easily create a text field with different label and placeholder value.

Creating Multiple Text Fields Using List

The final form has multiple text fields for user input. To present multiple text fields in vertical arrangement, you can use VStack to layout the text fields. However, since we can’t display all the information in a single view, we will make the form scrollable by embedding the stack using List. In SwiftUI, it provides a container called List that allows developers to quickly build a table or present rows of data in a single column.

Now update ContentView like this:

struct ContentView : View {
    var body: some View {
        List {
            VStack(alignment: .leading) {
                LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
                LabelTextField(label: "TYPE", placeHolder: "Fill in the restaurant type")
                LabelTextField(label: "ADDRESS", placeHolder: "Fill in the restaurant address")
                LabelTextField(label: "PHONE", placeHolder: "Fill in the restaurant phone")
                LabelTextField(label: "DESCRIPTION", placeHolder: "Fill in the restaurant description")
                }
 
        }
 
    }
}

As soon as you make the changes, you will see the preview updated like this:

If you want to adjust the space between the left/right edge of the text field and the display, you can insert a line of code after VStack:

List {
   VStack(alignment: .leading) {
                ...
   }
   .listRowInsets(EdgeInsets())
}

By using listRowInsets, you can extend the text field closer to the edges of the display.

Adding Featured Photo

You can first download the sample photo here or use whatever photo you like. Before using the photo, import it to the asset catalog.

SwiftUI provides a component called Image for you to present an image like this:

Image("chicken")

If you’ve placed the line of code before the creation of the vertical stack (VStack), you’ll end up with a huge photo that takes up the whole screen. To scale it down, you can write the code like this:

Image("chicken")
    .resizable()
    .scaledToFill()
    .frame(height: 300)
    .clipped()

The code declares the image as a resizable view and set the content mode to Scale to fill. Also, we set the height of the frame to a fixed value. Lastly, we clip the view to its bounding frame. Your result should be like this:

To extend the photo to the edges of the display, you can call listRowInsets and set its value to EdgeInsets(). Update the Image like this:

Image("chicken")
    .resizable()
    .scaledToFill()
    .frame(height: 300)
    .clipped()
    .listRowInsets(EdgeInsets())

Now the featured photo should be perfectly displayed. Lastly, you may notice that the Name field is very close to the bottom edge of the image. Insert the following line of code after the declaration of VStack to give it some space:

.padding(.top, 20)

If you’ve followed the tutorial correctly, your screen should look like this:

Creating a Rounded Button

Now that you’ve created a simple form, let’s see how to implement the Save button. The SwiftUI framework provides a component called Button to create a button. The simplest way of constructing a button can be written like this:

Button(action: {}) {
  Text("Save")
}

Here, we define a standard button named Save. The action parameter takes in a closure, which will be triggered when the button is tapped. For this demo, the button performs nothing.

To better organize our code, we will create another struct and name it RoundedButton :

struct RoundedButton : View {
    var body: some View {
        Button(action: {}) {
            Text("Save")
        }
    }
}

But how can you preview this rounded button? You can create the RoundedButton in ContentViewto preview the design in the current simulator. Xcode 11’s Previews feature lets developers write code to add multiple previews in the design canvas. Modify the ContentView_Previews struct to generate the preview of the rounded button:

struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
            RoundedButton().previewLayout(.sizeThatFits)
        }
    }
}

The Group allows you to group multiple previews. In the code above, we still generate the preview of the ContentView in your selected simulator. For the rounded button, we call the previewLayout method to alter the preview approach. Instead of rendering the rounded button on another simulator, we set the value to .sizeThatFits. This instructs Xcode to generate the preview in a container view like below:

Now update the code of RoundedButton to the following:

struct RoundedButton : View {
    var body: some View {
        Button(action: {}) {
            HStack {
                Spacer()
                Text("Save")
                    .font(.headline)
                    .color(Color.white)
                Spacer()
            }
        }
        .padding(.vertical, 10.0)
        .background(Color.red, cornerRadius: 4.0)
        .padding(.horizontal, 50)
    }
}

We simply change the button’s font type and font color. As you can see in the final deliverable, I want to make the button to look more like a button. So, we give the button a nice background by changing its background color and adding some paddings.

To use this button and add it to the form, you can insert the following line of code in ContentViewand put it after the last text field:

RoundedButton().padding(.top, 20)

We call the padding function to add some extra space between the text field and the Save button. Now your design canvas will show something like this:

Embedding the View in a Navigation View

We’re almost done with the implementation. The final step is to embed the entire form in a navigation view. If you’ve read our introductory tutorial, you know SwiftUI provides a container view named NavigationView for creating a navigation interface. All you need to do is embed the whole list in ContentView in a NavigationView like this:

struct ContentView : View {
    var body: some View {
 
        NavigationView {
            List {
 
                Image("chicken")
                    .resizable()
                    .scaledToFill()
                    .frame(height: 300)
                    .clipped()
                    .listRowInsets(EdgeInsets())
 
                VStack(alignment: .leading) {
                    LabelTextField(label: "NAME", placeHolder: "Fill in the restaurant name")
                    LabelTextField(label: "TYPE", placeHolder: "Fill in the restaurant type")
                    LabelTextField(label: "ADDRESS", placeHolder: "Fill in the restaurant address")
                    LabelTextField(label: "PHONE", placeHolder: "Fill in the restaurant phone")
                    LabelTextField(label: "DESCRIPTION", placeHolder: "Fill in the restaurant description")
 
                    RoundedButton().padding(.top, 20)
                }
                .padding(.top, 20)
                .listRowInsets(EdgeInsets())
            }
 
            .navigationBarTitle(Text("New Restaurant"))
            .navigationBarItems(trailing:
                    Button(action: {
 
                    }, label: {
                        Text("Cancel")
                    })
            )
        }
 
    }
}

To configure the title of the navigation bar, we call the navigationTitle function and specify the text component. If you need to set the bar items, you can call the navigationBarItems function to configure the view appeared on the leading/trailing edge of the navigation bar. In the code above, we configure the trailing edge with a Cancel button.

Previewing the UI with Fixed Layout

That’s how you lay out a form using SwiftUI. One last thing I want to show is the power of Xcode Previews. As you can see, the form is too long for the screen. To view the entire form, you can hit the Play button to run the app and then you can scroll through the form. Xcode 11 offers an alternative way to preview the entire form. Edit the ContentView_Previews struct and initiate the ContentView like this:

ContentView().previewLayout(.fixed(width: 375, height: 1000))

Instead of previewing the layout on a simulated device, we tell Xcode to render the preview in a fixed rectangular frame. By doing that, you can preview the entire form without running the app.

What’s Next

After playing around SwiftUI for a week, I really enjoy building UI using this brand new framework. With SwiftUI, it makes UI development a breeze and allows you to write much less code. I’m still exploring the framework, so please let me know if you find any errors in the tutorial.

In the future tutorials, we will cover other features of SwiftUI such as animations and complex layouts. Please stay tuned and let me know if you have any feedback. Love to read your comment.

Further reading:

How To Code A Dynamic Header In Flutter

Top 5 animations libraries you can use in React Native

How to build a simple CRUD with GraphQL and Flutter

A Deep Dive into Flutter: Front-end Frameworks for Developers

Flutter Tutorial: Flutter Layout Cheat Sheet

Dart Programming Tutorial - Full Course

Build a CRUD app using Firebase and Flutter (Provider)


iOS App Development: How Apple's iOS 13 & Swift 5 Will Change App Development Industry

iOS App Development: How Apple's iOS 13 & Swift 5 Will Change App Development Industry

Want to know how Apple's iOS 13 & Swift 5 will change the iOS app development industry? Refer to this blog that contains in-detail information on it.

In June this year, Apple announced the release of iOS 13 during the annual WWDC event along with a bunch of other releases and launches. The new iOS version- iOS 13, was discussed in detail, and the developer community was given reasons to be excited about the new release.

Dark Mode was one of the most awaited updates that the iPhone users were deprived of. Know more about how to enable dark mode for your iOS application.

Sign in with Apple and updates in background tasks kept the developers counting days. As with every new Apple product, whether it is software or hardware, we all look forward to what’s new in it. The release of iOS13 redefines the way mobile app development occurred today. In fact, the powerful combination of Apple’s iOS 13 and Swift 5 will be akin to Batman and Robin when developing mobile apps for the millennials in a demanding market.

If you have been viewing this space, and looking for an answer to the question – how iOS 13 will change the app development space, then here are some clues.

How iOS 13 and Swift 5 Will Change iOS App Development

  1. Augmented Reality Apps Development with ARKit 2
    Augmented Reality is redefining customer experiences. Remember how Pokemon Go became a rage a few years back? Almost everyone had it installed on their phones. They played it day and night, on the streets, competing to catch virtual Pokemon. In fact, it is the same with all the AR-based app solutions. Apple is making progress with its AR technology and has redefined the ARkit to match the needs of the future.

While the ARKit is getting a refined look, Apple has also announced a new AR technology known as Realitykit which will help the creators of AR apps render photorealistic digital material along with incorporating the right type of animation for the same. The best part of the new AR technology is that it works with Swift 5, which means you can develop AR apps for Mac, iPhone, and iPad with ease.

The company had earlier released headsets for AR app solutions, but the headsets were not connected with iPhone or iPad, however, they were linked to the camera app alone. Things have gradually changed in this department. The announcement of this new technology comes at a time when AR has not only picked up but has also been commanding several applications. This new development technique will enhance how mobile apps were creating experiences until now.

  1. Car Play Apps Development
    This combination will give your car the much-needed upgrade. The new version of CarPlay released with iOS 13 will definitely redefine your in-car experience.

The dashboard is the biggest overhaul this feature has received where you can easily view the current music being played by the stereo, the map showing the route to the destination as well as insights into your destination, all in one place. In fact, the dark/light theme is also available for the car plays dashboard. Basically, the new car play ends an era of switching between two screens and helps enjoy the drive.

The maps have also been revamped for the new version of iOS. You also get a better Siri interface with Carplay, which makes your connection with Siri interesting. Carplay also integrates the calendar app thus, allowing you a quick glance into your day’s appointment. The best part of the upgrade is that your car play screen is independent of the phone screen, which means you can read two different things at the same time.

  1. Machine Learning with Core ML
    The advancements made with CoreML are worth knowing, as it will improve the way you integrate machine learning into your iOS-based mobile apps. The new Core ML 3 will expand the horizons of development and will make it easy for you as a developer to introduce machine learning to the apps.

Apart from ease of development, the next big thing introduced in this version of CoreML is the on-device training. The server side training was done using frameworks such as PyTorch and then these applications were converted to Core ML for better predictions. CoreML3 is the new version that supports both inference and on-device training.

  1. Cloud-Based Apps
    You cannot be slow and demand to become successful at the same time. You ought to accelerate not only your app development process but also how your apps work irrespective of the environment. The cloud-based app solutions are the new feature devised by iOS13 that will work in favour of app developers.

From operations to storage flexibility, the cloud proves to be a brilliant way of getting apps to download faster and improve user experience. iOS13’s introduction of cloud-based apps will not only make app development economical by offering flexible pricing but also improve the functionality of the app solutions.

The new cloud-based app solutions offer an exceptional dashboard thus making data accessible, and pave ways for easy monitoring of the app solutions.

  1. watchOS 6
    With the release of iOS13, the watchOS6 will also be released. The new watchOS will come with shortcuts, custom controls, an upgraded Siri and an audio app. The new OS is also set to launch new health features for the fitness apps that you may develop.

The app store has already defined app solutions that can be used with watchOS6. One is the cycle tracking app which helps women track their menstrual cycles while the other app solution is the noise app which measures the level of noise in your environment.

  1. Increased security
    App developers are always concerned about security within the apps. There is always a fear of user accounts getting hacked or their data being stolen. They want to ensure that there is enough security so that the users feel safe when connecting with the apps. The new version of iOS has strengthened app security. The company will be using App Transport Security to incorporate new security standards whenever they are releasing a new app. The app developers will need to ensure that there is a single channel for all interactions with the mobile app.

Creating an iOS app with user presence using Node.js and Swift

Creating an iOS app with user presence using Node.js and Swift

In this tutorial, we are going to see how you can add user presence to an iOS application using Pusher Channels, Node.js, and Swift.

We will create a sample chat application to demonstrate this feature. However, because we are focusing on just user presence, we will not implement the actual chat feature.

If you are building an application that has a user base, you might need to show your users when their friends are currently online. This comes in handy especially in messenger applications where the current user would like to know which of their friends are available for instant messaging.

Here is a screen recording on how we want the application to work:

If you want a tutorial on how to create a messenger application on iOS, check out this article.


Prerequisites

To follow along you need the following requirements:

  1. Xcode installed on your machine. Download here.
  2. Knowledge of the Swift programming language.
  3. Knowledge of the Xcode IDE.
  4. A Pusher Channels app. Create one here.
  5. Cocoapods installed on your machine. Installation guide.
  6. Node.js and npm installed on your machine. Installation guide.
  7. Basic knowledge of JavaScript and Node.js.

Let’s get started.


Creating the backend of the application

Before creating the iOS application, let’s create the backend application in Node.js. This application will have the necessary endpoints the application will need to function properly. To get started, create a new directory for the project.


Installing the dependencies

In the root of the project, create a new package.json file and paste the following contents into it:

    {
      "name": "presensesample",
      "version": "1.0.0",
      "main": "index.js",
      "dependencies": {
        "body-parser": "^1.18.3",
        "express": "^4.16.4",
        "pusher": "^2.1.3"
      }
    }

Above, we have defined some npm dependencies that the backend application will need to function. Amongst the dependencies, we can see the pusher library. This is the Pusher JavaScript server SDK.

Next, open your terminal application and cd to the root of the project you just created and run the following command:

    $ npm install

This command will install all the dependencies we defined above in the package.json file.


Creating the main application

Next, create a new file called index.js and paste the following code into the file:

    // File: ./index.js
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const app = express();

let users = {};
let currentUser = {};

let pusher = new Pusher({
  appId: 'PUSHER_APP_ID',
  key: 'PUSHER_APP_KEY',
  secret: 'PUSHER_APP_SECRET',
  cluster: 'PUSHER_APP_CLUSTER'
});

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// TODO: Add routes here

app.listen(process.env.PORT || 5000);

In the code above, we imported the libraries we need for the backend application. We then instantiated two new variables: users and currentUser. We will be using these variables as a temporary in-memory store for the data since we are not using a database.

Next, we instantiated the Pusher library using the credentials for our application. We will be using this instance to communicate with the Pusher API. Next, we add the listen method which instructs the Express application to start the application on port 5000.

Next, let’s add some routes to the application. In the code above, we added a comment to signify where we will be adding the route definitions. Replace the comment with the following code:

    // File: ./index.js

// [...]

app.post('/users', (req, res) => {
  const name = req.body.name;
  const matchedUsers = Object.keys(users).filter(id => users[id].name === name);

  if (matchedUsers.length === 0) {
    const id = generate_random_id();
    users[id] = currentUser = { id, name };
  } else {
    currentUser = users[matchedUsers[0]];
  }

  res.json({ currentUser, users });
});

// [...]

Above, we have the first route. The route is a POST route that creates a new user. It first checks the users object to see if a user already exists with the specified name. If a user does not exist, it generates a new user ID using the generate_random_id method (we will create this later) and adds it to the users object. If a user exists, it skips all of that logic.

Regardless of the outcome of the user check, it sets the currentUser as the user that was created or matched and then returns the currentUser and users object as a response.

Next, let’s define the second route. Because we are using presence channels, and presence channels are private channels, we need an endpoint that will authenticate the current user. Below the route above, add the following code:

    // File: ./index.js

// [...]

app.post('/pusher/auth/presence', (req, res) => {
  let socketId = req.body.socket_id;
  let channel = req.body.channel_name;

  let presenceData = {
    user_id: currentUser.id,
    user_info: { name: currentUser.name }
  };

  let auth = pusher.authenticate(socketId, channel, presenceData);

  res.send(auth);
});

// [...]

Above, we have the Pusher authentication route. This route gets the expected socket_id and channel_name and uses that to generate an authentication token. We also supply a presenceData object that contains all the information about the user we are authenticating. We then return the token as a response to the client.

Finally, in the first route, we referenced a function generate_random_id. Below the route we just defined, paste the following code:

    // File: ./index.js

// [...]

function generate_random_id() {
  let s4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
  return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

// [...] 

The function above just generates a random ID that we can then use as the user ID when creating new users.

Let’s add a final default route. This will catch visits to the backend home. In the same file, add the following:

    // [...]

app.get('/', (req, res) => res.send('It works!'));

// [...]

With this, we are done with the Node.js backend. You can run your application using the command below:

    $ node index.js

Your app will be available here: http://localhost:5000.


Building the iOS application

Launch Xcode and create a new sample Single View App project. We will call ours presensesample.

When you are done creating the application, close Xcode. Open your terminal application and cd to the root directory of the iOS application and run the following command:

    $ pod init

This will create a new Podfile file in the root directory of your application. Open the file and replace the contents with the following:

    # File: ./Podfile
target 'presensesample' do
platform :ios, '12.0'

  use_frameworks!

  pod 'Alamofire', '~> 4.7.3'
  pod 'PusherSwift', '~> 5.0'
  pod 'NotificationBannerSwift', '~> 1.7.3'
end

Above, we have defined the application’s dependencies. To install the dependencies, run the following command:

    $ pod install

The command above will install all the dependencies in the Podfile and also create a new .xcworkspace file in the root of the project. Open this file in Xcode to launch the project and not the .xcodeproj file.


Creating the scenes

The first thing we will do is create the storyboard scenes we need for the application to work. We want the storyboard to look like this:

Open the main storyboard file and delete all the scenes in the file so it is empty. Next, add a view controller to the scene.

TIP: You can use the command + shift + L shortcut to bring the objects library.

With the view controller selected, click on Editor > Embed In > Navigation Controller. This will embed the current view controller in a navigation controller. Next, with the navigation view controller selected, open the attributes inspector and select the Is Initial View Controller option to set the navigation view controller as the entry point for the storyboard.

Next, design the view controller as seen in the screenshot below. Later on in the article, we will be connecting the text field and button to the code using an @IBOutlet and an @IBAction.

Next, add the tab bar controller and connect it to the view controller using a manual segue. Since tab bar controllers come with two regular view controllers, delete them and add two table view controllers instead as seen below:

When you are done creating the scenes, let’s start adding the necessary code.


Adding code to the created scenes

Create a new controller class called LoginViewController and set it as the custom class for the first view controller attached to the navigation controller. Paste the following code into the file:

    // File: ./presensesample/LoginViewController.swift
import UIKit
import Alamofire
import PusherSwift
import NotificationBannerSwift

class LoginViewController: UIViewController {
    var user: User? = nil
    var users: [User] = []

    @IBOutlet weak var nameTextField: UITextField!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        user = nil
        users = []

        navigationController?.isNavigationBarHidden = true
    }

    @IBAction func startChattingButtonPressed(_ sender: Any) {
        if nameTextField.text?.isEmpty == false, let name = nameTextField.text {
            registerUser(["name": name.lowercased()]) { successful in
                guard successful else {
                    return StatusBarNotificationBanner(title: "Failed to login.", style: .danger).show()
                }

                self.performSegue(withIdentifier: "showmain", sender: self)
            }
        }
    }

    func registerUser(_ params: [String : String], handler: @escaping(Bool) -> Void) {
        let url = "http://127.0.0.1:5000/users"

        Alamofire.request(url, method: .post, parameters: params)
            .validate()
            .responseJSON { resp in
                if resp.result.isSuccess,
                    let data = resp.result.value as? [String: Any],
                    let user = data["currentUser"] as? [String: String],
                    let users = data["users"] as? [String: [String: String]],
                    let id = user["id"], let name = user["name"]
                {
                    for (uid, user) in users {
                        if let name = user["name"], id != uid {
                            self.users.append(User(id: uid, name: name))
                        }
                    }

                    self.user = User(id: id, name: name)
                    self.nameTextField.text = nil

                    return handler(true)
                }

                handler(false)
        }
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let vc = segue.destination as? MainViewController {
            vc.viewControllers?.forEach {
                if let onlineVc = $0 as? OnlineTableViewController {
                    onlineVc.users = self.users
                    onlineVc.user = self.user
                }
            }
        }
    }
}

In the controller above, we have defined the users and user properties which will hold the available users and the current user when the user is logged in. We also have the nameTextField which is an @IBOutlet to the text field in the storyboard view controller, so make sure you connect the outlet if you hadn’t previously done so.

In the same controller, we have the startChattingButtonPressed method which is an @IBAction so make sure you connect it to the submit button in the storyboard view controller if you have not already done so. In this method, we call the registerUser method to register the user using the API. If the registration is successful, we direct the user to the showmain segue.

The segue between the login view controller and the tab bar controller should be set with an identifier of showmain.

In the registerUser method, we send the name to the API and receive a JSON response. We parse it to see if the registration was successful or not.

The final method in the class is the prepare method. This method is automatically called by iOS when a new segue is being loaded. We use this to preset some data to the view controller we are about to load.

Next, create a new file called MainViewController and set this as the custom class for the tab bar view controller. In the file, paste the following code:

    // File: ./presensesample/MainViewController.swift
import UIKit

class MainViewController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Who's Online"
        navigationItem.hidesBackButton = true
        navigationController?.isNavigationBarHidden = false

        // Logout button
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "Logout",
            style: .plain,
            target: self,
            action: #selector(logoutButtonPressed)
        )
    }

    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        navigationItem.title = item.title
    }

    @objc fileprivate func logoutButtonPressed() {
        viewControllers?.forEach {
            if let vc = $0 as? OnlineTableViewController {
                vc.users = []
                vc.pusher.disconnect()
            }
        }

        navigationController?.popViewController(animated: true)
    }
}

In the controller above, we have a few methods defined. The viewDidLoad method sets the title of the controller and other navigation controller specific things. We also define a Logout button in this method. The button will trigger the logoutButtonPressed method.

In the logoutButtonPressed method, we try to log the user out by resetting the users property in the view controller and also we disconnect the user from the Pusher connection.

Next, create a new controller class named ChatTableViewController. Set this class as the custom class for one of the tab bar controllers child controllers. Paste the following code into the file:

    // File: ./presensesample/ChatTableViewController.swift
import UIKit

class ChatTableViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
}

The controller above is just a base controller and we do not intend to add any chat logic to this controller.

Create a new controller class called OnlineTableViewController. Set this controller as the custom class for the second tab bar controller child controller. Paste the following code to the controller class:

    // File: ./presensesample/OnlineTableViewController.swift
import UIKit
import PusherSwift

struct User {
    let id: String
    var name: String
    var online: Bool = false

    init(id: String, name: String, online: Bool? = false) {
        self.id = id
        self.name = name
        self.online = online!
    }
}

class OnlineTableViewController: UITableViewController {

    var pusher: Pusher!
    var user: User? = nil
    var users: [User] = []

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "onlineuser", for: indexPath)
        let user = users[indexPath.row]

        cell.textLabel?.text = "\(user.name) \(user.online ? "[Online]" : "")"

        return cell
    }
}

In the code above, we first defined a User struct. We will use this to represent the user resource. We have already referenced this struct in previous controllers we created earlier.

Next, we defined the OnlineTableViewController class which is extends the UITableViewController class. In this class, we override the usual table view controller methods to provide the table with data.

You have to set the cell reuse identifier of this table to onlineuser in the storyboard.

Above we also defined some properties:

  • pusher - this will hold the Pusher SDK instance that we will use to subscribe to Pusher Channels.
  • users - this will hold an array of User structs.
  • user - this is the user struct of the current user.

Next, in the same class, add the following method:

    // File: ./presensesample/OnlineTableViewController.swift

// [...]

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.allowsSelection = false

    // Create the Pusher instance...
    pusher = Pusher(
        key: "PUSHER_APP_KEY",
        options: PusherClientOptions(
            authMethod: .endpoint(authEndpoint: "http://127.0.0.1:5000/pusher/auth/presence"),
            host: .cluster("PUSHER_APP_CLUSTER")
        )
    )

    // Subscribe to a presence channel...
    let channel = pusher.subscribeToPresenceChannel(
        channelName: "presence-chat",
        onMemberAdded: { member in
            if let info = member.userInfo as? [String: String], let name = info["name"] {
                if let index = self.users.firstIndex(where: { $0.id == member.userId }) {
                    let userModel = self.users[index]
                    self.users[index] = User(id: userModel.id, name: userModel.name, online: true)
                } else {
                    self.users.append(User(id: member.userId, name: name, online: true))
                }

                self.tableView.reloadData()
            }
        },
        onMemberRemoved: { member in
            if let index = self.users.firstIndex(where: { $0.id == member.userId }) {
                let userModel = self.users[index]
                self.users[index] = User(id: userModel.id, name: userModel.name, online: false)
                self.tableView.reloadData()
            }
        }
    )

    // Bind to the subscription succeeded event...
    channel.bind(eventName: "pusher:subscription_succeeded") { data in
        guard let deets = data as? [String: AnyObject],
            let presence = deets["presence"] as? [String: AnyObject],
            let ids = presence["ids"] as? NSArray else { return }

        for userid in ids {
            guard let uid = userid as? String else { return }

            if let index = self.users.firstIndex(where: { $0.id == uid }) {
                let userModel = self.users[index]
                self.users[index] = User(id: uid, name: userModel.name, online: true)
            }
        }

        self.tableView.reloadData()
    }

    // Connect to Pusher
    pusher.connect()
}

// [...]

In the viewDidLoad method above, we are doing several things. First, we instantiate the Pusher instance. In the options, we specify the authorize endpoint. We use the same URL as the backend we created earlier.

The next thing we do is subscribe to a presence channel called presence-chat. When working with presence channels, the channel name must be prefixed with presence-. The subscribeToPresenceChannel method has two callbacks that we can add logic to:

  • onMemberAdded - this event is called when a new user joins the presence-chat channel. In this callback, we check for the user that was added and mark them as online in the users array.
  • onMemberRemoved - this event is called when a user leaves the presence-chat channel. In this callback, we check for the user that left the channel and mark them as offline.

Next, we bind to the pusher:subscription_succeeded event. This event is called when a user successfully subscribes to updates on a channel. The callback on this event returns all the currently subscribed users. In the callback, we use this list of subscribed users to mark them online in the application.

Finally, we use the connect method on the pusher instance to connect to Pusher.


Allowing local connections on the iOS app

One last thing we need to do before we are done with the iOS application is allowing the application load data from arbitrary URLs. By default, iOS does not allow this, and it should not. However, since we are going to be testing locally, we need this turned on temporarily. Open the info.plist file and update it as seen below:

Now, our app is ready. You can run the application and you should see the online presence status of other users when they log in.


Conclusion

In this tutorial, we learned how to use presence channels in your iOS application using Pusher Channels.

The source code for the application created in this tutorial is available on GitHub.


Learn More

Learn Swift 4: From Beginner to Advanced

Top 10 Node.js Frameworks

Machine Learning In Node.js With TensorFlow.js

Express.js & Node.js Course for Beginners - Full Tutorial

How to Perform Web-Scraping using Node.js

Build a web scraper with Node

Getting started with Flutter

Android Studio for beginners

Building a mobile chat app with Nest.js and Ionic 4

Originally published by Neo Ighodaro at https://pusher.com/

Build an iOS Chat App with Swift

In this tutorial we show you how easy it is to build your own chat application for iOS using Swift and our new API for chat and messaging: https://getstream.io/chat/. Once you've completed this tutorial you'll have created a fully functioning chat app for the iPhone.