iOS App 程式開發

iOS 開發者指南:透過 Swift 4 學習 Delegates 與 Delegation

iOS 開發者指南:透過 Swift 4 學習 Delegates 與 Delegation
iOS 開發者指南:透過 Swift 4 學習 Delegates 與 Delegation
In: iOS App 程式開發, Swift 程式語言

本文主要講述 “delegates” 與 “delegation”。利用本文提供的完整專案源碼,我們將做一個簡單示例,在 Swift 4 中實現 delegation 設計模式。我將展示 delegation 的操作方式,讓你不再在複雜的專案中陷入困境。為了讓你成為頂尖開發者,我將會介紹最棒的設計工具 UML,以協助物件導向軟件開發。文中所展示的 UML 圖,設計並記錄了專案範例中使用 delegation 設計模式的實作。

我將展示如何構建一個 user interface (UI) helper,這是一個在指定 URL 下載文件的類別。最重要的是,文中會展示在圖像文件已完成下載後,UI helper 如何通過 delegation 通知 UIViewController 子類別,讓視圖控制器可在螢幕上顯示圖像。為了簡單明瞭,我們假設 Swift 對於從 URL 下載檔案提供最小支持。我們將使用 delegation 設計模式手動連接檔案已完成下載的通知。以下是我們將構建的應用程式:

Swift Delegate Demo

推薦閱讀

為了解我如何創建文中的 delegation 範例程式碼,建議你閱讀以下文章:

Delegation

讓我們先了解術語的定義。

劍橋字典 (Cambridge dictionary) 定義的 “delegate” 是表示「將特定的工作、職責、權利等給予別人,讓他們為你完成。」韋伯字典 (Merriam-Webster dictionary) 定義的 “delegation” 是指「授權給另一方行事的行為」。

簡而言之,「Delegation 是一種設計模式,使 class 和 structure 能夠將其某些職責交給(或委託)其他類型的物件。」

你們都可能使用過 delegation,它很常在 iOS 的應用程式中出現。

思考一下,一個 UIViewController 的子類別 (命名為 ViewController) 來管理 UICollectionViewViewController 包含一個 UICollectionView 物件 (命名為 collectionView),你將 collectionView 實例的 delegate 屬性設為 selfself 就是 ViewController

ViewController 採用 UICollectionViewDelegate 的 protocol (協議),delegate 就是 ViewController。為了符合 UICollectionViewDelegate 的 protocol,ViewController 實現像 collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) 的方法。

通過實現 didSelectItemAt 方法,UICollectionViewCell 物件被點擊時,ViewController 就會得到通知。當一個 cell 被點擊時,ViewController 實現的 didSelectItemAt 方法將被調用。你可以定義 didSelectItemAt 方法的實作內容,舉例來說,在點擊某個 cell 時,可以改變該 cell 的外觀,並執行一些特定的邏輯,collectionView 將處理點擊UICollectionViewCell 物件的責任委託ViewController

以下是 delegation 和 UICollectionViewDelegate 的範例 (點擊連接)。

引述 Apple 官方說明:

Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object–the delegate–and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object. …

以及:

A delegate is an object that acts on behalf of, or in coordination with, another object when that object encounters an event in a program. The delegating object is often a responder object–that is, an object inheriting from NSResponder in AppKit or UIResponder in UIKit–that is responding to a user event. The delegate is an object that is delegated control of the user interface for that event, or is at least asked to interpret the event in an application-specific manner. …

The programming mechanism of delegation gives objects a chance to coordinate their appearance and state with changes occurring elsewhere in a program, changes usually brought about by user actions. More importantly, delegation makes it possible for one object to alter the behavior of another object without the need to inherit from it. The delegate is almost always one of your custom objects, and by definition it incorporates application-specific logic that the generic and delegating object cannot possibly know itself. …

必要條件:protocol

為了編寫實現 delegation 設計模式的範例代碼,我需要一個 “protocol”。請參閱我這篇關於 protocol 的文章,並記住如蘋果官方所說(粗體為重點補充):

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. This design pattern is implemented by defining a protocol that encapsulates the delegated responsibilities, such that a conforming type (known as a delegate) is guaranteed to provide the functionality that has been delegated. Delegation can be used to respond to a particular action, or to retrieve data from an external source without needing to know the underlying type of that source.

蘋果官方解釋:

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol. …

程式碼講解

請注意:下文將用到的一些非常重要的術語詞彙與定義,如有需要,可參閱前文的段落以及引述的解釋。

委託對象 (the delegating object)

我將構建一個實作圖片下載器的類別 LogoDownloader,這個類別只要有一個特定的 URL 就可以異步下載檔案,並在下載完成時提供通知。這就是「委託對象」,如上文定義,即「保留對其他物件——代理對象——的引用,並在適當的時候向其發送消息。」

委託協議 (the delegate protocol)

一個 Protocol 是需要被定義的。請記住,「delegation 設計模式是通過定義一個封裝了委託職責的 protocol 來實現的,一個符合的類型(被稱為 delegate)就能保證提供被委託的功能」(粗體為重點補充)。它將是 LogoDownloaderDelegate 的 protocol。

代理對象 (the delegate)

我將實作一個繼承 UIViewController 的子類,並使其符合 LogoDownloaderDelegate delegate protocol (委託協議)。請記住,delegation 設計模式是通過定義一個封裝委託職責的 protocol 來實現的,符合的類型(稱為 delegate)能夠提供已被委託的功能,我將調用這個類別 ViewController

UML:這些角色 (Piece) 如何融合在一起

在進入專案程式碼之前,我繪製了一個 UML 圖。在設計物件導向軟體開發方向,UML 是我用過最好的工具。看一下我的圖表及參考備註,並閱讀下列 UML 的參考資料:連結一連結二連結三,以及連結四

UML Diagram

Swift Delegate Diagram

Code demo

這就是範例應用程式所呈現的畫面:

Swift Delegate Demo

編寫程式碼

委託對象 (the delegating object)

委託對象 LogoDownloader 可以下載(圖片)檔案,讓我們看一下它的程式碼。我對 delegate 物件(LogoDownloaderDelegate 類型)採取 optional 引用,允許 LogoDownloader 類別實例在沒有 delegate 時也能進行操作,如果 delegate 成員變數是 nilLogoDownloader 仍會下載檔案並調用自己的 didDownloadImage() 方法。

var delegate:LogoDownloaderDelegate?

以下是 LogoDownloader.swift 的程式碼——注意 optional(?) 的 “delegate” 屬性,並留意 delegate 變數向哪裡發送 delegate?.didFinishDownloading(self) 消息:

import UIKit

//
// This is the DELEGATING CLASS. An instance of this
// class is a DELEGATING OBJECT.
//
class LogoDownloader {
    
    var logoURL:String
    
    var image:UIImage?
    
    // weak var delegate:LogoDownloaderDelegate?
    // SEE NOTE BELOW
    var delegate: LogoDownloaderDelegate?
    
    init(logoURL:String) {
        self.logoURL = logoURL
    }
    
    func downloadLogo() {
        // Start the image download task asynchronously by submitting
        // it to the default background queue; this task is submitted
        // and DispatchQueue.global returns immediately.
        DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
    
            // I'm PURPOSEFULLY downloading the image using a synchronous call
            // (NSData), but I'm doing so in the BACKGROUND.
            let imageURL = URL(string: self.logoURL)
            let imageData = NSData(contentsOf: imageURL!)
            self.image = UIImage(data: imageData! as Data)
            print("image downloaded")
    
            // Once the image finishes downloading, I jump onto the MAIN
            // THREAD TO UPDATE THE UI.
            DispatchQueue.main.async {
                // Tell the delegate that the image
                // has downloaded so the delegate can
                // display the image.
                self.didDownloadImage()
            }
    
        } // end DispatchQueue.global
    }
    
    // Since this class has a reference to the delegate,
    // "at the appropriate time [it] sends a message to" the delegate.
    // Finishing the logo download is definitely
    // the appropriate time.
    func didDownloadImage() {
        delegate?.didFinishDownloading(self)
    }
    
} // end class LogoDownloader

請注意: 從技術上講,我應該將 delegate 的宣告標記為:

weak var delegate:LogoDownloaderDelegate?

藉此來避免 retain cycles,注意,這裡的 LogoDownloaderDelegate 只是一個 Protocol。雖然理解 delegate 本就是相當困難的,在試圖理解 delegate 時,我不想讓你更加困惑,但我不得不在此介紹另一個概念。

如果我將 delegate 宣告為 weak,將會收到錯誤消息,“‘weak’ may only be applied to class and class-bound protocol types, not ‘LogoDownloaderDelegate’”。

你可以在我的這篇教程 “Tutorial: delegates and delegation in Objective-C” 中看到,在使用 Objective-C 開發時,宣告為 weak delegate 的限制相對寬容。

委託協議 (the delegate protocol)

LogoDownloaderDelegate 是 delegate protocol (委託協議),你可以將 protocol 的概念化為契約 (contract)承諾 (promise),可應用於 class、structure 或 enumeration。下面我會將 ViewController 帶入一個 LogoDownloaderDelegate 協議的 contract 中,ViewController承諾實現這個契約,實作 LogoDownloaderDelegate 要求實體或履行的內指定方法與成員變數。

以下為 LogoDownloaderDelegate.swift 程式碼:

import Foundation

import UIKit

//
// This is the DELEGATE PROTOCOL
//
protocol LogoDownloaderDelegate {
    // Classes that adopt this protocol MUST define
    // this method -- and hopefully do something in
    // that definition.
    func didFinishDownloading(_ sender:LogoDownloader)
}
練習:

我為何要在 didFinishDownloading(_) 宣告函式中指定對 LogoDownloader 物件的引用?

代理對象 (the delegate)

我的 ViewController 接受——符合——LogoDownloaderDelegate 這個委託協議,注意:

1) 它有一個 optional (?)LogoDownloader 實例 (var logoDownloader:LogoDownloader?);

2) 我使用 optional chaining (可選鏈),「Optional Chaining 是一個請求和呼叫屬性、方法及子腳本的過程,它的可選性當前可能為空 (nil)。如果可選的目標有數值,就會成功呼叫屬性、方法或子腳本;相反,如果選擇的目標為空 (nil),呼叫將回傳空 (nil)。多次請求或呼叫可以被鏈接形成一個鏈,如果任何一個節點為空 (nil) 將導致整個鏈失效。」

3) 它將 LogoDownloader 實例的成員變量 delegate 設置為self

4) 它提供 LogoDownloader自定義操作,允許我在呼叫這些協議方法時執行任何操作,例如完成下載時驅動圖像顯示。

以下為 ViewController.swift 程式碼:

import UIKit

// NOTE: I set auto layout constraints for the view controller
// in the storyboard corresponding to this ViewController
// for "View as: iPhone SE."

//
// This is the DELEGATE
//
class ViewController: UIViewController, LogoDownloaderDelegate {
    
    @IBOutlet weak var loadingLabel: UILabel!
    @IBOutlet weak var loginView: UIView!
    @IBOutlet weak var imageView: UIImageView!
    
    var logoDownloader:LogoDownloader?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        // Initially, the image view is hidden so we can fade it in with animation.
        imageView.alpha = 0.0
        
        // Initially, the login area, with username and password, are hidden
        // until the logo image downloads, and then we fade it in.
        loginView.alpha = 0.0
        
        // NASA images used pursuant to https://www.nasa.gov/multimedia/guidelines/index.html
        let imageURL: String = "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic1509a.jpg"
        
        // Construct a LogoDownloader to download the NASA file.
        logoDownloader = LogoDownloader(logoURL: imageURL)
        // Set a reference in the delegating object, LogoDownloader, to
        // this class, ViewController. ViewController is the delegate.
        // LogoDownloader tells ViewController that the image at the NASA
        // URL has downloaded by calling the delegate method
        // didFinishDownloading(_).
        logoDownloader?.delegate = self // try nil here
        // Start the logo image download and get informed when it
        // finished downloading when didFinishDownloading(_) is called.
        logoDownloader?.downloadLogo()
        
        // Since the delegating object, LogoDownloader, has an optional
        // reference to this class, ViewController, that reference can
        // be nil, and since we use optional chaining, ViewController
        // can run with or without the delegating object.
        if logoDownloader?.delegate == nil {
            loginView.alpha = 1.0
        }
    } // end func viewDidLoad()
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    // Defining this method makes ViewController conform/adopt
    // the LogoDownloaderDelegate protocol. This method is called
    // when the logo image finished downloading.
    func didFinishDownloading(_ sender: LogoDownloader) {
        imageView.image = logoDownloader?.image
        
        // Animate the appearance of this ViewController's
        // user interface.
        UIView.animate(withDuration: 2.0, delay: 0.5, options: UIViewAnimationOptions.curveEaseIn, animations:  {
            self.loadingLabel.alpha = 0.0
            self.imageView.alpha = 1.0
        }) { (completed:Bool) in
            if (completed) {
                UIView.animate(withDuration: 2.0) {
                    self.loginView.alpha = 1.0
                }
            }
        }
    } // end func didFinishDownloading
    
} // end class ViewController

總結

希望你能夠徹底閱讀和消化關於 delegation 的初步討論,可參考文中分享的連結,並閱讀推薦文章。請注意,文章沒有解釋每一個細節,在查看程式碼時,希望你多作思考。如果讀者不了解某個概念,請在參考資料或其他資訊中查找相關內容,編寫出你自己的 delegation code。

最重要的是,不斷練習,取得經驗——並請享受整個過程!

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文Understanding Delegates and Delegation in Swift 4

作者
Andrew Jaffee
熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。