Skip to content

Commit bd3d893

Browse files
chore: update example app on how to embed SwiftUI auth in UIKit
1 parent c2f4335 commit bd3d893

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,30 @@ struct ContentView: View {
118118
}
119119
}
120120
.tint(Color(.label))
121+
NavigationLink {
122+
UIKitEmbeddingExample()
123+
.navigationTitle("Embedding in UIKit")
124+
} label: {
125+
VStack(alignment: .leading, spacing: 16) {
126+
Text("UIKit embedding example")
127+
.font(.headline)
128+
.fontWeight(.bold)
129+
Text("How to host FirebaseSwiftUI inside a UIKit view controller")
130+
Text(
131+
"• Inline authentication surface\n• UIHostingController inside UIKit\n• No auth sheet toggle required"
132+
)
133+
.font(.caption)
134+
.foregroundColor(.secondary)
135+
}
136+
.multilineTextAlignment(.leading)
137+
.padding()
138+
.frame(maxWidth: .infinity, alignment: .leading)
139+
.background {
140+
RoundedRectangle(cornerRadius: 16)
141+
.fill(Color(UIColor.secondarySystemBackground))
142+
}
143+
}
144+
.tint(Color(.label))
121145
}
122146
.padding()
123147
.navigationTitle("FirebaseUI Demo")
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseAppleSwiftUI
16+
import FirebaseAuth
17+
import FirebaseAuthSwiftUI
18+
import FirebaseGoogleSwiftUI
19+
import SwiftUI
20+
import UIKit
21+
22+
struct UIKitEmbeddingExample: View {
23+
private let authService: AuthService
24+
25+
init() {
26+
authService = AuthService()
27+
.withAppleSignIn()
28+
.withGoogleSignIn()
29+
.withEmailSignIn()
30+
}
31+
32+
var body: some View {
33+
ScrollView {
34+
VStack(alignment: .leading, spacing: 20) {
35+
Text("Embed FirebaseSwiftUI inside any UIKit container")
36+
.font(.title2)
37+
.fontWeight(.bold)
38+
39+
Text(
40+
"This example creates a UIKit view controller, mounts a SwiftUI screen with UIHostingController, and uses AuthPickerView for the unauthenticated flow."
41+
)
42+
.foregroundStyle(.secondary)
43+
44+
FirebaseAuthUIKitContainer()
45+
.frame(minHeight: 620)
46+
}
47+
.padding()
48+
}
49+
.background(Color(UIColor.systemGroupedBackground))
50+
.environment(authService)
51+
}
52+
}
53+
54+
private struct FirebaseAuthUIKitContainer: UIViewControllerRepresentable {
55+
@Environment(AuthService.self) private var authService
56+
57+
func makeUIViewController(context: Context) -> EmbeddedAuthViewController {
58+
let viewController = EmbeddedAuthViewController()
59+
viewController.update(authService: authService)
60+
return viewController
61+
}
62+
63+
func updateUIViewController(_ uiViewController: EmbeddedAuthViewController, context: Context) {
64+
uiViewController.update(authService: authService)
65+
}
66+
}
67+
68+
@MainActor
69+
private final class EmbeddedAuthViewController: UIViewController {
70+
private var hostingController: UIHostingController<AnyView>?
71+
72+
override func viewDidLoad() {
73+
super.viewDidLoad()
74+
view.backgroundColor = .clear
75+
}
76+
77+
func update(authService: AuthService) {
78+
let rootView = AnyView(EmbeddedAuthView().environment(authService))
79+
80+
if let hostingController {
81+
hostingController.rootView = rootView
82+
return
83+
}
84+
85+
let hostingController = UIHostingController(rootView: rootView)
86+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
87+
hostingController.view.backgroundColor = .clear
88+
89+
addChild(hostingController)
90+
view.addSubview(hostingController.view)
91+
NSLayoutConstraint.activate([
92+
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
93+
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
94+
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
95+
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
96+
])
97+
hostingController.didMove(toParent: self)
98+
99+
self.hostingController = hostingController
100+
}
101+
}
102+
103+
private struct EmbeddedAuthView: View {
104+
@Environment(AuthService.self) private var authService
105+
106+
var body: some View {
107+
AuthPickerView {
108+
authenticatedApp
109+
}
110+
.onChange(of: authService.authenticationState) { _, newValue in
111+
if newValue != .authenticating {
112+
authService.isPresented = newValue == .unauthenticated
113+
}
114+
}
115+
}
116+
117+
private var authenticatedApp: some View {
118+
VStack(spacing: 24) {
119+
VStack(alignment: .leading, spacing: 8) {
120+
Text("UIKit-hosted auth flow")
121+
.font(.headline)
122+
.fontWeight(.semibold)
123+
124+
Text("This SwiftUI view is rendered by a UIKit UIViewController.")
125+
.font(.subheadline)
126+
.foregroundStyle(.secondary)
127+
}
128+
.frame(maxWidth: .infinity, alignment: .leading)
129+
130+
if authService.authenticationState == .unauthenticated {
131+
VStack(spacing: 16) {
132+
Text("Not Authenticated")
133+
.font(.title3)
134+
.fontWeight(.semibold)
135+
136+
Text(
137+
"AuthPickerView handles the sign-in UI. This UIKit-hosted screen just decides what to show before and after authentication."
138+
)
139+
.multilineTextAlignment(.center)
140+
.foregroundStyle(.secondary)
141+
142+
Button("Authenticate") {
143+
authService.isPresented = true
144+
}
145+
.buttonStyle(.borderedProminent)
146+
}
147+
.frame(maxWidth: .infinity)
148+
.padding(.vertical, 32)
149+
} else {
150+
VStack(spacing: 20) {
151+
Image(systemName: "person.crop.circle.badge.checkmark")
152+
.font(.system(size: 56))
153+
.foregroundStyle(.green)
154+
155+
Text(authService.currentUser?.email ?? "Signed in")
156+
.font(.title3)
157+
.fontWeight(.semibold)
158+
159+
Text("Firebase Auth is now authenticated. From here, UIKit or SwiftUI can take over the rest of your app flow.")
160+
.multilineTextAlignment(.center)
161+
.foregroundStyle(.secondary)
162+
163+
Button("Manage Account") {
164+
authService.isPresented = true
165+
}
166+
.buttonStyle(.bordered)
167+
168+
Button("Sign Out") {
169+
Task {
170+
try? await authService.signOut()
171+
}
172+
}
173+
.buttonStyle(.borderedProminent)
174+
}
175+
.frame(maxWidth: .infinity)
176+
.padding(.top, 24)
177+
}
178+
}
179+
.padding(24)
180+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
181+
.background(
182+
RoundedRectangle(cornerRadius: 24, style: .continuous)
183+
.fill(Color(UIColor.secondarySystemGroupedBackground))
184+
)
185+
}
186+
187+
}

0 commit comments

Comments
 (0)