[iOS]使用Swift解決鍵盤擋住輸入框問題

前言

iOS開發會遇到的問題中,鍵盤擋到輸入框這件事一定包括在內

網路上也提供了很多解決的方案

我自己評估了方便性、實作難易度、呈現效果後

提供給大家一個我自己比較喜歡的一種方式

範例(in Swift 5)

需要將想監聽的輸入框元件的 delegate 設定到本 ViewController


import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    
    /* 暫存輸入框元件 */
    var currentTextField: UITextField?
    /* 暫存 View 的範圍 */
    var rect: CGRect?
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        /* 開始輸入時,將輸入框實體儲存 */
        currentTextField = textField
    }
     
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /* 監聽 鍵盤顯示/隱藏 事件 */
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil)
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil)
        
        /* 將 View 原始範圍儲存 */
        rect = view.bounds
    }
    
    /* 這個地方寫法有問題,請看文章下方補充
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        /* 移除監聽 */
        NotificationCenter.default.removeObserver(
            self,
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.removeObserver(
            self,
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    */
     
    @objc func keyboardWillShow(note: NSNotification) {
        if currentTextField == nil {
            return
        }
        
        let userInfo = note.userInfo!
        /* 取得鍵盤尺寸 */
        let keyboard = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.size
        let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
        /* 取得焦點輸入框的位置 */
        let origin = (currentTextField?.frame.origin)!
        /* 取得焦點輸入框的高度 */
        let height = (currentTextField?.frame.size.height)!
        /* 計算輸入框最底部Y座標,原Y座標為上方位置,需要加上高度 */
        let targetY = origin.y + height
        /* 計算扣除鍵盤高度後的可視高度 */
        let visibleRectWithoutKeyboard = self.view.bounds.size.height - keyboard.height
        
        /* 如果輸入框Y座標在可視高度外,表示鍵盤已擋住輸入框 */
        if targetY >= visibleRectWithoutKeyboard {
            var rect = self.rect!
            /* 計算上移距離,若想要鍵盤貼齊輸入框底部,可將 + 5 部分移除 */
            rect.origin.y -= (targetY - visibleRectWithoutKeyboard) + 5
            
            UIView.animate(
                withDuration: duration,
                animations: { () -> Void in
                    self.view.frame = rect
                }
            )
        }
    }
     
    @objc func keyboardWillHide(note: NSNotification) {
        /* 鍵盤隱藏時將畫面下移回原樣 */
        let keyboardAnimationDetail = note.userInfo as! [String: AnyObject]
        let duration = TimeInterval(truncating: keyboardAnimationDetail[UIResponder.keyboardAnimationDurationUserInfoKey]! as! NSNumber)
        
        UIView.animate(
            withDuration: duration,
            animations: { () -> Void in
                self.view.frame = self.view.frame.offsetBy(dx: 0, dy: -self.view.frame.origin.y)
            }
        )
    }
}

補充

社團 iOS @ Taipei 內的開發者們提供一些建議與資訊

建議1

在 iOS 9 之後可以選擇不手動移除觀察者,原因在於會對觀察者採用 weak reference

與 unsafe unretained 的差異為若對象消滅了,unsafe unretained 並不會將內容改為nil

故有機會造成系統崩潰,詳細內容請參考官方文件

如此一來上面範例中的 viewDidDisappear 函式將可移除不使用

建議2

如需要手動移除,建議將註冊觀察者放至於 viewWillAppear 函式

移除則放至於 viewWillDisappear 函式內

原因在於 viewDidLoad 只在第一次加載 View 後調用

而 viewDidDisappear 則是 View 發生覆蓋、隱藏、消滅時調用,包括跳轉到其他頁面

這樣會造成跳轉時發生移除觀察者的行為,返回原畫面並不會重新註冊觀察者(不會再次執行 viewDidLoad)

效果

切換輸入框會重新計算一次高度

若這邊教學有幫助到你的話~請多多分享轉發出去給更多的人知道

謝謝大家的觀看

Read more

[LeetCode] #12 Integer to Roman 解題

題目連結 題型解說 這是一題難度為普通的題目 需要設計一個方法,此方法會傳入一個整數 num 要求是把整數轉換成羅馬字母,轉換清單如下 I => 1 V => 5 X => 10 L => 50 C => 100 D => 500 M => 1000 但羅馬字母有一些特殊規則 4 並非 IIII 而是 IV,9 並非 VIIII 而是 IX 這規則同樣可以套用到 40 90 400 900 解題思路 既然知道特殊規則是一樣的,變得是使用的符號,那麼先從 num 取個位數開始 轉換完成後,把 num 除上 10,消除個位數,

By Michael

[LeetCode] #11 Container With Most Water 解題

題目連結 題型解說 這是一題難度為中等的題目 需要設計一個方法,此方法會傳入一個數字陣列 height 陣列中的元素代表每一個柱子的高度 現在需要計算出,該陣列中以某兩隻柱子為邊界,最多可以裝多少水 以範例來說 height = [1,8,6,2,5,4,8,3,7] 最多可以裝 7 * 7 = 49 單位的水 解題思路 計算面積就是底乘上高 底的計算方式為 「右邊柱子的 index」 減去 「左邊柱子的 index」 高就取最短的那一根柱子高度 拿題目給的例子來當範例 建立三個變數 result、left、right left、right 代表左右兩邊的 index result 代表目前最大容量,初始值 0 第一步,找出最短的柱子高度,

By Michael

[LeetCode] #941 Valid Mountain Array 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個數字陣列 arr 判斷陣列中的元素是不是由低到高再從高到低(山形)的排序,且不連續一個以上數字 比如說 [1,2,3,2] 就是一個山形陣列,但 [1,2,2,3,2] 不是,因為有兩個 2 [1,2,3,4,5] 和 [5,4,3,2,1] 也不算是山形陣列,前者只有往上沒有往下,後者相反 解題思路 準備一個數字變數(temp)和布林變數(asc),跑一次迴圈,有可能遇到如下狀況 1. 某個數字與前一個數字相同,這時候直接回傳 false

By Michael

[LeetCode] #944 Delete Columns to Make Sorted 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個字串陣列 strs 這個陣列中每個字串的長度都相同,字串內容都是小寫英文 需要檢查每個元素的第 N 個字元是不是由小至大排列,並回傳有幾個錯誤排列 比如傳入的陣列長這樣 ["cba","daf","ghi"] 取第一個字元 = cdg 取第二個字元 = bah 取第三個字元 = afi 其中第二組的結果(bah)並不是由小至大排列,故回傳 1 解題思路 這一題就用兩個迴圈各別把字元取出來,並比較是否比上一個字元大(Java 中的字元可以直接比較),如果不是就將結果+1 程式碼 Java class Solution { public int minDeletionSize(String[] strs) { int result = 0; for (int i = 0,

By Michael