diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index 298b859..61b720e 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -20,7 +20,7 @@
-
+
@@ -34,6 +34,9 @@
+
+
+
diff --git a/Sources/Showcase/BiometricAuthenticationPlayground.swift b/Sources/Showcase/BiometricAuthenticationPlayground.swift
new file mode 100644
index 0000000..5971161
--- /dev/null
+++ b/Sources/Showcase/BiometricAuthenticationPlayground.swift
@@ -0,0 +1,87 @@
+// Copyright 2023–2026 Skip
+import SwiftUI
+import SkipKit
+
+/// This component uses the `SkipKit` module from https://source.skip.tools/skip-kit
+struct BiometricAuthenticationPlayground: View {
+ @State var authenticationType = BiometricAuthentication.authenticationType
+ @State var result = "Not authenticated"
+ @State var authenticationAttempt = 0
+
+ private var canAuthenticate: Bool { BiometricAuthentication.canAuthenticate }
+
+ private var resultColor: Color {
+ switch self.result {
+ case "Authenticated": .green
+ case "Cancelled": .orange
+ case "Failed", "Unavailable": .red
+ default:
+ .secondary
+ }
+ }
+
+ var body: some View {
+ List {
+ Section("Availability") {
+ LabeledContent("Type",
+ value: {
+ switch self.authenticationType {
+ case .fingerprint: "Fingerprint"
+ case .facialRecognition: "Facial Recognition"
+ case .unspecified: "Unspecified Biometric"
+ case .none: "None"
+ }
+ }()
+ )
+ LabeledContent("Can Authenticate") {
+ Text(self.canAuthenticate ? "Yes" : "No")
+ .foregroundStyle(self.canAuthenticate ? Color.green : Color.red)
+ }
+ }
+
+ if self.canAuthenticate {
+ Section("Authentication") {
+ Button("Authenticate") { self.authenticate() }
+
+ Text(self.result)
+ .foregroundStyle(self.resultColor)
+ }
+ }
+ }
+ .onAppear { self.refresh() }
+ .onDisappear { self.cancelAuthentication() }
+ .toolbar {
+ PlaygroundSourceLink(file: "BiometricAuthenticationPlayground.swift")
+ }
+ }
+
+ private func authenticate() {
+ self.authenticationAttempt += 1
+ let authenticationAttempt = self.authenticationAttempt
+ self.result = "Authenticating..."
+
+ BiometricAuthentication.authenticate(
+ localizedReason: "Authenticate with biometrics"
+ ) { authenticationResult in
+ guard authenticationAttempt == self.authenticationAttempt else {
+ return
+ }
+
+ self.result = switch authenticationResult {
+ case .success: "Authenticated"
+ case .cancelled: "Cancelled"
+ case .failed: "Failed"
+ case .unavailable: "Unavailable"
+ }
+ self.refresh()
+ }
+ }
+
+ private func refresh() {
+ self.authenticationType = BiometricAuthentication.authenticationType
+ }
+
+ private func cancelAuthentication() {
+ self.authenticationAttempt += 1
+ }
+}
diff --git a/Sources/Showcase/PlaygroundListView.swift b/Sources/Showcase/PlaygroundListView.swift
index b3d10b7..89e6c15 100644
--- a/Sources/Showcase/PlaygroundListView.swift
+++ b/Sources/Showcase/PlaygroundListView.swift
@@ -8,6 +8,7 @@ enum PlaygroundType: CaseIterable, View {
case animation
case audio
case background
+ case biometricAuthentication
case blendMode
case blur
case border
@@ -113,6 +114,8 @@ enum PlaygroundType: CaseIterable, View {
return LocalizedStringResource("Audio", comment: "Title of Audio playground")
case .background:
return LocalizedStringResource("Background", comment: "Title of Background playground")
+ case .biometricAuthentication:
+ return LocalizedStringResource("Biometric Authentication", comment: "Title of Biometric Authentication playground")
case .blendMode:
return LocalizedStringResource("BlendMode", comment: "Title of BlendMode playground")
case .blur:
@@ -316,6 +319,8 @@ enum PlaygroundType: CaseIterable, View {
#endif
case .background:
BackgroundPlayground()
+ case .biometricAuthentication:
+ BiometricAuthenticationPlayground()
case .blendMode:
BlendModePlayground()
case .blur:
diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings
index f9cf7c8..0224a8a 100644
--- a/Sources/Showcase/Resources/Localizable.xcstrings
+++ b/Sources/Showcase/Resources/Localizable.xcstrings
@@ -1002,12 +1002,30 @@
},
"AsyncImage" : {
+ },
+ "Authenticate" : {
+
+ },
+ "Authenticate with biometrics" : {
+
+ },
+ "Authenticated" : {
+
+ },
+ "Authenticating..." : {
+
+ },
+ "Authentication" : {
+
},
"Audio" : {
"comment" : "Title of Audio playground"
},
"AudioPlayground is Lite-only until ported to SkipAV" : {
+ },
+ "Availability" : {
+
},
"B" : {
@@ -1077,6 +1095,9 @@
},
"Binding View" : {
+ },
+ "Biometric Authentication" : {
+ "comment" : "Title of Biometric Authentication playground"
},
"BipBop" : {
@@ -1182,6 +1203,12 @@
},
"Cancel" : {
+ },
+ "Cancelled" : {
+
+ },
+ "Can Authenticate" : {
+
},
"Capsule" : {
@@ -1673,6 +1700,12 @@
},
"F2" : {
+ },
+ "Facial Recognition" : {
+
+ },
+ "Failed" : {
+
},
"Favorite" : {
@@ -1700,6 +1733,9 @@
},
"fill(.red)\n .stroke(.green, lineWidth: 10)" : {
+ },
+ "Fingerprint" : {
+
},
"First" : {
@@ -2647,6 +2683,9 @@
},
"NO" : {
+ },
+ "No" : {
+
},
"No badge (count 0)" : {
@@ -2665,6 +2704,12 @@
},
"Normal" : {
+ },
+ "None" : {
+
+ },
+ "Not authenticated" : {
+
},
"Not supported on macOS" : {
@@ -4225,6 +4270,9 @@
},
"Title Only" : {
+ },
+ "Type" : {
+
},
"To:" : {
@@ -4306,6 +4354,9 @@
},
"Unavailable" : {
+ },
+ "Unspecified Biometric" : {
+
},
"Underline" : {
@@ -4719,6 +4770,9 @@
},
"Yellow" : {
+ },
+ "Yes" : {
+
},
"YES" : {
@@ -4734,4 +4788,4 @@
}
},
"version" : "1.0"
-}
\ No newline at end of file
+}