#13 Xcode — APP 的解鎖畫面(Passcode)
目錄
⦿ Passcode
⦿ Outlet & Outlet Collection
⦿ State Control
⦿ Spaghetti Code
⦿ Modify
⦿ 流程檢查
Passcode
Passcode
跟 Password 有點像,Password 通常用於帳號登入時,Passcode
則像是從螢幕鎖定狀態,回到正常使用狀態前需輸入的幾位數字,如 4 ~ 6 位數字碼。
下面我們來試試看用 Xcode 寫一個鎖定畫面。
繼續閱讀|回目錄
Outlet & Outlet Collection
先上 Gif 跟流程圖:
參照下面這篇文章,我們了解到 IBOutlet
跟 Outlet Collection
的差異:
IBOutlet 可以將一個個 element 分開控制,而 Outlet Collection 則是在一行程式碼中可以控制多個 elements。
於是,我們便可將數字按鈕(0 ~ 9)、Passcode 欄位(OOOO)拉線將 Xib 與程式碼結合,程式碼如 [UIButton] 與 [UIImageView],即是 Button 的矩陣與 ImageView 的矩陣。
IBOutlet 我們已經很熟了,在程式碼中長成如下:
@IBOutlet weak var myButton: UIButton!
但在 Outlet Collection 中則不會冠上 weak,如下:
接著,Button Pressed(Tapped)這個 Action function 也如法泡製,會寫成如下:
@IBAction func passBtnsTapped(_ sender: UIButton) {
if enterNum.count != 4 {
enterNum += sender.currentTitle!
}
changeImage()
print(enterNum)
}
這個 Action 包括了 0 ~ 9 這些按鈕的點擊,enterNum 是 String,當它的位數小於 4 時(即 Passcode 未輸入完全),enterNum 就是不斷地加上你現在按下的按鈕的 title,當然這些 Button 的 title 就是數字 0 ~ 9 了。
繼續閱讀|回目錄
State Control
changeImage
接著看看 changeImage 這個 function:
func changeImage() {
switch enterNum.count {
case 1:
for i in 0...3 {passImageViews[i].isHighlighted = false}
for i in 0...0 {passImageViews[i].isHighlighted = true}
case 2:
for i in 0...3 {passImageViews[i].isHighlighted = false}
for i in 0...1 {passImageViews[i].isHighlighted = true}
case 3:
for i in 0...3 {passImageViews[i].isHighlighted = false}
for i in 0...2 {passImageViews[i].isHighlighted = true}
case 4:
for i in 0...3 {passImageViews[i].isHighlighted = false}
for i in 0...3 {passImageViews[i].isHighlighted = true}
checkPass()
default:
for i in 0...3 {passImageViews[i].isHighlighted = false}
}
}
enterNum 只有五種可能,0 位 ~ 4 位,再去檢查它的 0 ~ 4 位時的情況。然而為什麼出現這麼多 isHighlighted 的控制,我們先看到下圖:
左圖表示我們用 isHighlighted 來作為 Passcode 欄位的狀態控制
,即是有沒有輸入密碼
,有
輸入密碼的狀態下,不論輸入的是 1 ~ 4 位,我們都要將密碼遮起來。在第 4 位輸入後,密碼遮完,欄位也要清空,回到 0 位的狀態,即是沒有
輸入密碼的狀態。
但 isHighlighted 在使用者交互狀態下,當這個 element 被選中,可能會影響你的狀態控制
,為避免此狀況,我們先將 User Interaction Enabled 取消。
而這段 switch 程式碼的意思是,如果輸入 1 位 Passcode,我必須要將這 1 位密碼遮起來,然而 for i in 0…3 { passImageViews[i].isHighlighted = false }
是每次都先將狀態清空,再設定狀態,避免狀態的耦合
。(當然,這是方便且偷懶(?)的做法)
所以 2、3、4 位的狀態也須先將狀態清空,再設定狀態,但在第 4 位時我們還須去檢查是否密碼正確。
checkPass
接著進到 checkPass 這個 function,如下:
private func checkPass() {
if enterNum == password {
performSegue(withIdentifier: "myFrontPage", sender: self)
reset()
} else {
let alertController = UIAlertController(title: "Caution",
message: "Forget Password ?",
preferredStyle: .alert)
let notOkAction = UIAlertAction(title: "Back",
style: .cancel,
handler: nil)
alertController.addAction(notOkAction)
present(alertController, animated: true, completion: reset)
}
}
如果輸入的密碼跟設定的密碼相同
時,就跳往鎖定前的畫面
,否則就跳出警示視窗
,告訴使用者密碼是錯的,重新回到輸入密碼的流程。
並且,此刻會 reset 掉 Passcode 欄位,另外,這邊的 password 設定為 5566
這個 String。
reset
我們來看看 reset:
private func reset() {
for i in passImageViews {
i.isHighlighted = false
}
enterNum = ""
}
對於所有的 passImageViews 元素,都將 isHighLighted 設為 false,且 enterNum 清空,讓它可以重新加入。
Spaghetti Code
但你不覺得程式碼有點義大利麵嗎?從 passBtnsPressed 開始,裡面先處理了 enterNum,再由 enterNum 去判斷 UI 的呈現(changeImage),並在 enterNum 為 4 的時候又去呼叫了 checkPass 以檢查是否密碼正確。
說起來 UI、Data、使用者交互、function 分工並不是那麼清晰。
下面,我們先試著改改看程式碼。
繼續閱讀|回目錄
Modify
首先我們改改看 changeImage:
func changeImage() {
let count = enterNum.count
for i in 0...3 {
passImageViews[i].isHighlighted = i < count
}
if count == 4 {
checkPass()
}
}
count 為設定密碼當次,密碼按了幾下(或說輸入幾位),依照順序,imageView 用 i < count 來判斷是否 isHighLighted。
如果是 1 位,0 < 1,所以 isHighLighted 是 true 的有一個,當然後面 1、2、3 就是 false。
如果是 2 位,0 < 2、1 < 2,所以 isHighLighted 是 true 的有兩個,當然後面 2、3 就是 false。
如果是 3 位,0 < 3、1 < 3、2 < 3。所以 isHighLighted 是 true 的有三個,當然後面 3 就是 false。
如果是 4 位,除了所有欄位都要遮罩,還須 checkPass
。
改完 changeImage(),我們回頭看看 VC 所有的程式碼:
@IBOutlet var passImageViews: [UIImageView]!
@IBOutlet var passBtns: [UIButton]!
private var password = "5566"
private var enterNum = ""
private func checkPass() {
if enterNum == password {
performSegue(withIdentifier: "myFrontPage", sender: self)
reset()
} else {
let alertController = UIAlertController(title: "Caution", message: "Forget Password ?", preferredStyle: .alert)
let notOkAction = UIAlertAction(title: "Back", style: .cancel, handler: nil)
alertController.addAction(notOkAction)
present(alertController, animated: true, completion: reset)
}
}
private func reset() {
for i in passImageViews {
i.isHighlighted = false
}
enterNum = ""
}
private func changeImage() {
let count = enterNum.count
for i in 0...3 {
passImageViews[i].isHighlighted = i < count
}
if count == 4 {
checkPass()
}
}
@IBAction func passBtnsTapped(_ sender: UIButton) {
if enterNum.count != 4 {
enterNum += sender.currentTitle!
}
changeImage()
}
@IBAction func deleteBtnTapped(_ sender: Any) {
if enterNum.count != 0 {
enterNum.removeLast()
}
changeImage()
}
集中處理的 Outlet 有 passImageViews、passBtns,分別是 Passcode 欄位跟欲輸入的數字。
密碼設為 5566,enterNum 用來儲存現在輸入的數字,兩者皆為 String。
checkPass 的邏輯為,若是 5566 則回鎖定前畫面;若不是,則跳出警示視窗並將 Passcode 欄位清空。清空這個動作在原先 changeImage() 中是先做完 for i in passImageViews { i.isHighlighted = false }
,再設定特定 Passcode 欄位圖片,即更改 i.isHighlighted=true
,但 changeImage() 就不能清空 enterNum 了,不然要拿什麼比對 5566
?
而 passBtnsTapped 也是將 passBtns 這個 Outlet Collection 集中動作;deleteBtnTapped 則是在 enterNum 不為空時退一位,接著再去改動 changeImage()。
但是!changeImage() 竟然裡面擺了 checkPass(),所以仍有待改進的地方。
繼續閱讀|回目錄
流程檢查
最後兩個 IBAction,其實 deleteBtnTapped 裡不需 checkPass,因為在你按下 delete 的情況下是 enterNum ≤ 3 的情況下才會發生,當 enterNum == 4 的時候就會進到 checkPass 的程序。
passBtnsTapped
跟 deleteBtnTapped
是兩個關鍵 function,流程如下:
passBtnsTapped:
按下數字按鍵 => 更改 Passcode 欄位 => 輸入完做 checkPass => 通過回該回的頁面;不通過彈窗警示並清空 Passcode 欄位。
deleteBtnTapped:
在輸入期間按下 delete => 更改 Passcode 欄位。
我們希望 changeImage 與 checkPass 不要擺在一起,因為只有 enterNum == 4 的時候才 checkPass,我們又希望在輸入完第四個就 checkPass,所以應該將 checkPass 擺在 passBtnsTapped
裡。
同時在 deleteBtnTapped
裡,不需要 checkPass,只需要 changeImage,既然如此 changeImage 裡面就擺該有的邏輯即可。
最後這兩個 IBAction 及 changeImage 就會長這樣:
private func changeImage() {
//let count = enterNum.count
for i in 0...3 {
passImageViews[i].isHighlighted = i < enterNum.count
}
//if count == 4 {
// checkPass()
//}
}
@IBAction func passBtnsTapped(_ sender: UIButton) {
if enterNum.count != 4, let title = sender.currentTitle {
enterNum += title
}
changeImage()
if enterNum.count == 4 {
checkPass()
}
}
@IBAction func deleteBtnTapped(_ sender: Any) {
if enterNum.count != 0 {
enterNum.removeLast()
}
changeImage()
}
如此,我們就把程式碼拆分成,更改 Data、由 Data 去判斷 UI、由 Data 判斷最終流程及重設 UI。最終形成一個迴圈。
checkPass:由 Data 判斷去處,重設 UI(reset)。
reset:重設 UI 及 Data。
changeImage:設定 UI。
passBtnsTapped:改變 Data,設定 UI(changeImage),由 Data 判斷最終去處(checkPass)。
deleteBtnTapped:改變 Data,設定 UI。
完成了!
這次就分享到這,感謝您的閱讀。
繼續閱讀|回目錄
附上 Reference:
附上 GitHub: