協議與委托代理回調在之前的博客中也是經常提到和用到的在《Objective-C中的委托(代理)模式》和《iOS開發之窺探UICollectionViewController(四) --一款功能強大的自定義瀑布流》等博客內容中都用到的Delegate回調。說到協議,在Objective-C中也是有協議的,並且Swift中的協議和Objc中的協議使用起來也是大同小異的,在Java等現代面向對象編程語言中有接口(Interface)的概念,其實和Swift中或者Objc中的Protocol(協議)是一個東西。論Interface和Protocol的功能來說,兩者也是大同小異的。
今天就結合兩個實例來窺探一下Swift中的協議與Delegate回調(委托代理回調)。本篇先給出CocoaTouch中常用控件UITableView的常用回調,並以此來認識一下回調的使用方式。緊接着會給出如何去實現自己的Delegate回調,即在自定義控件中去實現委托代理回調。言歸正傳,開始今天的博客主題。
一.從UITableView中來窺探協議的委托代理回調
UITableView這個高級控件在iOS開發中的出鏡率是比較高的,今天的重點不是介紹如何使用UITableView, 而是讓通過UITableView的工作方式來直觀的感受一下協議的使用場景,以及Delegate代理的工作方式。如果你對UITableView控件不熟的話,完全可以跳過這一部分,直接進入第二部分。如果你要更好的理解Delegate委托回調,還是很有必要看這一部分的。
下面就先以UITableView的UITableViewDatasource協議來看一下委托代理的使用方式。為了簡化代碼呢,下面的TableView的使用就沒有實現UITableViewDelegate協議還是那句話,今天的重點是Protocol和Delegate, 而不是如何使用UITableView。下方的截圖就是我們要使用UITableView和UITableViewDatasource來做的事情。當然下方的實例無論是代碼還是布局方面還是灰常簡單的,運行效果如下所示。
上面的Cell中就是一個ImageView和一個Label, 布局灰常簡單啦,接下來就簡單介紹一下在Swift中是如何實現(說白了,和Objc實現起來大同小異)。還是結合着Storyboard來做吧,畢竟使用Storyboard布局更為簡單一些。
1. 使用Storyboard來布局控件,控件布局如下:
2. 給上述Cell綁定相應的Swift源碼,並關聯ImageView和Label, 相應Cell(BeautifulGrillCell)的代碼如下所示。girlImageView即為做吧的圖片,
girlNameLable為圖片右邊的文字。
1 import UIKit 2 3 class BeautifulGrillCell: UITableViewCell { 4 5 @IBOutlet var girlImageView: UIImageView! 6 7 @IBOutlet var girlNameLable: UILabel! 8 9 override func awakeFromNib() { 10 super.awakeFromNib() 11 // Initialization code 12 } 13 14 override func setSelected(selected: Bool, animated: Bool) { 15 super.setSelected(selected, animated: animated) 16 17 // Configure the view for the selected state 18 } 19 20 }
3.接下來就是要模擬我們在TableView上顯示的數據了,在正常開放中這些數據往往來源於網絡請求,而在本篇博客中就模擬數據源,來為我們的TableView提供顯示的數據。數據源的格式是一個數組,而數組中存放的是多個字典,每個字典有兩個鍵值對,一個鍵值對存儲要顯示圖片的文件名,另一個鍵值對則存儲美女的名字。為了使該數據的存儲結構,請看下方結構圖。
原理圖有了,接下來就要使用代碼來創建出上述結構的數據以供TableView的數據源使用,下面的方法就是實現上述結構的函數。
(1) 首先我們要在視圖控制器相應的類中添加一個可變數組,用來存放數據,如下所示:
1 private var dataSource:Array<Dictionary<String, String>>?
(2) 接着就是往上面這個數組中填充數據了,代碼如下:
1 //-----------創建Table要顯示的數據------------------------- 2 func createSourceData() { 3 self.dataSource = Array<Dictionary<String, String>>(); 4 for (var i = 0; i<10; i++) { 5 let imageName:String = "00\(i).jpg" 6 let girlName:String = "美女\(i + 1)" 7 self.dataSource?.append([IMAGE_NAME:imageName, GIRL_NAME:girlName]) 8 } 9 }
4. 我們上面Storyboard中的視圖控制器使用的是UIViewController而不是UITableViewController。 我們在UIViewController上貼了一層UITableView, 所以我們需要在相應的ViewController對應的Swift源碼中進行UITableView的綁定,並實現UITableViewDatasource代理,並為UITableView指定該代理。下方的代碼就是關聯tableview並指定代理方法。代碼如下:
1 import UIKit 2 3 class ViewController: UIViewController, UITableViewDataSource { 4 5 @IBOutlet var myTableView: UITableView! 6 //life cycle 7 override func viewDidLoad() { 8 super.viewDidLoad() 9 self.createSourceData() 10 self.myTableView.dataSource = self 11 } 12 }
4. 對myTableView的dataSource(數據提供者)指定完代理對象后,接下來就是要實現UITableViewDataSource中的相應的方法了,ViewController通過這些協議委托回調的代理方法來為TableView提供數據。下方是UITableViewDataSource委托方法中返回TableView的Section個數的回調方法,如下所示:
1 /** 2 - parameter tableView: 當前要顯示的TableView 3 4 - returns: TableView中Section的個數 5 */ 6 func numberOfSectionsInTableView(tableView: UITableView) -> Int { 7 return 18 }
5.上面回調方法是返回Section個數的,緊接着下方就是返回每個Section中Cell個數的回調方法。Cell的個數就是數組dataSource中元素的個數。
1 /** 2 返回每個Section中的Cell個數 3 4 - parameter tableView: 當前顯示的TableView 5 - parameter section: 對應的Section 6 7 - returns: 對應Section中cell的個數 8 */ 9 func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ 10 return self.dataSource!.count
11 }
6. 下面這個方法是比較重要的,下方的方法,就是返回每行的Cell的委托回調方法。通過Cell的重用標示符來創建Cell的實例對象,並對Cell上的一些屬性賦值,並返回當前是Cell實例對象,代碼如下所示。
1 /** 2 返回要顯示的Cell 3 4 - parameter tableView: cell要顯示的TableView 5 - parameter indexPath: cell的索引信息 6 7 - returns: 返回要顯示的Cell對象 8 */ 9 func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 10 11 let cell:BeautifulGrillCell = self.myTableView.dequeueReusableCellWithIdentifier("BeautifulGrillCell", forIndexPath: indexPath) as! BeautifulGrillCell 12 13 let tempItem:Dictionary? = self.dataSource![indexPath.row] 14 15 if tempItem != nil { 16 let imageName:String = tempItem![IMAGE_NAME]! 17 cell.girlImageView.image = UIImage(named: imageName) 18 19 let girlName:String = tempItem![GIRL_NAME]! 20 cell.girlNameLable.text = girlName 21 } 22 23 return cell 24 } 25 }
經過上面這些步驟,你就可以去實現博客最上方截圖中的效果了,上面主要用到的還是TableView的UITableViewDatasource委托代理, 使用方法如上。上面使用的委托回調主要是使用Swift中的協議(Protocol)來實現的。那么如何使用協議來實現你自己的委托回調呢?這將是下面將要介紹的內容。
二. 認識協議,並使用協議實現委托回調
接下來的內容就要介紹如何使用協議來定義屬於你自己的委托代理回調(Delegate)了。第二部分還是以實例為准,在上面的Demo中加入我們自己定義的委托代理回調。我們需要做的就是,在上面界面中,我們點擊任意Cell就可以Push(導航控制器展示視圖控制器的一種方式,可以理解為視圖控制器壓棧的過程)到一個ViewController中,這個ViewController要做的事情就是輸入美女的名字,點擊返回后通過自己定義的委托回調,把你輸入的值回調到上一個頁面(TableView)中去,並修改相應Cell上的名字。說白了,就是對美女的名字做一個修改。
如果上面的文字讓你迷惑的話,那么接下來看實例好了,該實例還算是簡單的。下方是實例的操作步驟,如下所示:
上面實例的意思就是把下一個頁面的值通過委托代理回調的形式傳到上個頁面中去,在前面的博客《窺探Swift之函數與閉包的應用實例》中也做了同樣的事情,不過之前我們是使用閉包(Closure)回調來實現的。先在我們要通過Delegate來實現。接下來我們就定義協議,然后再協議的基礎上實現委托代理回調。接下來了開始我擴充的部分。
1.實現編輯美女姓名的頁面
(1) 在Storyboard上新添加一個視圖控制器(UIViewController), 並命名為EditViewController,給視圖控制器就是上方截圖中綠色的那個視圖控制器,主要用來對美女姓名 修改,並通過委托回調把值傳給上個頁面。該視圖控制器的頁面布局比較簡單,具體如下所示:
(2)UI就如數所示,為EditViewController關聯EditViewController.swift源文件后,再對其上面的使用到的控件進行關聯即可。緊接着我們要實現一個協議,這個協議我們用來所委托回調使用。這個協議可以定義在EditViewController.swift源文件中。在協議定義之前,先對什么是協議簡單的提上一嘴。先簡單的理解,協議中的方法只有聲明,沒有實現,並且使用protocol關鍵自進行聲明,下方的代碼就是我們要使用的協議。協議中有一個fetchGirlName(name:String)的方法,用來回調出輸入的數值。默認方法是必選的,你可以使用optional關鍵字使方法可選,在此就不做過多贅述了。
1 protocol EditViewControllerDelegate: NSObjectProtocol{ 2 func fetchGirlName(name:String) 3 }
(3) 接着要實現EditViewController類中的東西了,代碼如下。
成員變量var girlOldName:String?負責接收上個頁面傳過來的美女的姓名。weak var delegate: EditViewControllerDelegate? 這個聲明為weak的delegate成員變量則是必須要實現EditViewControllerDelegate協議的委托代理者,使用weak修飾為了避免強引用循環。接着是girlNameTextField就是關聯的輸入框了,負責接收用戶輸入,把值交付給委托代理者。
在viewWillDisappear方法中,會將用戶輸入的值交付給委托代理者的fetchGirlName方法。deinit是析構函數,用來觀察是否引起強引用循環,因為我們是使用的weak, 所以不會引起強引用循環,該deinit方法當返回時,是會被釋放掉的。
1 class EditViewController: UIViewController { 2 3 var girlOldName:String? 4 weak var delegate: EditViewControllerDelegate? 5 @IBOutlet var girlNameTextField: UITextField! 6 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 if self.girlOldName != nil { 11 self.girlNameTextField.text = self.girlOldName! 12 } 13 } 14 15 override func viewWillDisappear(animated: Bool) { 16 let name:String! = self.girlNameTextField.text 17 if name != "" { 18 if delegate != nil { 19 delegate!.fetchGirlName(name) 20 } 21 } 22 } 23 24 override func didReceiveMemoryWarning() { 25 super.didReceiveMemoryWarning() 26 } 27 28 deinit { 29 print("釋放") 30 } 31 }
2.上面的代碼是實現編輯頁面並實現相應的委托協議,下方就是要從之前TableView中進行跳轉。也就是點擊TableView的每一行,然后跳轉到編輯頁面對其當前點擊的cell進行編輯,編輯后返回通過代理進行值的修改。
(1)首先要解決的就是點擊Cell跳轉到EditViewController, 要執行這個事件,我們還必須實現TableView的另一個協議,就是UITableViewDelegate, 以為點擊Cell的事件獲取的方法就在TableViewDelegate中。所以我們要在TableView所在的ViewController中的viewDidLoad()中指定UITableViewDelegate的委托代理者。如下所示。同時該ViewContoller也要實現UITableViewDelegate協議。
1 self.myTableView.delegate = self
(2) 實現UITableViewDelegate協議中點擊Cell的方法,方法中的內容如下所示。在該方法中,首先我們要暫存一下點擊的是哪個Cell, 也就是記錄一下點擊Cell的IndexPath, 然后就是獲取點擊的Cell對象,因為通過該Cell對象,可以獲取相應Cell上的數據。具體的不多說了,請看代碼中的注釋。
1 //-----------UITableViewDelegate------------------ 2 func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 3 4 //記錄當前點擊的IndexPath 5 self.selectIndexPath = indexPath 6 7 //獲取當前點擊的Cell對象 8 let currentSelectCell:BeautifulGrillCell? = self.myTableView.cellForRowAtIndexPath(indexPath) as? BeautifulGrillCell 9 10 //從storyboard中實例化編輯視圖控制器 11 let editViewController:EditViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("EditViewController") as! EditViewController 12 13 //指定編輯視圖控制器委托代理對象 14 editViewController.delegate = self 15 16 //把點擊Cell上的值傳遞給編輯視圖控制器 17 if currentSelectCell != nil { 18 editViewController.girlOldName = currentSelectCell!.girlNameLable.text! 19 } 20 21 //push到編輯視圖控制器 22 self.navigationController?.pushViewController(editViewController, animated: true) 23 }
(3)上面是跳轉,接下來就是要實現EditViewControllerDelegate中的回調方法,來處理相應的回調參數了。下方就是在表視圖中實現的回調方法,具體請看代碼中的注釋:
1 //-----------EditViewControllerDelegate------------------ 2 3 func fetchGirlName(name: String) { 4 5 if selectIndexPath != nil { 6 //獲取當前點擊Cell的索引 7 let index = (selectIndexPath?.row)! 8 9 //更新數據源中相應的數據 10 self.dataSource![index][GIRL_NAME] = name 11 12 //重載TableView 13 self.myTableView.reloadData() 14 } 15 16 }
經過上面的步驟,我們就可以去定義屬於自己的協議,並在此協議上實現委托回調了。上面的場景在iOS開發中極為常見,使用場景也是比較廣泛的。所以協議無論在Swift還是在iOS開發中都是極為重要的概念之一。好今天的博客內容也挺多的了,就到此為止,剩下的東西,會在以后的博客中繼續更新。
上面實例GitHub分享地址(基於Xcode7.1):https://github.com/lizelu/SwiftDelegateDemo