一、簡述
在使用swift語言進行iOS應用程序開發的過程中,我們會經常接觸到“Delegate”這個概念。爲了更好地理解這個概念,我們以文本輸入框組件UITextField爲例。
二、概念
//*******************************************************************************************//
//* 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(_:)”方法
//*******************************************************************************************//
從這段說明我們就能夠非常容易理解swift中的delegate的概念了,簡單地說,使用delegate能夠捕獲到用戶在設備上所觸發的各種事件,並根據我們自己所編寫的代碼來響應這些事件。
三、示例
四、代碼及實現
4.1算法簡述
/**
* 因爲本界面上的TextField數量過多,當用戶在TextField中進行輸入時,界面下方
* 彈出來的虛擬鍵盤會遮擋掉一部分的TextField,影響用戶的正常使用
* 爲此,每當用戶在某一個TextField中進行文字輸入時,都需要將這個TextField移動到頁面的頂端,確保不被鍵盤遮擋
*
* 我所採用的方式,是整體將所有的TextField整體往上移動。
*
* 移動的方法說明如下:
*
* 在界面上,所有的TextField都是以約束形式來固定位置的
* 每一個TextField的y軸的位置,都取決於它和在它上方的TextField之間間距多少距離
* 也就是每一個TextField的y軸的位置實際上都取決於與它緊鄰的上方的那個TextField
* 而最上方的一個TextField的y軸的位置,則取決於它和界面頂部距離多少
*
* 所以,想要改變任何一個TextField的y軸的位置,其實都只要改變最上方的TextField的y軸的位置就可以了
*
*/
4.2繼承代理事件
textFieldShouldBeginEditing(_ textField: UITextField) -> Bool
textFieldShouldReturn(_ textField: UITextField) -> Bool
從這兩個方法的名字我們就可以看出,當用戶點擊某個文本輸入框的時候,iOS就會自動調用到第一個方法。而當用戶完成文字輸入,並且點擊鍵盤上的回車鍵的時候,iOS就會自動調用到第二個方法。4.3定義屬性
/*
* 這個變量表示的是界面上用於放置所有組件的最大的根容器
*/
var viewMainContent: UIView!
/*
* 這個變量表示的是界面上的最上方第一個組件的佈局約束:距離整個界面頂端的間距
*/
@IBOutlet weak var layoutConstraintTop: NSLayoutConstraint!
/*
* 這個變量是一個數組,用來存放界面上所有的縱向排列的界面組件
* 數組中的每一個元素就代表一行組件
*/
var viewArr:[UIView] = [UIView]()
/*
* 界面上的每一個TextField的淨高度
*/
let heightOfTextField:CGFloat = 30.0
/*
* 界面上的上下相鄰的兩個TextField之間的間距
*/
let spaceBetweenTextField:CGFloat = 8.0
4.4初始化所有的文本輸入框
/*
* 獲取界面上用於放置所有組件的最大的根容器
*/
self.viewMainContent = self.view
/*
* 獲取界面上的所有縱向排列的界面組件
*/
self.viewArr = self.viewMainContent.subviews
4.5爲所有文本輸入框設置代理
這裏我單獨封裝了一個方法:/**
* 爲當前界面上所有文本輸入框註冊事件代理
*/
func initTextFieldDelegate(){
for viewObj in self.viewArr{
if viewObj.isKind(of: UITextField.self){
let textFieldObj = viewObj as! UITextField
textFieldObj.delegate = self
}
}
}
然後在viewDidLoad的時候調用這個方法:self.initTextFieldDelegate()
4.6編寫方法,實現移動文本輸入框
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))
}
第二個方法是將文本輸入框的位置復原:
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)
}
4.7編寫方法,隱藏或顯示所有其它的文本輸入框
/**
* 對於某一個指定的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
}
}
}
4.8捕獲事件
第一步,當用戶在某個文本輸入框中錄入文字的時候,捕獲這一事件,我們需要實現以下方法:func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool{
/*
* 將正在進行文本輸入操作的文本框移動到界面的最頂端
* 從而避免虛擬鍵盤將文本輸入框遮擋住
*/
self.resetY(viewObj: textField)
/*
* 隱藏目前正在接受文字輸入的文本輸入框下方的所有其它文本輸入框
*/
self.hideTextFieldUnder(viewObj: textField)
return true
}
而當用戶完成輸入,點擊鍵盤上的回車按鈕的時候,我們也要捕獲這一事件,此時需要實現以下方法:
func textFieldShouldReturn(_ textField: UITextField) -> Bool{
/*
* 完成在文本輸入框中的文字輸入操作後,需要將這個文本輸入框從事件響應隊列中移除
*/
textField.resignFirstResponder();
/*
* 完成在文本輸入框中的文字輸入操作後,需要將文本輸入框的位置歸位
*/
self.reloadY(viewObj: textField)
/*
* 顯示所有下方的文本輸入框
*/
self.showTextFieldUnder(viewObj: textField)
return true;
}
好了,以上就是一個關於文本輸入框代理事件的最簡單的例子。
//
// 關於文本輸入框的事件代理,摘錄蘋果開發者中心的官方解釋如下:
//*******************************************************************************************//
//* 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
}
}
}
}