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:
npm install -g @ionic/cli
Next, create a new Ionic project:
ionic start LoginDemo blank --type=angular
Navigate to the new project directory:
cd LoginDemo
Now, let's create a simple login form. Open src/app/home/home.page.html
and replace its content with the following:
<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:
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:
npm install @chatty-webviews/js
Then open the AppComponent and attach the messaging service:
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:
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:
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.
Here's the code for our main view, ContentView
:
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
:
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:
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:
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:
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:
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.
Top comments (0)