1. 程式人生 > >基於Swift的iOS應用程式開發:通過UITextFieldDelegate快速理解Delegate事件代理

基於Swift的iOS應用程式開發:通過UITextFieldDelegate快速理解Delegate事件代理

//
//  關於文字輸入框的事件代理,摘錄蘋果開發者中心的官方解釋如下:
//*******************************************************************************************//
//*  To understand when these methods get called and what they need to do,                  *//
//*  it’s important to know how text fields respond to user events.                         *//
//*  When the user taps a text field, it automatically becomes the first responder.         *//
//*  In an app, the first responder is an object                                            *//
//*  that is first on the line for receiving many kinds of app events,                      *//
//*  including key events,motion events, and action messages, among others.                 *//
//*  In other words, many of the events generated by the user are initially routed          *//
//*  to the first responder.                                                                *//
//*                                                                                         *//
//*  As a result of the text field becoming the first responder,                            *//
//*  iOS displays the keyboard and begins an editing session for that text field.           *//
//*  What a user types using that keyboard gets inserted into the text field.               *//
//*                                                                                         *//
//*  When a user wants to finish editing the text field,                                    *//
//*  the text field needs to resign its first-responder status.                             *//
//*  Because the text field will no longer be the active object in the app,                 *//
//*  events need to get routed to a more appropriate object.                                *//
//*                                                                                         *//
//*  This is where your implementation of UITextFieldDelegate methods comes in.             *//
//*  You need to specify that the text field should resign its first-responder status       *//
//*  when the user taps a button to end editing in the text field.                          *//
//*  You do this in the textFieldShouldReturn(_:) method,                                   *//
//*  which gets called when the user taps Return (or in this case, Done) on the keyboard.   *//
//*******************************************************************************************//
//  簡單地翻譯如下:
//*******************************************************************************************//
//*     為了能夠理解文字輸入框的代理事件會在什麼時候被呼叫,以及它們需要做哪些事情,
//*     我們必須首先理解文字輸入框是如何來響應使用者所觸發的事件的,這非常重要。
//*     當用戶輕輕地點選了一個文字輸入框,它就會自動成為一個“第一響應”(原文為“the first responder”)。
//*     在一個app中,第一響應會(比其它介面元件)更早接收到app事件,
//*     包括鍵盤輸入事件、滑動事件、訊息、以及其它一些事件。
//*     換句話說,使用者所觸發的很多事件,都會被轉發到第一響應那兒去。
//*     
//*     當一個文字輸入框成為了第一響應以後,iOS就會將鍵盤顯示出來,並且開啟文字輸入會話。
//*     當用戶用這個鍵盤進行輸入的時候,文字就會顯示在作為第一響應的那個文字輸入框中。
//*
//*     當用戶希望結束文字輸入的時候,文字輸入框就必須放棄第一響應狀態。
//*     因為此時文字輸入框將不再是app中的“活躍物件”(原文為“active object”),
//*     而(使用者所觸發的)事件也必須被轉發到其它(更適合的)元件物件上去。
//*
//*     這就是為什麼你需要繼承“UITextFieldDelegate”這個代理事件,並實現它的方法。
//*     當用戶輕輕敲擊了某個按鈕,來結束文字輸入的時候,你必須非常明確地讓文字輸入框放棄第一響應狀態。
//*     你可以實現方法“textFieldShouldReturn(_:)”來達到這一目的,
//*     這個方法會在使用者點選了鍵盤上的“回車”鍵(回車鍵也可能會被標記成“完成”、“下一個”等等)時被呼叫。
//*
//*     以下為補漏:
//*     如同其它的一些代理事件一樣,如果某個文字輸入框被使用者點選,成為了第一響應之後,
//*     你希望你的程式能夠立刻能夠處理一些業務工作,那麼你可以實現“textFieldShouldBeginEditing(_:)”方法
//*******************************************************************************************//
//
//  DemoTextFieldDelegateViewController.swift
//  MyDemo
//
//  Created by 徐威男 on 2017/8/3.
//  Copyright © 2017年 freezingxu. All rights reserved.
//

import UIKit

class DemoTextFieldDelegateViewController: UIViewController,UITextFieldDelegate {
    //MARK:屬性
    
    /*
     *  這個變量表示的是介面上用於放置所有元件的最大的根容器
     */
    var viewMainContent: UIView!
    
