liman.io

liman.io

The logos of Sign-in with Apple and realm by MongoDB

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

RealmSwiftUI

published on 2022-06-11 (last edited on 2022-11-19)

📚  This article is part of a series on Realm and SwiftUI:
  • Tackling `AttributeGraph precondition failure: setting value during update` using Realm in SwiftUI
  • Updating Schema of Synced Realm and iOS App in Production
  • Open Third-Party URLs in a SwiftUI App Using a Share Extension
  • Infinite Scrolling List for Paginated Results from GraphQL with SwiftUI
  • Synced Realm on iOS with SwiftUI using Sign-in with Apple for Authentication

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 aSignInWithAppleButton 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 theRealmSwift.App using the Realm’s ID (which can be retrieved from the corresponding MongoDB project) for later use throughout our app:

App.swift
import RealmSwift
import SwiftUI

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

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
import RealmSwift
import SwiftUI

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

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
import AuthenticationServices
import RealmSwift
import SwiftUI

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

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 inlogin(). For SplashScreen, the implementation is similarly straightforward:

SplashScreen.swift
import AuthenticationServices
import RealmSwift
import RevenueCat
import SwiftUI

struct SplashScreen: View {
    @AsyncOpen(appId: realmAppID, partitionValue: "", timeout: 5000) var asyncOpen
    
    @ObservedObject var app: RealmSwift.App
    
    @State private var isLoadingIndicatorVisible = false

    var body: some View {
        switch asyncOpen {
        case .connecting, .error, .progress, .waitingForUser:
            VStack {
                Spacer()
                HStack(alignment: .center, spacing: 25) {
                    Image(uiImage: Bundle.main.appIcon!).cornerRadius(15)
                    Text(appName).bold().font(.largeTitle).foregroundColor(.accentColor)
                }
                ProgressView().opacity(isLoadingIndicatorVisible ? 1 : 0)
                Spacer()
            }
            .onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    isLoadingIndicatorVisible = true
                }
            }
        case .open(let realm):
            MainView().environment(\.realm, realm)
        }
    }
}

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 of the connection logic. We do not need to provide apartitionValue 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 ofRealmSwift.App, remove login(), and remove @AsyncOpen.

Daniel Fürst © 2023 • Legal Notice • Privacy Policy