Skip to content

Commit 39a5b7a

Browse files
Merge pull request #1329 from firebase/test-uikit
2 parents ac28677 + b6ed109 commit 39a5b7a

3 files changed

Lines changed: 204 additions & 5 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@ struct ContentView: View {
119119
}
120120
}
121121
.tint(Color(.label))
122+
NavigationLink {
123+
UIKitEmbeddingExample()
124+
.navigationTitle("Embedding in UIKit")
125+
} label: {
126+
VStack(alignment: .leading, spacing: 16) {
127+
Text("UIKit embedding example")
128+
.font(.headline)
129+
.fontWeight(.bold)
130+
Text("How to host FirebaseSwiftUI inside a UIKit view controller")
131+
Text(
132+
"• Inline authentication surface\n• UIHostingController inside UIKit\n• No auth sheet toggle required"
133+
)
134+
.font(.caption)
135+
.foregroundColor(.secondary)
136+
}
137+
.multilineTextAlignment(.leading)
138+
.padding()
139+
.frame(maxWidth: .infinity, alignment: .leading)
140+
.background {
141+
RoundedRectangle(cornerRadius: 16)
142+
.fill(Color(UIColor.secondarySystemBackground))
143+
}
144+
}
145+
.tint(Color(.label))
122146
}
123147
.padding()
124148
.navigationTitle("FirebaseUI Demo")

samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Examples/AuthPickerViewExample.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,5 @@ struct AuthPickerViewExample: View {
5555
}
5656
}
5757
}
58-
.onChange(of: authService.authenticationState) { _, newValue in
59-
if newValue != .authenticating {
60-
authService.isPresented = newValue == .unauthenticated
61-
}
62-
}
6358
}
6459
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
EmbeddedAuthViewController()
59+
}
60+
61+
func updateUIViewController(_ uiViewController: EmbeddedAuthViewController, context: Context) {
62+
uiViewController.update(authService: authService)
63+
}
64+
}
65+
66+
@MainActor
67+
private final class EmbeddedAuthViewController: UIViewController {
68+
private var hostingController: UIHostingController<AnyView>?
69+
70+
override func viewDidLoad() {
71+
super.viewDidLoad()
72+
view.backgroundColor = .clear
73+
}
74+
75+
func update(authService: AuthService) {
76+
let rootView = AnyView(EmbeddedAuthView().environment(authService))
77+
78+
if let hostingController {
79+
hostingController.rootView = rootView
80+
return
81+
}
82+
83+
let hostingController = UIHostingController(rootView: rootView)
84+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
85+
hostingController.view.backgroundColor = .clear
86+
87+
addChild(hostingController)
88+
view.addSubview(hostingController.view)
89+
NSLayoutConstraint.activate([
90+
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
91+
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
92+
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
93+
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
94+
])
95+
hostingController.didMove(toParent: self)
96+
97+
self.hostingController = hostingController
98+
}
99+
}
100+
101+
private struct EmbeddedAuthView: View {
102+
@Environment(AuthService.self) private var authService
103+
104+
var body: some View {
105+
AuthPickerView {
106+
authenticatedApp
107+
}
108+
}
109+
110+
private var authenticatedApp: some View {
111+
VStack(spacing: 24) {
112+
VStack(alignment: .leading, spacing: 8) {
113+
Text("UIKit-hosted auth flow")
114+
.font(.headline)
115+
.fontWeight(.semibold)
116+
117+
Text("This SwiftUI view is rendered by a UIKit UIViewController.")
118+
.font(.subheadline)
119+
.foregroundStyle(.secondary)
120+
}
121+
.frame(maxWidth: .infinity, alignment: .leading)
122+
123+
if authService.authenticationState == .unauthenticated {
124+
VStack(spacing: 16) {
125+
Text("Not Authenticated")
126+
.font(.title3)
127+
.fontWeight(.semibold)
128+
129+
Text(
130+
"AuthPickerView handles the sign-in UI. This UIKit-hosted screen just decides what to show before and after authentication."
131+
)
132+
.multilineTextAlignment(.center)
133+
.foregroundStyle(.secondary)
134+
135+
Button("Authenticate") {
136+
authService.isPresented = true
137+
}
138+
.buttonStyle(.borderedProminent)
139+
}
140+
.frame(maxWidth: .infinity)
141+
.padding(.vertical, 32)
142+
} else {
143+
VStack(spacing: 20) {
144+
Image(systemName: "person.crop.circle.badge.checkmark")
145+
.font(.system(size: 56))
146+
.foregroundStyle(.green)
147+
148+
Text(authService.currentUser?.email ?? "Signed in")
149+
.font(.title3)
150+
.fontWeight(.semibold)
151+
152+
Text("Firebase Auth is now authenticated. From here, UIKit or SwiftUI can take over the rest of your app flow.")
153+
.multilineTextAlignment(.center)
154+
.foregroundStyle(.secondary)
155+
156+
Button("Manage Account") {
157+
authService.isPresented = true
158+
}
159+
.buttonStyle(.bordered)
160+
161+
Button("Sign Out") {
162+
Task {
163+
try? await authService.signOut()
164+
}
165+
}
166+
.buttonStyle(.borderedProminent)
167+
}
168+
.frame(maxWidth: .infinity)
169+
.padding(.top, 24)
170+
}
171+
}
172+
.padding(24)
173+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
174+
.background(
175+
RoundedRectangle(cornerRadius: 24, style: .continuous)
176+
.fill(Color(UIColor.secondarySystemGroupedBackground))
177+
)
178+
}
179+
180+
}

0 commit comments

Comments
 (0)