    /*
     *  這個變量表示的是介面上的最上方第一個元件的佈局約束:距離整個介面頂端的間距
     */
    @IBOutlet weak var layoutConstraintTop: NSLayoutConstraint!

    /*
     *  這個變數是一個數組,用來存放介面上所有的縱向排列的介面元件
     *  陣列中的每一個元素就代表一行元件
     */
    var viewArr:[UIView] = [UIView]()
    
    /*
     *  介面上的每一個TextField的淨高度
     */
    let heightOfTextField:CGFloat = 30.0
    
    /*
     *  介面上的上下相鄰的兩個TextField之間的間距
     */
    let spaceBetweenTextField:CGFloat = 8.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /*
         *  獲取介面上用於放置所有元件的最大的根容器
         */
        self.viewMainContent = self.view
        
        /*
         *  獲取介面上的所有縱向排列的介面元件
         */
        self.viewArr = self.viewMainContent.subviews
        
        /*
         *  為介面上的所有文字輸入框註冊事件
         */
        self.initTextFieldDelegate()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    }
    */

    /**
     *  當用戶點選某一個文字輸入框,希望開始輸入文字的時候,該方法會被立刻呼叫到
     *  系統會將這個被點選的輸入框放在事件響應器(responder)佇列的第一位
     *  即讓這個文字輸入框能夠響應所有使用者的螢幕操作,包括在虛擬鍵盤上輸入文字
     *
     *  由於當前介面上的文字輸入框數量較多,觸發手機的虛擬鍵盤進行輸入操作時,螢幕上彈出的虛擬鍵盤會遮擋住一部分介面
     *  這對使用者操作相當不友好
     *  所以當用戶點選某個文字輸入框的時候,要讓當前正在進行輸入操作的文字輸入框能夠顯示在螢幕的最上方
     *  等到完成輸入(即點選了虛擬鍵盤上的“完成(也可能是“下一步”等)”按鈕)的時候,再將它歸位
     *
     *  除了移動當前的文字輸入框的位置之外,為了避免有些使用者可能會搞不清楚狀況
     *  還需要將當前文字輸入框下方的所有其它輸入框全部都隱藏起來
     *  這樣以來,當用戶在進行文字輸入的時候,就保證介面上只有一個文字輸入框了
     *
     *  蘋果開發者中心官方對TextFiled元件的這些監聽事件的解釋摘錄詳見本類的頭部註釋
     */
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool{
        /*
         *  將正在進行文字輸入操作的文字框移動到介面的最頂端
         *  從而避免虛擬鍵盤將文字輸入框遮擋住
         */
        self.resetY(viewObj: textField)
        
        /*
         *  隱藏目前正在接受文字輸入的文字輸入框下方的所有其它文字輸入框
         */
        self.hideTextFieldUnder(viewObj: textField)
        
        return true
    }
    
    /**
     *
     *  當用戶完成在一個文字輸入框中的輸入後,點選虛擬鍵盤上的“完成(也可能是“下一步”等)”按鈕,則需要移除這個文字輸入框的第一響應狀態
     *  這個方法就是用來處理使用者點選虛擬鍵盤上的“完成(也可能是“下一步”等)”按鈕時的業務邏輯
     *
     *  蘋果開發者中心官方對TextFiled元件的這些監聽事件的解釋摘錄詳見本類的頭部註釋
     *
     */
    func textFieldShouldReturn(_ textField: UITextField) -> Bool{
        /*
         *  完成在文字輸入框中的文字輸入操作後,需要將這個文字輸入框從事件響應佇列中移除
         */
        textField.resignFirstResponder();
        
        /*
         *  完成在文字輸入框中的文字輸入操作後,需要將文字輸入框的位置歸位
         */
        self.reloadY(viewObj: textField)
        
        /*
         *  顯示所有下方的文字輸入框
         */
        self.showTextFieldUnder(viewObj: textField)
        
        return true;
    }
    
    //MARK:方法------------------------------
    /**
     *  為當前介面上所有文字輸入框註冊事件代理
     */
    func initTextFieldDelegate(){
        for viewObj in self.viewArr{
            if viewObj.isKind(of: UITextField.self){
                let textFieldObj = viewObj as! UITextField
                textFieldObj.delegate = self
            }
        }
    }
    
    /**
     *  因為本介面上的TextField數量過多,當用戶在TextField中進行輸入時,介面下方
     *  彈出來的虛擬鍵盤會遮擋掉一部分的TextField,影響使用者的正常使用
     *  為此,每當使用者在某一個TextField中進行文字輸入時,都需要將這個TextField移動到頁面的頂端,確保不被鍵盤遮擋
     *
     *  我所採用的方式,是整體將所有的TextField整體往上移動。
     *
     *  移動的方法說明如下:
     *
     *  在介面上,所有的TextField都是以約束形式來固定位置的
     *  每一個TextField的y軸的位置,都取決於它和在它上方的TextField之間間距多少距離
     *  也就是每一個TextField的y軸的位置實際上都取決於與它緊鄰的上方的那個TextField
     *  而最上方的一個TextField的y軸的位置,則取決於它和介面頂部距離多少
     *
     *  所以,想要改變任何一個TextField的y軸的位置,其實都只要改變最上方的TextField的y軸的位置就可以了
     *
     *  入參說明:
     *  textField:UITextField   使用者當前正在進行文字輸入操作的TextField
     */
    func resetY(viewObj:UIView){
        /*
         *  計算TextFiled在y軸方向上的單位移動距離
         *
         *  介面上的每一個TextView元件,它與和它相鄰的、位於它上方的元件的間距,加上它自己的淨高度
         *  就是TextView在y軸方向移動時的一個單位高度
         */
        let distanceOfY:CGFloat = spaceBetweenTextField + heightOfTextField
        
        /*
         *  獲取當前使用者正在進行文字輸入操作的TextField,並計算這是第幾個TextField
         */
        let index:CGFloat = CGFloat(self.viewArr.index(of: viewObj)!)
        
        /*
         *  將第一個TextField整體往y軸的上方進行移動(即y值變小)
         *  這樣以來所有的TextField都會跟著一起移動
         *
         *  注意實際操作的其實並不是最上方的第一個TextField元件
         *  而是它與頁面頂端之間的“佈局約束”物件
         */
        self.layoutConstraintTop.constant.add(0 - (index * distanceOfY))
    }
    
    /**
     *  當用戶完成在一個TextField中進行輸入後,需要將TextField從頁面最頂端的位置恢復到它原本所在的位置
     *  恢復的演算法請詳見本類中的方法“resetY(textField:UITextField)”
     *  即“怎麼修改的,就怎麼恢復”
     */
    func reloadY(viewObj:UIView){
        /*
         *  計算TextFiled在y軸方向上的單位移動距離
         *
         *  介面上的每一個TextView元件,它與和它相鄰的、位於它上方的元件的間距,加上它自己的淨高度
         *  就是TextView在y軸方向移動時的一個單位高度
         */
        let distanceOfY:CGFloat = spaceBetweenTextField + heightOfTextField
        
        /*
         *  獲取當前使用者正在進行文字輸入操作的TextField,並計算這是第幾個TextField
         */
        let index:CGFloat = CGFloat(self.viewArr.index(of: viewObj)!)
        
        /*
         *  將第一個TextField整體往y軸的下方進行移動(即y值變大)
         *  這樣以來所有的TextField都會跟著一起移動
         *
         *  注意實際操作的其實並不是最上方的第一個TextField元件
         *  而是它與頁面頂端之間的“佈局約束”物件
         */
        self.layoutConstraintTop.constant.add(index * distanceOfY)
    }
    
    /**
     *  對於某一個指定的TextField,將所有在它下方的其它TextField都隱藏起來
     */
    func hideTextFieldUnder(viewObj:UIView){
        let index:Int = self.viewArr.index(of: viewObj)!
        for tf in self.viewArr{
            if self.viewArr.index(of: tf)! > index{
                tf.isHidden = true
            }
        }
    }
    
    /**
     *  對於某一個指定的TextField,將所有在它下方的其它TextField全部都顯示出來
     *  其實有一個更簡單的方法,就是無腦地把所有的TextField都設定為“顯示”
     *  但是不那麼做是為了與本類的方法“hideTextFieldUnder(textField:TextField)”能夠呼應起來
     */
    func showTextFieldUnder(viewObj:UIView){
        let index:Int = self.viewArr.index(of: viewObj)!
        for tf in self.viewArr{
            if self.viewArr.index(of: tf)! > index{
                tf.isHidden = false
            }
        }
    }

}