Last active
December 29, 2018 04:45
-
-
Save fumiyasac/6b43963844c59c9463c32a58a8f1e976 to your computer and use it in GitHub Desktop.
画面のパスコードロック機能を構築する際における実装例とポイントまとめ ref: https://qiita.com/fumiyasac@github/items/6124f9b272f5ee6ebb40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
・・・(省略)・・・ | |
func applicationDidEnterBackground(_ application: UIApplication) { | |
print("applicationDidEnterBackground: バックグラウンドへ移行完了した時") | |
// パスコードロック画面を表示する | |
displayPasscodeLockScreenIfNeeded() | |
} | |
・・・(省略)・・・ | |
// MARK: - Private Function | |
private func displayPasscodeLockScreenIfNeeded() { | |
let passcodeModel = PasscodeModel() | |
// パスコードロックを設定していない場合は何もしない | |
if !passcodeModel.existsHashedPasscode() { | |
return | |
} | |
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController { | |
// 現在のrootViewControllerにおいて一番上に表示されているViewControllerを取得する | |
var topViewController: UIViewController = rootViewController | |
while let presentedViewController = topViewController.presentedViewController { | |
topViewController = presentedViewController | |
} | |
// すでにパスコードロック画面がかぶせてあるかを確認する | |
let isDisplayedPasscodeLock: Bool = topViewController.children.map{ | |
return $0 is PasscodeViewController | |
}.contains(true) | |
// パスコードロック画面がかぶせてなければかぶせる | |
if !isDisplayedPasscodeLock { | |
let nav = UINavigationController(rootViewController: getPasscodeViewController()) | |
nav.modalPresentationStyle = .overFullScreen | |
nav.modalTransitionStyle = .crossDissolve | |
topViewController.present(nav, animated: true, completion: nil) | |
} | |
} | |
} | |
private func getPasscodeViewController() -> PasscodeViewController { | |
// 遷移先のViewControllerに関する設定をする | |
let sb = UIStoryboard(name: "Passcode", bundle: nil) | |
let vc = sb.instantiateInitialViewController() as! PasscodeViewController | |
vc.setTargetInputPasscodeType(.displayPasscodeLock) | |
vc.setTargetPresenter(PasscodePresenter(previousPasscode: nil)) | |
return vc | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
applicationWillResignActive: フォアグラウンドからバックグラウンドへ移行しようとした時 | |
認証成功: FaceID | |
applicationDidBecomeActive: アプリの状態がアクティブになった時 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import FontAwesome_swift | |
class GlobalTabBarController: UITabBarController, UITabBarControllerDelegate { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
self.delegate = self | |
self.viewControllers = [UIViewController(), UIViewController()] | |
setupUserInterface() | |
// アプリ起動完了時のパスコード画面表示の通知監視 | |
NotificationCenter.default.addObserver(self, selector: #selector(self.displayPasscodeLockScreenIfNeeded), name: UIApplication.didFinishLaunchingNotification, object: nil) | |
} | |
// MARK: - Private Function | |
@objc private func displayPasscodeLockScreenIfNeeded() { | |
let passcodeModel = PasscodeModel() | |
// パスコードロックを設定していない場合は何もしない | |
if !passcodeModel.existsHashedPasscode() { | |
return | |
} | |
let nav = UINavigationController(rootViewController: getPasscodeViewController()) | |
nav.modalPresentationStyle = .overFullScreen | |
nav.modalTransitionStyle = .crossDissolve | |
self.present(nav, animated: false, completion: nil) | |
} | |
・・・(省略)・・・ | |
private func getPasscodeViewController() -> PasscodeViewController { | |
// 遷移先のViewControllerに関する設定をする | |
let sb = UIStoryboard(name: "Passcode", bundle: nil) | |
let vc = sb.instantiateInitialViewController() as! PasscodeViewController | |
vc.setTargetInputPasscodeType(.displayPasscodeLock) | |
vc.setTargetPresenter(PasscodePresenter(previousPasscode: nil)) | |
return vc | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import UIKit | |
import FontAwesome_swift | |
class InputPasscodeDisplayView: CustomViewBase { | |
private let defaultKeyImageAlpha: CGFloat = 0.3 | |
private let selectedKeyImageAlpha: CGFloat = 1.0 | |
@IBOutlet private var keyImageViews: [UIImageView]! | |
// MARK: - Initializer | |
required init(frame: CGRect) { | |
super.init(frame: frame) | |
setupInputPasscodeDisplayView() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setupInputPasscodeDisplayView() | |
} | |
// MARK: - Function | |
// 鍵マーク表示部分が増えていくような動きを実現する | |
func incrementDisplayImagesBy(passcodeStringCount: Int = 0) { | |
keyImageViews.enumerated().forEach { | |
let imageView = $0.element | |
guard let superView = imageView.superview else { | |
return | |
} | |
// MEMO: 引数で渡された値とタグ値が一致した場合にはアニメーションを実行する | |
if imageView.tag == passcodeStringCount { | |
imageView.alpha = selectedKeyImageAlpha | |
executeKeyImageAnimation(for: superView) | |
} else if imageView.tag < passcodeStringCount { | |
imageView.alpha = selectedKeyImageAlpha | |
} else { | |
imageView.alpha = defaultKeyImageAlpha | |
} | |
} | |
} | |
// 鍵マーク表示部分が減っていくような動きを実現する | |
func decrementDisplayImagesBy(passcodeStringCount: Int = 0) { | |
keyImageViews.enumerated().forEach { | |
let imageView = $0.element | |
// MEMO: 入力した情報を消去する場合はアニメーションは実行しません | |
if imageView.tag <= passcodeStringCount { | |
imageView.alpha = selectedKeyImageAlpha | |
} else { | |
imageView.alpha = defaultKeyImageAlpha | |
} | |
} | |
} | |
// MARK: - Private Function | |
private func setupInputPasscodeDisplayView() { | |
keyImageViews.enumerated().forEach { | |
let imageView = $0.element | |
imageView.image = UIImage.fontAwesomeIcon(name: .key, style: .solid, textColor: .black, size: CGSize(width: 48.0, height: 48.0)) | |
imageView.alpha = defaultKeyImageAlpha | |
} | |
} | |
// パスコード入力画面用の画像が弾む様なアニメーションをする | |
private func executeKeyImageAnimation(for targetView: UIView, completionHandler: (() -> ())? = nil) { | |
// アイコン画像用のViewが縮むようにバウンドするアニメーションを付与する | |
UIView.animateKeyframes(withDuration: 0.06, delay: 0.0, options: [.autoreverse], animations: { | |
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 1.0, animations: { | |
targetView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) | |
}) | |
UIView.addKeyframe(withRelativeStartTime: 1.0, relativeDuration: 1.0, animations: { | |
targetView.transform = CGAffineTransform.identity | |
}) | |
}, completion: { finished in | |
completionHandler?() | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import UIKit | |
// MEMO: このViewに配置しているボタンが押下された場合に値の変更を反映させるためのプロトコル | |
protocol InputPasscodeKeyboardDelegate: NSObjectProtocol { | |
// 0~9の数字ボタンが押下された場合にその数字を文字列で送る | |
func inputPasscodeNumber(_ numberOfString: String) | |
// 削除ボタンが押下された場合に値を削除する | |
func deletePasscodeNumber() | |
// TouchID/FaceID搭載端末の場合に実行する | |
func executeLocalAuthentication() | |
} | |
class InputPasscodeKeyboardView: CustomViewBase { | |
weak var delegate: InputPasscodeKeyboardDelegate? | |
// ボタン押下時の軽微な振動を追加する | |
private let buttonFeedbackGenerator: UIImpactFeedbackGenerator = { | |
let generator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) | |
generator.prepare() | |
return generator | |
}() | |
// パスコードロック用の数値入力用ボタン | |
// MEMO: 「Outlet Collection」を用いて接続しているのでweakはけつけていません | |
@IBOutlet private var inputPasscodeNumberButtons: [UIButton]! | |
// パスコードロック用のLocalAuthentication実行用ボタン | |
@IBOutlet private weak var executeLocalAuthenticationButton: UIButton! | |
// パスコードロック用の数値削除用ボタン | |
@IBOutlet private weak var deletePasscodeNumberButton: UIButton! | |
// MARK: - Initializer | |
required init(frame: CGRect) { | |
super.init(frame: frame) | |
setupInputPasscodeKeyboardView() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setupInputPasscodeKeyboardView() | |
} | |
// MARK: - Function | |
func shouldEnabledLocalAuthenticationButton(_ result: Bool = true) { | |
executeLocalAuthenticationButton.isEnabled = result | |
executeLocalAuthenticationButton.superview?.alpha = (result) ? 1.0 : 0.3 | |
} | |
// MARK: - Private Function | |
@objc private func inputPasscodeNumberButtonTapped(sender: UIButton) { | |
guard let superView = sender.superview else { | |
return | |
} | |
executeButtonAnimation(for: superView) | |
buttonFeedbackGenerator.impactOccurred() | |
self.delegate?.inputPasscodeNumber(String(sender.tag)) | |
} | |
@objc private func deletePasscodeNumberButtonTapped(sender: UIButton) { | |
guard let superView = sender.superview else { | |
return | |
} | |
executeButtonAnimation(for: superView) | |
buttonFeedbackGenerator.impactOccurred() | |
self.delegate?.deletePasscodeNumber() | |
} | |
@objc private func executeLocalAuthenticationButtonTapped(sender: UIButton) { | |
guard let superView = sender.superview else { | |
return | |
} | |
executeButtonAnimation(for: superView) | |
buttonFeedbackGenerator.impactOccurred() | |
self.delegate?.executeLocalAuthentication() | |
} | |
private func setupInputPasscodeKeyboardView() { | |
inputPasscodeNumberButtons.enumerated().forEach { | |
let button = $0.element | |
button.addTarget(self, action: #selector(self.inputPasscodeNumberButtonTapped(sender:)), for: .touchDown) | |
} | |
deletePasscodeNumberButton.addTarget(self, action: #selector(self.deletePasscodeNumberButtonTapped(sender:)), for: .touchDown) | |
executeLocalAuthenticationButton.addTarget(self, action: #selector(self.executeLocalAuthenticationButtonTapped(sender:)), for: .touchDown) | |
} | |
private func executeButtonAnimation(for targetView: UIView, completionHandler: (() -> ())? = nil) { | |
// MEMO: ユーザーの入力レスポンスがアニメーションによって遅延しないような考慮をする | |
UIView.animateKeyframes(withDuration: 0.16, delay: 0.0, options: [.allowUserInteraction, .autoreverse], animations: { | |
UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 1.0, animations: { | |
targetView.alpha = 0.5 | |
}) | |
UIView.addKeyframe(withRelativeStartTime: 1.0, relativeDuration: 1.0, animations: { | |
targetView.alpha = 1.0 | |
}) | |
}, completion: { finished in | |
completionHandler?() | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum InputPasscodeType { | |
case inputForCreate // パスコードの新規作成 | |
case retryForCreate // パスコードの新規作成時の確認 | |
case inputForUpdate // パスコードの変更 | |
case retryForUpdate // パスコードの変更時の確認 | |
case displayPasscodeLock // パスコードロック画面の表示時 | |
// MARK: - Function | |
func getTitle() -> String { | |
switch self { | |
case .inputForCreate, .retryForCreate: | |
return "パスコード登録" | |
case .inputForUpdate, .retryForUpdate: | |
return "パスコード変更" | |
case .displayPasscodeLock: | |
return "パスコードロック" | |
} | |
} | |
func getMessage() -> String { | |
switch self { | |
case .inputForCreate: | |
return "登録したいパスコードを入力して下さい" | |
case .inputForUpdate: | |
return "変更したいパスコードを入力して下さい" | |
case .retryForCreate, .retryForUpdate: | |
return "確認用に再度パスコードを入力して下さい" | |
case .displayPasscodeLock: | |
return "設定したパスコードを入力して下さい" | |
} | |
} | |
func getNextInputPasscodeType() -> InputPasscodeType? { | |
switch self { | |
case .inputForCreate: | |
return .retryForCreate | |
case .inputForUpdate: | |
return .retryForUpdate | |
default: | |
return nil | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import LocalAuthentication | |
class LocalAuthenticationManager { | |
// MARK: - Static Function | |
static func getDeviceOwnerLocalAuthenticationType() -> LocalAuthenticationType { | |
let localAuthenticationContext = LAContext() | |
// iOS11以上の場合: FaceID/TouchID/パスコードの3種類 | |
if #available(iOS 11.0, *) { | |
if localAuthenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { | |
switch localAuthenticationContext.biometryType { | |
case .faceID: | |
return .authWithFaceID | |
case .touchID: | |
return .authWithTouchID | |
default: | |
return .authWithManual | |
} | |
} | |
// iOS10以下の場合: TouchID/パスコードの2種類 | |
} else { | |
if localAuthenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { | |
return .authWithTouchID | |
} else { | |
return .authWithManual | |
} | |
} | |
return .authWithManual | |
} | |
static func evaluateDeviceOwnerLocalAuthentication(successHandler: (() -> ())? = nil, errorHandler: (() -> ())? = nil) { | |
let type = self.getDeviceOwnerLocalAuthenticationType() | |
// パスコードでの解除の場合は以降の処理は行わない | |
if type == .authWithManual { | |
return | |
} | |
// FaceID/TouchIDでの認証結果に応じて引数のクロージャーに設定した処理を実行する | |
let localAuthenticationContext = LAContext() | |
localAuthenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: type.getLocalizedReason(), reply: { success, evaluateError in | |
if success { | |
// 認証成功時の処理を書く | |
successHandler?() | |
print("認証成功:", type.getDescriptionTitle()) | |
} else { | |
// 認証失敗時の処理を書く | |
errorHandler?() | |
print("認証失敗:", evaluateError.debugDescription) | |
} | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum LocalAuthenticationType { | |
case authWithFaceID // FaceIDでのパスコード解除 | |
case authWithTouchID // TouchIDでのパスコード解除 | |
case authWithManual // 手動入力でのパスコード解除 | |
// MARK: - Function | |
func getDescriptionTitle() -> String { | |
switch self { | |
case .authWithFaceID: | |
return "FaceID" | |
case .authWithTouchID: | |
return "TouchID" | |
default: | |
return "" | |
} | |
} | |
func getLocalizedReason() -> String { | |
switch self { | |
case .authWithFaceID, .authWithTouchID: | |
return "\(self.getDescriptionTitle())を利用して画面ロックを解除します。" | |
default: | |
return "" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class PasscodeModel { | |
private let userHashedPasscode = "PasscodeModel:userHashedPasscode" | |
private var ud: UserDefaults { | |
return UserDefaults.standard | |
} | |
// MARK: - Function | |
// ユーザーが入力したパスコードを保存する | |
func saveHashedPasscode(_ passcode: String) -> Bool { | |
if isValid(passcode) { | |
setHashedPasscode(passcode) | |
return true | |
} else { | |
return false | |
} | |
} | |
// ユーザーが入力したパスコードと現在保存されているパスコードを比較する | |
func compareSavedPasscodeWith(inputPasscode: String) -> Bool { | |
let hashedInputPasscode = getHashedPasscodeByHMAC(inputPasscode) | |
let savedPasscode = getHashedPasscode() | |
return hashedInputPasscode == savedPasscode | |
} | |
// ユーザーが入力したパスコードが存在するかを判定する | |
func existsHashedPasscode() -> Bool { | |
let savedPasscode = getHashedPasscode() | |
return !savedPasscode.isEmpty | |
} | |
// HMAC形式でハッシュ化されたパスコード取得する | |
func getHashedPasscode() -> String { | |
return ud.string(forKey: userHashedPasscode) ?? "" | |
} | |
// 現在保存されているパスコードを削除する | |
func deleteHashedPasscode() { | |
ud.set("", forKey: userHashedPasscode) | |
} | |
// MARK: - Private Function | |
// 引数で受け取ったパスコードをhmacで暗号化した上で保存する | |
private func setHashedPasscode(_ passcode: String) { | |
let hashedPasscode = getHashedPasscodeByHMAC(passcode) | |
ud.set(hashedPasscode, forKey: userHashedPasscode) | |
} | |
// 引数で受け取った値をhmacで暗号化する | |
private func getHashedPasscodeByHMAC(_ passcode: String) -> String { | |
return passcode.hmac(algorithm: .SHA256) | |
} | |
// 引数で受け取った値の形式が正しいかどうかを判定する | |
private func isValid(_ passcode: String) -> Bool { | |
return isValidLength(passcode) && isValidFormat(passcode) | |
} | |
// 引数で受け取った値が4文字かを判定する | |
private func isValidLength(_ passcode: String) -> Bool { | |
return passcode.count == AppConstant.PASSCODE_LENGTH | |
} | |
// 引数で受け取った値が半角数字かを判定する | |
private func isValidFormat(_ passcode: String) -> Bool { | |
let regexp = try! NSRegularExpression.init(pattern: "^(?=.*?[0-9])[0-9]{4}$", options: []) | |
let targetString = passcode as NSString | |
let result = regexp.firstMatch(in: passcode, options: [], range: NSRange.init(location: 0, length: targetString.length)) | |
return result != nil | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
protocol PasscodePresenterDelegate: NSObjectProtocol { | |
func goNext() | |
func dismissPasscodeLock() | |
func savePasscode() | |
func showError() | |
} | |
class PasscodePresenter { | |
private let previousPasscode: String? | |
weak var delegate: PasscodePresenterDelegate? | |
// MARK: - Initializer | |
// MEMO: 前の画面で入力したパスコードを利用したい場合は引数に設定する | |
init(previousPasscode: String?) { | |
self.previousPasscode = previousPasscode | |
} | |
// MARK: - Function | |
// ViewController側でパスコードの入力が完了した場合に実行する処理 | |
func inputCompleted(_ passcode: String, inputPasscodeType: InputPasscodeType) { | |
let passcodeModel = PasscodeModel() | |
switch inputPasscodeType { | |
case .inputForCreate, .inputForUpdate: | |
// 再度パスコードを入力するための確認画面へ遷移する | |
self.delegate?.goNext() | |
break | |
case .retryForCreate, .retryForUpdate: | |
// 前画面で入力したパスコードと突き合わせて、同じだったらUserDefaultへ登録する | |
if previousPasscode != passcode { | |
self.delegate?.showError() | |
return | |
} | |
if passcodeModel.saveHashedPasscode(passcode) { | |
self.delegate?.savePasscode() | |
} else { | |
self.delegate?.showError() | |
} | |
break | |
case .displayPasscodeLock: | |
// 保存されているユーザーが設定したパスコードと突き合わせて、同じだったらパスコードロック画面を解除する | |
if passcodeModel.compareSavedPasscodeWith(inputPasscode: passcode) { | |
self.delegate?.dismissPasscodeLock() | |
} else { | |
self.delegate?.showError() | |
} | |
break | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import AudioToolbox | |
class PasscodeViewController: UIViewController { | |
// 画面遷移前に引き渡す変数 | |
private var inputPasscodeType: InputPasscodeType! | |
private var presenter: PasscodePresenter! | |
private var userInputPasscode: String = "" | |
@IBOutlet weak private var inputPasscodeDisplayView: InputPasscodeDisplayView! | |
@IBOutlet weak private var inputPasscodeKeyboardView: InputPasscodeKeyboardView! | |
@IBOutlet weak private var inputPasscodeMessageLabel: UILabel! | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// MEMO: PasscodePresenterに定義したプロトコルの処理を実行するようにする | |
presenter.delegate = self | |
setupUserInterface() | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
hideTabBarItems() | |
} | |
// MARK: - Function | |
func setTargetPresenter(_ presenter: PasscodePresenter) { | |
self.presenter = presenter | |
} | |
func setTargetInputPasscodeType(_ inputPasscodeType: InputPasscodeType) { | |
self.inputPasscodeType = inputPasscodeType | |
} | |
// MARK: - Private Function | |
private func setupUserInterface() { | |
setupNavigationItems() | |
setupInputPasscodeMessageLabel() | |
setupPasscodeNumberKeyboardView() | |
} | |
private func setupNavigationItems() { | |
setupNavigationBarTitle(inputPasscodeType.getTitle()) | |
removeBackButtonText() | |
} | |
private func setupInputPasscodeMessageLabel() { | |
inputPasscodeMessageLabel.text = inputPasscodeType.getMessage() | |
} | |
private func setupPasscodeNumberKeyboardView() { | |
inputPasscodeKeyboardView.delegate = self | |
// MEMO: 利用している端末のFaceIDやTouchIDの状況やどの画面で利用しているか見てボタン状態を判断する | |
var isEnabledLocalAuthenticationButton: Bool = false | |
if inputPasscodeType == .displayPasscodeLock { | |
isEnabledLocalAuthenticationButton = LocalAuthenticationManager.getDeviceOwnerLocalAuthenticationType() != .authWithManual | |
} | |
inputPasscodeKeyboardView.shouldEnabledLocalAuthenticationButton(isEnabledLocalAuthenticationButton) | |
} | |
private func hideTabBarItems() { | |
if let tabBarVC = self.tabBarController { | |
tabBarVC.tabBar.isHidden = true | |
} | |
} | |
private func acceptUserInteraction() { | |
self.view.isUserInteractionEnabled = true | |
} | |
private func refuseUserInteraction() { | |
self.view.isUserInteractionEnabled = false | |
} | |
// 最初の処理Aを実行 → 指定秒数後に次の処理Bを実行するためのラッパー | |
// MEMO: 早すぎる入力を行なった際に意図しない画面遷移を実行される現象の対応策として実行している | |
private func executeSeriesAction(firstAction: (() -> ())? = nil, deleyedAction: @escaping (() -> ())) { | |
// 最初は該当画面のUserInteractionを受け付けない | |
self.refuseUserInteraction() | |
firstAction?() | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.24) { | |
// 指定秒数経過後は該当画面のUserInteractionを受け付ける | |
self.acceptUserInteraction() | |
deleyedAction() | |
} | |
} | |
} | |
// MARK: - PasscodeNumberKeyboardDelegate | |
extension PasscodeViewController: InputPasscodeKeyboardDelegate { | |
func inputPasscodeNumber(_ numberOfString: String) { | |
// パスコードが0から3文字の場合はキーボードの押下された数値の文字列を末尾に追加する | |
if 0...3 ~= userInputPasscode.count { | |
userInputPasscode = userInputPasscode + numberOfString | |
inputPasscodeDisplayView.incrementDisplayImagesBy(passcodeStringCount: userInputPasscode.count) | |
} | |
// パスコードが4文字の場合はPasscodePresenter側に定義した入力完了処理を実行する | |
if userInputPasscode.count == AppConstant.PASSCODE_LENGTH { | |
presenter.inputCompleted(userInputPasscode, inputPasscodeType: inputPasscodeType) | |
} | |
} | |
func deletePasscodeNumber() { | |
// パスコードが1から3文字の場合は数値の文字列の末尾を削除する | |
if 1...3 ~= userInputPasscode.count { | |
userInputPasscode = String(userInputPasscode.prefix(userInputPasscode.count - 1)) | |
inputPasscodeDisplayView.decrementDisplayImagesBy(passcodeStringCount: userInputPasscode.count) | |
} | |
} | |
func executeLocalAuthentication() { | |
// パスコードロック画面以外では操作を許可しない | |
guard inputPasscodeType == .displayPasscodeLock else { | |
return | |
} | |
// TouchID/FaceIDによる認証を実行し、成功した場合にはパスコードロックを解除する | |
LocalAuthenticationManager.evaluateDeviceOwnerLocalAuthentication( | |
successHandler: { | |
DispatchQueue.main.async { | |
self.dismiss(animated: true, completion: nil) | |
} | |
}, | |
errorHandler: {} | |
) | |
} | |
} | |
// MARK: - PasscodePresenterProtocol | |
extension PasscodeViewController: PasscodePresenterDelegate { | |
// 次に表示するべき画面へ入力された値を引き継いだ状態で遷移する | |
func goNext() { | |
executeSeriesAction( | |
firstAction: {}, | |
deleyedAction: { | |
// Enum経由で次のアクションで設定すべきEnumの値を取得する | |
guard let nextInputPasscodeType = self.inputPasscodeType.getNextInputPasscodeType() else { | |
return | |
} | |
// 遷移先のViewControllerに関する設定をする | |
let sb = UIStoryboard(name: "Passcode", bundle: nil) | |
let vc = sb.instantiateInitialViewController() as! PasscodeViewController | |
vc.setTargetInputPasscodeType(nextInputPasscodeType) | |
vc.setTargetPresenter(PasscodePresenter(previousPasscode: self.userInputPasscode)) | |
self.navigationController?.pushViewController(vc, animated: true) | |
self.userInputPasscode.removeAll() | |
self.inputPasscodeDisplayView.decrementDisplayImagesBy() | |
} | |
) | |
} | |
// パスコードロック画面を解除する | |
func dismissPasscodeLock() { | |
executeSeriesAction( | |
firstAction: {}, | |
deleyedAction: { | |
self.dismiss(animated: true, completion: nil) | |
} | |
) | |
} | |
// ユーザーが入力したパスコードを保存して設定画面へ戻る | |
func savePasscode() { | |
executeSeriesAction( | |
firstAction: {}, | |
deleyedAction: { | |
self.navigationController?.popToRootViewController(animated: true) | |
} | |
) | |
} | |
// ユーザーが入力した値が正しくないことをユーザーへ伝える | |
func showError() { | |
executeSeriesAction( | |
// 実行直後はエラーメッセージを表示する & バイブレーションを適用する | |
firstAction: { | |
self.inputPasscodeMessageLabel.text = "パスコードが一致しませんでした" | |
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) | |
}, | |
// 秒数経過後にユーザーが入力したメッセージを空にする & パスコードのハート表示をリセットする | |
deleyedAction: { | |
self.userInputPasscode.removeAll() | |
self.inputPasscodeDisplayView.decrementDisplayImagesBy() | |
} | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import UIKit | |
class StickyStyleFlowLayout: UICollectionViewFlowLayout { | |
// 拡大縮小比を変更するための変数(値を変更する必要がある場合のみ利用する) | |
var firstItemTransform: CGFloat? | |
// 引数で渡された範囲内に表示されているUICollectionViewLayoutAttributesを返す | |
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | |
// 該当のUICollectionViewLayoutAttributesを取得する | |
let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true) | |
// 該当のUICollectionViewにへHeaderまたはFooterを | |
var headerAttributes: UICollectionViewLayoutAttributes? | |
var footerAttributes: UICollectionViewLayoutAttributes? | |
// 該当のUICollectionViewLayoutAttributesに対してUICollectionViewLayoutAttributesの更新をする | |
items.enumerateObjects(using: { (object, _, _) -> Void in | |
let attributes = object as! UICollectionViewLayoutAttributes | |
// Header・Footer・セルの場合で場合分けをする | |
if attributes.representedElementKind == UICollectionView.elementKindSectionHeader { | |
headerAttributes = attributes | |
} else if attributes.representedElementKind == UICollectionView.elementKindSectionFooter { | |
footerAttributes = attributes | |
} else { | |
self.updateCellAttributes(attributes, headerAttributes: headerAttributes, footerAttributes: footerAttributes) | |
} | |
}) | |
return items as? [UICollectionViewLayoutAttributes] | |
} | |
// 更新された位置情報からレイアウト処理を再実行するか | |
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { | |
return true | |
} | |
// MARK: - Private Function | |
// 該当のセルにおけるUICollectionViewLayoutAttributesの値を更新する | |
private func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes, headerAttributes: UICollectionViewLayoutAttributes?, footerAttributes: UICollectionViewLayoutAttributes?) { | |
// 配置しているUICollectionViewにおける最大値・最小値を取得しておく | |
let minY = collectionView!.bounds.minY + collectionView!.contentInset.top | |
var maxY = attributes.frame.origin.y | |
// Headerを利用している場合はその分の高さ減算する | |
if let headerAttributes = headerAttributes { | |
maxY -= headerAttributes.bounds.height | |
} | |
// Footerを利用している場合はその分の高さ減算する | |
if let footerAttributes = footerAttributes { | |
maxY -= footerAttributes.bounds.height | |
} | |
// 該当のUICollectionViewLayoutAttributesの拡大縮小比を調節して表示する | |
var origin = attributes.frame.origin | |
let finalY = max(minY, maxY) | |
let deltaY = (finalY - origin.y) / attributes.frame.height | |
if let itemTransform = firstItemTransform { | |
let scale = 1 - deltaY * itemTransform | |
attributes.transform = CGAffineTransform(scaleX: scale, y: scale) | |
} | |
origin.y = finalY | |
attributes.frame = CGRect(origin: origin, size: attributes.frame.size) | |
attributes.zIndex = attributes.indexPath.row | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment