Skip to content

Instantly share code, notes, and snippets.

@fumiyasac
Last active December 29, 2018 04:45
Show Gist options
  • Save fumiyasac/6b43963844c59c9463c32a58a8f1e976 to your computer and use it in GitHub Desktop.
Save fumiyasac/6b43963844c59c9463c32a58a8f1e976 to your computer and use it in GitHub Desktop.
画面のパスコードロック機能を構築する際における実装例とポイントまとめ ref: https://qiita.com/fumiyasac@github/items/6124f9b272f5ee6ebb40
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
}
}
applicationWillResignActive: フォアグラウンドからバックグラウンドへ移行しようとした時
認証成功: FaceID
applicationDidBecomeActive: アプリの状態がアクティブになった時
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
}
}
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?()
})
}
}
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?()
})
}
}
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
}
}
}
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)
}
})
}
}
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 ""
}
}
}
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
}
}
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
}
}
}
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()
}
)
}
}
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