← Blog

Synced Realm on iOS with SwiftUI using Sign-in with Apple for Authentication

iOS
Realm
SwiftUI
published on:
📚  This article is part of a series on 
iOS
Realm
, and 
SwiftUI
:

Some apps store sensitive user information that requires authorized access. For example, an app may store a user’s first and last name which should not be accessed by any other user. As such, these apps need to integrate with authorized data storage. On iOS, two commonly employed technologies for authentication and authorized data storage are Sign-in with Apple and Realm, respectively.

Sign-in with Apple

Apple’s mechanism provides seamless authentication for users of third-party apps and websites on iOS. Using their existing Apple ID, users can sign up for an app or a website without verifying their email address or creating a new password.

As developers, Apple allows us to implement Sign-in with Apple through the Authentication Services framework. In SwiftUI, we simply combine this framework with a SignInWithAppleButton to create the corresponding view. Everything else is conveniently handled by Apple and SwiftUI.

Realm

Realm is an ACID-compliant mobile database by MongoDB that features mobile-to-cloud synchronization. The storage technology is backed by MongoDB Atlas and is available for programming languages and frameworks including Swift and SwiftUI. Complementing the declarative nature of SwiftUI, Realm enables the developer to subscribe to data changes from the UI for a reactive user experience.

Integration

To be able to use the two technologies in our iOS app, we start by integrating Realm. For the app.swift, we initialize the RealmSwift.App using the Realm’s ID (which can be retrieved from the corresponding MongoDB project) for later use throughout our app:

App.swift

_13
import RealmSwift
_13
import SwiftUI
_13
_13
@main
_13
struct App: SwiftUI.App {
_13
private let app: RealmSwift.App? = RealmSwift.App(id: realmAppID)
_13
_13
var body: some Scene {
_13
WindowGroup {
_13
WelcomeView(app: app!)
_13
}
_13
}
_13
}

Passing RealmSwift.App’s instance to WelcomeView where we delegate to SplashScreen or LoginView based on the user’s login status showing the splash screen or allowing the user to log in, respectively:

WelcomeView.swift

_14
import RealmSwift
_14
import SwiftUI
_14
_14
struct WelcomeView: View {
_14
@ObservedObject var app: RealmSwift.App
_14
_14
var body: some View {
_14
if let user = app.currentUser {
_14
SplashScreen(app: app).environment(\.partitionValue, user.id)
_14
} else {
_14
LoginView(app: app)
_14
}
_14
}
_14
}

In particular, LoginView contains the logic for Sign-in with Apple, and SplashScreen loads the Realm. As mentioned at the beginning, using Sign-in with Apple in SwiftUI is fairly straightforward:

LoginView.swift

_58
import AuthenticationServices
_58
import RealmSwift
_58
import SwiftUI
_58
_58
struct LoginView: View {
_58
@ObservedObject var app: RealmSwift.App
_58
_58
@Environment(\.colorScheme) private var colorScheme
_58
@State private var isLoggingIn: Bool = false
_58
_58
var body: some View {
_58
VStack {
_58
SignInWithAppleButton(.signIn, onRequest: { request in
_58
isLoggingIn = true
_58
_58
request.requestedScopes = [.email]
_58
}, onCompletion: { result in
_58
switch result {
_58
case .success(let authResults):
_58
guard let credentials = authResults.credential as? ASAuthorizationAppleIDCredential, let identityToken = credentials.identityToken, let identityTokenString = String(data: identityToken, encoding: .utf8) else { return }
_58
_58
Database.shared.setAppleIdentityToken(appleIdentityToken: identityTokenString)
_58
Log.debug("Successfully signed in with Apple.")
_58
_58
login()
_58
case .failure(let error):
_58
isLoggingIn = false
_58
_58
Log.error("Sign in with Apple failed: \(error.localizedDescription, privacy: .public)")
_58
}
_58
})
_58
.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
_58
.disabled(isLoggingIn)
_58
}
_58
}
_58
_58
private func login() -> Void {
_58
let appleIdentityToken = Database.shared.getAppleIdentityToken()
_58
var credentials: Credentials
_58
_58
if appleIdentityToken != nil {
_58
credentials = Credentials.apple(idToken: appleIdentityToken!)
_58
} else {
_58
credentials = Credentials.anonymous
_58
}
_58
_58
app.login(credentials: credentials) { result in
_58
switch result {
_58
case .failure(let error):
_58
Log.error("Login to Realm failed: \(error.localizedDescription, privacy: .public)")
_58
case .success(let user):
_58
Log.debug("Successfully logged into Realm as \(user.id, privacy: .private).")
_58
}
_58
_58
isLoggingIn = false
_58
}
_58
}
_58
}

While Sign-in with Apple and Realm are two separate technologies, the latter easily allows to integrate with the former. Simply providing the identity token of the Apple ID in question to Realm is enough to carry through the authentication as we can observe in login(). For SplashScreen, the implementation is similarly straightforward:

SplashScreen.swift

_34
import AuthenticationServices
_34
import RealmSwift
_34
import RevenueCat
_34
import SwiftUI
_34
_34
struct SplashScreen: View {
_34
@AsyncOpen(appId: realmAppID, partitionValue: "", timeout: 5000) var asyncOpen
_34
_34
@ObservedObject var app: RealmSwift.App
_34
_34
@State private var isLoadingIndicatorVisible = false
_34
_34
var body: some View {
_34
switch asyncOpen {
_34
case .connecting, .error, .progress, .waitingForUser:
_34
VStack {
_34
Spacer()
_34
HStack(alignment: .center, spacing: 25) {
_34
Image(uiImage: Bundle.main.appIcon!).cornerRadius(15)
_34
Text(appName).bold().font(.largeTitle).foregroundColor(.accentColor)
_34
}
_34
ProgressView().opacity(isLoadingIndicatorVisible ? 1 : 0)
_34
Spacer()
_34
}
_34
.onAppear {
_34
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
_34
isLoadingIndicatorVisible = true
_34
}
_34
}
_34
case .open(let realm):
_34
MainView().environment(\.realm, realm)
_34
}
_34
}
_34
}

At this point in the implementation, we no longer have to take care of Sign-in with Apple since the user is already signed in. Likewise, the user is already logged into the Realm such that we only need to connect to the Realm for synchronization. Luckily, the Swift SDK provides a property wrapper, i.e., @AsyncOpen, that abstracts all the connection logic. We do not need to provide a partitionValue since the partition value is already supplied as an environmental variable in WelcomeView.swift. Finally, utilize the resulting property asyncOpen to react to the connection state where we show the MainView() after the Realm is opened.

The implementation that we have outlined above includes asynchronous partition-based synchronization with Atlas App Services. We can utilize the synchronized Realm to provide cross-device synchronization as a feature or develop a cross-platform application (there is even a Web SDK for Realm!) with a unified backend. If we were to use Realm on iOS as a solution for local storage only, we would leave out the initialization of RealmSwift.App, remove login(), and remove @AsyncOpen.