#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 跟流程圖:

參照下面這篇文章,我們了解到 IBOutletOutlet 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 清空,讓它可以重新加入。

delete

@IBAction func deleteBtnTapped(_ sender: Any) {
if enterNum.count != 0 {
enterNum.removeLast()
}
changeImage()
print(enterNum)
}

在這個 IBAction 裡,如果 enterNum 不為空,我們呼叫 changeImage(),重新判斷 enterNum 的位數,給予顯示恰當的 UI 呈現。

繼續閱讀|回目錄

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 的程序。

passBtnsTappeddeleteBtnTapped 是兩個關鍵 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:

--

--

Chun-Li 春麗
彼得潘的 Swift iOS / Flutter App 開發教室

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.