How to Implement a Cross-Platform Login Screen with Angular within a WKWebView
Tags: beginners, ios, mobile, swift
Published: 2023-07-26 13:23:31



## TLDR;
You can develop cross-platform components for your mobile apps using webviews without necessarily having to rely on hybrid frameworks.

## Introduction

In many cases, especially when you're building a native app, you might want to have a cross-platform authentication flow. This could be because you want to maintain the native performance and feel for the rest of your app, but need a quick and efficient way to handle authentication. 

You could go for a full-fledged hybrid app framework like Ionic or React Native, but that might be overkill if you're only looking to make a few parts of your app cross-platform. Plus, transitioning to a hybrid app framework comes with its own set of challenges, like learning a new programming language or dealing with the nuances of a different development environment. Or you might just have a web developer to help you with few screens...

This is where webviews come in. By creating a login screen using a webview, you can write the code once with a framework you (or whoever is writing the component) are already familiar with (Angular, React, Vue or whatever) and use it across multiple platforms. It's a lightweight solution that doesn't require a full hybrid app framework, making it perfect for simpler features like a login screen.

And to make things even easier, we're going to use an open-source library I created called **ChattyWebviews**. It's designed to facilitate communication between webviews and native classes, making it a breeze to integrate webview-based features into your native apps. If you find the article useful, consider giving it a star, so I can continue working on it - https://github.com/ChattyWebviews/ChattyWebviews.



## **Creating the Webview Login Screen**

For the purpose of this tutorial, we'll use Ionic to create our login screen. Ionic is a popular framework for building cross-platform applications using web technologies. It's a great choice for our login screen because it allows us to write our code with javascript and have the native iOS or Android look.

First, let's install Ionic if you haven't done so already:

```bash
npm install -g @ionic/cli
```

Next, create a new Ionic project:

```bash
ionic start LoginDemo blank --type=angular
```

Navigate to the new project directory:

```bash
cd LoginDemo
```

Now, let's create a simple login form. Open `src/app/home/home.page.html` and replace its content with the following:

```html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Login
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <form (ngSubmit)="onSubmit()" #loginForm="ngForm">
    <ion-item>
      <ion-label position="floating">Username</ion-label>
      <ion-input type="text" ngModel name="username" required></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="floating">Password</ion-label>
      <ion-input type="password" ngModel name="password" required></ion-input>
    </ion-item>
    <ion-button expand="full" type="submit" [disabled]="!loginForm.valid">Login</ion-button>
  </form>
</ion-content>
```

In `src/app/home/home.page.ts` add the `onSubmit()` method:

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor() {}

  onSubmit() {
      //here we will send the message to the native class
  }

}
```

This creates a simple login form with fields for username and password. The `(ngSubmit)` directive is an Angular feature that allows us to run a function when the form is submitted. 

In the next section, we'll look at how to handle the form submission and communicate with our native app using ChattyWebviews.

## **Handling Form Submission with ChattyWebviews**

Now that we have our login form, we need to handle the form submission. When the user submits the form, we want to send the username and password to our native app. This is where ChattyWebviews comes in.

First, install ChattyWebviews in your Ionic project:

```bash
npm install @chatty-webviews/js
```

Then open the AppComponent and attach the messaging service:

```typescript
import { Component } from '@angular/core';
import { attach } from '@chatty-webviews/js';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {

    constructor() {
        attach();  
    }
}
```

Next, open `src/app/home/home.page.ts` and replace its content with the following:

```typescript
import { Component, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { sendMessage } from '@chatty-webviews/js';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  @ViewChild('loginForm') loginForm!: NgForm;

  constructor() {}

  onSubmit() {
    if (this.loginForm.valid) {
      const { username, password } = this.loginForm.value;
      sendMessage('login', { username, password });
    }
  }
}
```

In this code, we're importing `sendMessage` from `@chatty-webviews/js`. We're also defining an `onSubmit` method that gets called when the form is submitted. In this method, we're sending a message to our native app with the username and password.

## **Setting Up the Native App**

We need to set up our native app to display the login screen and handle messages from the webview. We'll be using SwiftUI for this. 

Before writing the code, we need to do few things. First, add the ChattyWebviews' SPM package by going to File->Add Packages and pasting the repository url:

```
https://github.com/ChattyWebviews/ChattyWebviews
```

You will also need to build the Ionic web app package by running `ionic build`. When the build is done, drag the `www` folder inside the Xcode project:


![Drag the www folder in the Xcode project tree](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5q0zmvt1pyhl6zklx17r.png)

**Note:** It's important to select `Create folder references`. If you want the package to be automatically updated after every build, leave `Copy items if needed` unchecked.


![Select Create folder references](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6vv2mf4u6wiqm91sq11.png) 

Here's the code for our main view, `ContentView`:

```swift
struct ContentView: View {
    @State private var showingSheet = false
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Button("Login") {
                        showingSheet.toggle()
                    }
                    .sheet(isPresented: $showingSheet) {
                        SheetView()
                    }
        }
        .padding()
    }
    
    init(showingSheet: Bool = false) {
        self.showingSheet = showingSheet
        self.setupModules()
    }
    
    func setupModules() {

        let module = CWModule(name: "www", location: .Resources)
        WebViewFactory.initModules(modules: [module])
    }
}
```

In this code, we're creating a SwiftUI view with a button that displays a login screen when clicked. The login screen is a `SheetView`, which we'll define next.

## **Creating the Webview Login Screen**

Next, we'll create the `SheetView` that displays the login screen. This view is a `UIViewControllerRepresentable`, which means it's a wrapper for a UIKit view controller that can be used in SwiftUI.

Here's the code for `SheetView`:

```swift
struct SheetView: UIViewControllerRepresentable, CWMessageDelegate {
    typealias UIViewControllerType = CWViewController
    @Environment(\.dismiss) var dismiss
    
    func controller(_ controller: ChattyWebviews.CWViewController, didReceive message: ChattyWebviews.CWMessage) {
        print(message)
        dismiss()
        
    }
    
    func makeUIViewController(context: Context) -> ChattyWebviews.CWViewController {
        let module = WebViewFactory.getModules()[0]
        let scheduleVC = WebViewFactory.createWebview(from: module, path: nil)
        scheduleVC?.messageDelegate = self
        return scheduleVC!
    }
}
```

In this code, we're creating a `CWViewController` that displays the login screen. We're also setting up a message delegate to handle messages from the webview. When a message is received, we print it and dismiss the login screen.

## **Handling Messages in the Native App**

Finally, we need to handle the messages that we receive from the webview. This involves setting up a `CWMessageDelegate` in our native app.

Here's the code for the message delegate:

```swift
extension SheetView: CWMessageDelegate {
    
    func controller(_ controller: ChattyWebviews.CWViewController, didReceive message: ChattyWebviews.CWMessage) {
        print(message)
        dismiss()
        
    }
}
```

In this code, we're defining a method that gets called when a message is received from the webview. We're printing the message and dismissing the login screen.

## Fetching Login Credentials in Native Code


In our `SheetView` class, we have already set up a `CWMessageDelegate` to handle messages from the webview. When a message is received, the `controller(_:didReceive:)` method is called. This is where we can fetch the login credentials.

Here's how we can modify this method to fetch the login credentials:

```swift
func controller(_ controller: ChattyWebviews.CWViewController, didReceive message: ChattyWebviews.CWMessage) {
    if message.topic == "login" {
        let credentials = message.body as? [String: String]
        let username = credentials?["username"]
        let password = credentials?["password"]
        print("Username: \(username ?? ""), Password: \(password ?? "")")
    }
    dismiss()
}
```

In this code, we're checking if the message topic is "login". If it is, we're fetching the username and password from the message body and printing them, then dismissing the modal:


![Demo gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5z359xvikcpvs4o621d.gif)



## Handling Login in Native Code

Now that we have the login credentials, we can handle the login in our native code. This could involve validating the credentials, fetching user data from a server, etc.

Here's an example of how you might handle the login:

```swift
func handleLogin(username: String, password: String) {
    // Validate the credentials
    guard validateCredentials(username: username, password: password) else {
        print("Invalid credentials")
        return
    }
    
    // Fetch user data from server
    fetchUserData(username: username) { userData in
        // Handle user data
    }
}
```

In this code, we're validating the credentials and fetching user data from a server. The `validateCredentials(username:password:)` and `fetchUserData(username:completion:)` methods are not defined here, as their implementation would depend on your specific use case.

## Conclusion

With this setup, you can create a simple login screen using a webview and handle the login in your native code. This approach is quick and easy, and it doesn't require a full hybrid app framework like Capacitor. It also allows you to reuse your web code across different platforms, which can save you a lot of time and effort.