1. はじめに
初めまして、たなぼうと申します。
記事を書いている現在は大学生であり、独学でiosアプリを開発をしています。
(※独学のためコードが美しくない可能性高)
今回のテーマは
・iPhoneのロック画面解除のような機能をアプリに搭載したい!
どうやれば良いのか少し悩んだので、共有する記事になります。
全コードは以下に載せてます。
https://github.com/RyotaLab/faceID.git
目次
1.完成図
2.手順紹介
3.実装(コード付き)
4.おわりに
1. 完成図
解除方法は
- 4つのボタンで解除
- FaceIDで解除
toggleボタンをtrueでパスコード作成、falseでパスコード解除になります。
デザインは私の趣味なので、自分で好きな色や形に変更してね!
2. 手順紹介
- faceIDを行うクラスや関数を作成
- 初期画面の分岐作成
- パスコード作成画面を実装
- ロック画面を実装
faceIDに関しては他の有識者の記事の方が分かりやすいため、解説は省きます。
正直、classをコピペすれば問題なく動くので、「より理解したい方」のみ以下の記事がおすすめです。
LocalAuthenticationを使用して生体認証を試してみた
今回はロック画面をどうやって作成するかをメインに解説します。
3. 実装
それでは始めていきましょう!
(紹介するコードをコピペしていけば、完成図のようなアプリが作れます)
3-1. faceIDのクラス作成
生体認証を行うために、info.plistに「Privacy - Face ID Usage Description」を追加する必要があります。Valueには生体認証を使う理由を書きましょう。
次にファイルを作成して、faceIDを行うclassを記述します。
以下をコピペでOK!
━━━━━━━━━━━━━━━━━━┓
┃ ソースコードを表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import Foundation
import LocalAuthentication
class FaceAuth {
//認証のAPIを提供するLocalAuthenticationContext
var context: LAContext = LAContext()
//認証ポップアップに表示するメッセージ
let reason = "FaceID"
//FaceIDの処理
func auth(complation:@escaping(Bool) -> Void) {
//認証できるかのチェック
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil){
//認証始め
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ){ success, error in
//成功
if success{
//successが分かってからクロージャを呼び出す
DispatchQueue.main.async {
complation(true)
}
//失敗
}else if let laError = error as? LAError{
switch laError.code {
//認証失敗
case .authenticationFailed:
complation(false)
break
//ユーザーがキャンセルボタンを押した時
case .userCancel:
complation(false)
break
//アプリを閉じて失敗
case .systemCancel:
complation(false)
break
//パスコードがセットされてない
case .passcodeNotSet:
complation(false)
break
//パスコード認証がpolicyで許可されてない場合
case .userFallback:
complation(false)
break
//指紋認証の失敗上限
case .touchIDNotAvailable:
complation(false)
break
//指紋認証が許可されてない
case .touchIDNotEnrolled:
complation(false)
break
//指紋認証が登録されてない
case .touchIDLockout:
complation(false)
break
//アプリの内部によるキャンセル
case .appCancel:
complation(false)
break
//不明なエラー
case .invalidContext:
complation(false)
break
//よくわからん
case .notInteractive:
complation(false)
break
@unknown default:
break
}
}
}
}else {
//生体認証ができない場合
}
}
}
今作成したclassを参照すれば、いつでもfaceIDは使えます!
おまけ:使い方の例を表示(折りたたみ)
let face:FaceAuth = FaceAuth()
//この関数を実行すれば生体認証が行われる
func exec() {
face.auth{ result in
if result == true {
//成功した時の処理をここに記述
}
}
}
3-2. 初期画面の分岐作成
もしパスコードが設定されている場合、ロック画面に移動します。
@ mainが記述されているファイルを次のように変更します。
━━━━━━━━━━━━━━━━━━┓
┃ ソースコードを表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import SwiftUI
@main
struct faceIDApp: App {
//何もない時はfalse
let SetPass = UserDefaults.standard.bool(forKey: "SetPass")
var body: some Scene {
WindowGroup {
if SetPass{
//パスコードあり
LockView()
}else{
//パスコードなし
ContentView()
.environmentObject(passCheck())
}
}
}
}
UserDefaultsの"SetPass"
にて、ユーザーがパスコードを作成したかを判断し、最初に開く画面を変えます。
それだけです。
また、今後使う「1回目と2回目の入力が一致しているかを確認するためのclass」も用意しておきます。
━━━━━━━━━━━━━━━━━━┓
┃ classを表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import Foundation
class passCheck: ObservableObject {
@Published var firstCheck:[String?] = [nil, nil, nil, nil]
@Published var secondCheck:[String?] = [nil, nil, nil, nil]
@Published var passText:String = "パスワードを忘れると復元できません\n忘れないようご注意ください"
}
3-3. パスコード作成画面を実装
機能:パスコードを2回入力し、一致した時のみパスコード作成
画面の流れ:最初の画面(ContentView
) → 1回目の入力画面(SetLockView1
) → 2回目の入力画面(SetLockView2
)
3つの画面のコードは以下の通りです。
━━━━━━━━━━━━━━━━━━┓
┃ ContentViewを表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import SwiftUI
enum LockPath {
case pathSub1
case pathSub2
}
struct ContentView: View {
@State private var navigatePath: [LockPath] = []
//何もない時はfalse
@State var toggle = UserDefaults.standard.bool(forKey: "SetPass")
var body: some View {
NavigationStack(path: $navigatePath) {
List{
//下のNavigationLinkは関係ない
NavigationLink(destination: Text("機能なし")) {
Text("サンプル")
}
//パスコード作成 off -> onの時のみ画面遷移
Toggle("パスコード設定",isOn: $toggle)
.onAppear{
toggle = UserDefaults.standard.bool(forKey: "SetPass")
}
.onChange(of: toggle) {
if toggle {
//パスワード設定画面へ
navigatePath.append(.pathSub1)
}else{
//pass解除
UserDefaults.standard.set(false, forKey: "SetPass")
UserDefaults.standard.set(false, forKey: "UseFaceID")
}
}
}
.navigationDestination(for: LockPath.self){ value in
switch value {
case .pathSub1:
SetLockView1(path: $navigatePath)
case .pathSub2:
SetLockView2(path: $navigatePath)
}
}
}//navigationStack
}
}
━━━━━━━━━━━━━━━━━━┓
┃ SetLockView1を表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import SwiftUI
struct SetLockView1: View {
@Binding var path: [LockPath]
@Environment(\.dismiss) var dismiss
@EnvironmentObject var passcheck: passCheck
@State var count = 0
var body: some View {
VStack{
Spacer()
Text("パスコードの入力")
.font(.title3)
.fontWeight(.bold)
HStack{
//黒丸
ForEach(0..<4) { index in
if passcheck.firstCheck[index] == nil {
Image(systemName: "circle")
.padding()
}else {
Image(systemName: "circle.fill")
.padding()
}
}
}
//注意事項
Text("\(passcheck.passText)")
.font(.footnote)
.multilineTextAlignment(.center)
.foregroundColor(Color.pink)
Spacer()
//入力ボタン
HStack{
ForEach(1..<4){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(4..<7){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(7..<10){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
Button{
inputText(number: "0")
}label:{
Text("0")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
//入力ボタン終
Spacer()
}
.navigationTitle("入力")
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarLeading){
Button{
//リストに戻り、初期化
passcheck.firstCheck = [nil, nil, nil, nil]
passcheck.passText = "パスワードを忘れると復元できません\n忘れないようご注意ください"
path.removeLast()
}label:{
Image(systemName: "arrowshape.turn.up.backward.fill")
}
}
}
}
//ボタンが押された時の関数
private func inputText(number : String) {
for (index, getText) in passcheck.firstCheck.enumerated() {
//nilかチェック → 入力済みならスキップ
//入力したらfor文を抜け出す
if getText == nil {
passcheck.firstCheck[index] = number
//もしi.index = 3なら 画面遷移
if index == 3 {
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
path.append(.pathSub2)
}
}
break
}
}
}
}
━━━━━━━━━━━━━━━━━━┓
┃ SetLockView2を表示(折りたたみ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import SwiftUI
struct SetLockView2: View{
@Binding var path: [LockPath]
@Environment(\.dismiss) var dismiss
@EnvironmentObject var passcheck: passCheck
@State var isShowAlert = false
@State var passCode = ""
var body: some View{
VStack{
Spacer()
Text("パスコードの再入力")
.font(.title3)
.fontWeight(.bold)
//黒丸or白丸
HStack{
ForEach(0..<4) { index in
if passcheck.secondCheck[index] == nil {
Image(systemName: "circle")
.padding()
}else {
Image(systemName: "circle.fill")
.padding()
}
}
}
//注意事項
Text("\(passcheck.passText)")
.font(.footnote)
.multilineTextAlignment(.center)
.foregroundColor(Color.clear)
Spacer()
//入力ボタン
HStack{
ForEach(1..<4){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(4..<7){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(7..<10){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
Button{
inputText(number: "0")
}label:{
Text("0")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
//入力ボタン終
Spacer()
}//VStack
.navigationTitle("確認入力")
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarLeading){
Button{
//リストに戻り、初期化
passcheck.firstCheck = [nil, nil, nil, nil]
path.removeLast()
}label:{
Image(systemName: "arrowshape.turn.up.backward.fill")
}
}
}
//パスコードが一致した時のアラート
.alert(Text("FaceIdを使いますか?"), isPresented: $isShowAlert){
//ルートへ
Button("はい"){
UserDefaults.standard.set(true, forKey: "UseFaceID")
path.removeLast(path.count)
passcheck.firstCheck = [nil, nil, nil, nil]
passcheck.secondCheck = [nil, nil, nil, nil]
}
Button("いいえ"){
UserDefaults.standard.set(false, forKey: "UseFaceID")
path.removeLast(path.count)
passcheck.firstCheck = [nil, nil, nil, nil]
passcheck.secondCheck = [nil, nil, nil, nil]
}
}
}//body
//ボタンが押された時の関数
private func inputText(number : String) {
for (index, getText) in passcheck.secondCheck.enumerated() {
//nilかチェック → 入力済みならスキップ
//入力したらfor文を抜け出す
if getText == nil {
passcheck.secondCheck[index] = number
//全入力された時
if index == 3 {
if passcheck.firstCheck == passcheck.secondCheck {
//同じなら保存, alert(faceID)、
for i in 0...3 {
passCode = passCode + (passcheck.firstCheck[i] ?? "")
}
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
UserDefaults.standard.set(true, forKey: "SetPass")
UserDefaults.standard.set(passCode, forKey: "password")
print(passCode)
isShowAlert = true
}
}else{
//違うなら初期化して1つ前へ
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
passcheck.passText = "パスワードが1回目と異なります\nもう一度行ってください"
passcheck.firstCheck = [nil, nil, nil, nil]
passcheck.secondCheck = [nil, nil, nil, nil]
path.removeLast()
}
}
}
break
}
}
}
}
長々とコードがありますが、ほとんどデザイン関連なので気負わないでね!
数字ボタンを押したときに呼ばれるinputText(number : String)
が機能の9割です。
急ぎの方はコピペでOKです。
一様ポイントを3つ解説しておきますね。
3-3-1. NavigationStack(path: $~){}を使用すること
SetLockView2
からContentView
に戻るとき、2画面を一気に移動する必要があります。
これをスムーズに行うのがNavigationStack(path: $~){}です。
画面遷移の記録がストックされていきます(なんて便利!)
//画面遷移
navigatePath.append(~)
//1つ前に戻る
path.removeLast()
//全部戻る
path.removeLast(path.count)
3-3-2. DispatchQueue.main.asyncAfter(deadline: .now()+0.1){}について
画面遷移をする際に、0.1秒だけ遅らせています。
理由は入力を反映した上で、画面遷移を行いたいから。
0.1秒遅らせないと、ユーザー視点では4つ目を入力した瞬間に遷移してしまい、4つ目を入力した感覚がありません(自分で実験すればわかります笑)
//画面遷移
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
path.append(.~)
}
3-3-3. パスコード作成時にUserDefaultsに3つ保存する
- key = "password":パスコードに設定した数字4つ
- key = "SetPass":パスコードが設定されているか判断
- key = "UseFaceID":faceIDを使うか
kya名はお好みで!
3-4. パスコード解除画面を実装
最後にロック解除画面を作成します。
ほとんどパスコード設定画面と同じですね。
━━━━━━━━━━━━━━━━━━┓
┃ LockViewを表示(折りたたみ ) ┃
┗━━━━━━━━━━━━━━━━━━┛
import SwiftUI
struct LockView: View {
@State var passCheck: [String?] = [nil, nil, nil, nil]
//->trueでcontentViewを表示
@State var isShow = false
//設定したパスワード
let answer = UserDefaults.standard.string(forKey: "password")
//ficeID関連
let face:FaceAuth = FaceAuth()
let useFaceID = UserDefaults.standard.bool(forKey: "UseFaceID")
var body: some View {
ZStack{
VStack{
Spacer()
Text("パスコードの入力")
.font(.title3)
.fontWeight(.bold)
//ボタンやらもろもろ
HStack{
//黒丸
ForEach(0..<4) { index in
if passCheck[index] == nil {
Image(systemName: "circle")
.padding()
}else {
Image(systemName: "circle.fill")
.padding()
}
}
}
Spacer()
//入力ボタン
HStack{
ForEach(1..<4){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(4..<7){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
HStack{
ForEach(7..<10){ i in
Button{
inputText(number: String(i))
}label:{
Text("\(i)")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
}
}
Button{
inputText(number: "0")
}label:{
Text("0")
.font(.title)
.frame(width: 90, height: 45)
.foregroundColor(Color(.orange))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color(.orange), lineWidth: 1.0)
)
}
Spacer()
//入力ボタン終
.onAppear{
//faceidをするかどうか
if useFaceID {
exec()
}
}
}
if isShow {
ContentView()
.environmentObject(faceID.passCheck())
.transition(.opacity)
}
}
}
//入力関数
private func inputText(number : String) {
var checkAnswer = ""
for (index, getText) in passCheck.enumerated() {
//nilかチェック → 入力済みならスキップ
//入力したらfor文を抜け出す
if getText == nil {
passCheck[index] = number
if index == 3 {
for i in 0...3 {
checkAnswer = checkAnswer + (passCheck[i] ?? "")
}
if checkAnswer == answer {
//一致
DispatchQueue.main.asyncAfter(deadline: .now()+0.3) {
withAnimation{
isShow.toggle()
}
}
}else{
//初期化
passCheck = [nil, nil, nil, nil]
}
}
break
}
}
}
//顔認証の関数
func exec() {
face.auth{ result in
//認証が成功した時の記述
if result == true {
passCheck = ["a", "a", "a", "a"]
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
withAnimation{
isShow.toggle()
}
}
}
}
}
}
機能としては、画面を開いたときにfaceID認証。もしfaceIDが「ダメor設定なし」ならボタン入力という形です。
入力後は、ZStackでContentViewを表示する形します。
2つのポイントを解説します。
3-4-1. faceID関連の説明
以下のexec()
を使用すれば、好きなタイミングでfaceID認証が行えます。
私の場合はonAppear
の中に記述し、画面表示した際に認証を行なっています。
let face:FaceAuth = FaceAuth()
func exec() {
face.auth{ result in
if result == true {
//認証を成功した時の記述
}
}
}
3-4-2. withAnimation{}の必要性
ZStackで画面遷移を行なっているため、withAnimation{}がないと、画面遷移がオシャレではありません(個人の感想)。パッ!パッ!で感じで遷移してしまうんですよね。
私は.transition(.opacity)
を使用しています。←ゆっくり透明化
(withAnimationはお好みで色々試してみてね)
4. おわりに
今までのコードをコピペすれば、完成図のように動きます。プライバシーを守りたいアプリを作りたい方は参考にしてみてください!
全コードは以下に載せてます。
https://github.com/RyotaLab/faceID.git
最後に宣伝です。
このような誕生日を通知するiosアプリをリリースしています。
誕生日book-通知とメモをリストで管理
今回紹介したパスコード機能もあります。是非使ってみてください!