Associated Objects trong Swift

Swift extensions cho phép chúng ta tuỳ biến rất cao trong việc thêm các hàm vào các class có sẵn, nhưng nó cũng có những hạn chế giống như categories trong Objective C: bạn không thể thêm được các stored property vào các class có sẵn thông qua extension. Associated Object chính là vị cứu tinh của chúng ta trong việc thêm stored property vào các class sẵn có.

1. Associated Objects là gì

Associated Objects hay còn được gọi là Associative References là 1 tính năng của ios runtime, tính năng này cho phép chúng ta thêm instance variable vào object mà không cần thay đổi code của object, nó sẽ tự bị remove khi object bị deallocated. Để sử dụng accociated object bạn cần import thêm vào class mà cần dùng <objc/runtime.h> Các key được sử dụng trong Associated Objects:

  • objc_setAssociatedObject
public func objc_setAssociatedObject(object: AnyObject!, _ key: UnsafePointer<Void>, _ value: AnyObject!, _ policy: objc_AssociationPolicy)
  • objc_getAssociatedObject
public func objc_getAssociatedObject(object: AnyObject!, _ key: UnsafePointer<Void>) -> AnyObject!
  • objc_removeAssociatedObjects
public func objc_removeAssociatedObjects(object: AnyObject!)
  • object: các kiểu object mà bạn muốn liên kết dữ liệu, ví dụ nếu dùng với UIViewController thì object chính là class UIViewController
  • key: khoá để liên kết dữ liệu với object, thường nó sẽ là kiểu địa chỉ bộ nhớ
  • value: giá trị mà bạn muốn liên kết với object.
  • objc_AssociationPolicy: OBJC_ASSOCIATION_ASSIGN: tham chiếu weak đến đến Associated Objects OBJC_ASSOCIATION_RETAIN_NONATOMIC: tham chiếu strong đến đến Associated Objects và kiểu liên kết ko phải atomically OBJC_ASSOCIATION_COPY_NONATOMIC : Associated Objects được copy và kiểu liên kết ko phải atomically OBJC_ASSOCIATION_RETAIN : tham chiếu strong đến đến Associated Objects và kiểu liên kết là atomically OBJC_ASSOCIATION_COPY : Associated Objects được copy và kiểu liên kết là atomically

2. Sử dụng Associated Objects

Gỉa dụ chúng ta có 1 list danh sách People:

Khi kéo 1 dòng sang trái sẽ chọn delete sẽ hiển thị 1 alert xác nhận xoá people tại dòng đó

Sau khi nhấn OK thì people đó sẽ bị xoá khỏi danh sách. Nếu làm theo cách truyền thống chúng ta sẽ tạo 1 indexPath lưu lại vị trí đã chọn people

var selectedIndexPath: NSIndexPath?

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            let alert = UIAlertView(title: "Delete People",
                                    message: "Are you sure you want to delete this people ?",
                                    delegate: self,
                                    cancelButtonTitle: "Cancel",
                                    otherButtonTitles: "OK")
            alert.show()
            self.selectedIndexPath = indexPath
        }
    }

và khi tiến hành delete, ta sẽ xoá phần từ trong mảng tương ứng với indexPath vừa lưu

func alertView(alertView: UIAlertView, didDismissWithButtonIndex buttonIndex: Int) {
        if buttonIndex == 1 {
            guard let indexPath = self.selectedIndexPath else {
                return
            }
            
            self.items.removeAtIndex(indexPath.row)
            self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .None)
            self.selectedIndexPath = nil
        }
    }

Vấn đề đã được giải quyết, nhưng tại sao chúng ta lại cần sử dụng thêm 1 biến để lưu giá trị mà thật ra trong class chỉ dùng đến dúng ở 1 hàm là là khi xoá people ? Và nếu chẳng may 1 sự kiện không mong muốn nào đó làm thay đổi selectedIndexPath và chúng ta sẽ có lỗi xảy ra ở đây.

2.1 Giải pháp thay thế

Sử dụng associate object để lưu trực tiếp indexPath people muốn xoá vào trong alert và khi muốn xoá chúng ta chỉ việc lấy lại indexPath từ alert ra để xoá, việc này không cần phải viết lại code của UIAlertView mà vẫn đảm bảo lưu được gía trị vào object UIAlertView

Trước tiến ta định nghĩa 1 enum key

struct PropertyKeys {
    static var DeleteKey = "Delete Key"
}

Lưu indexPath vào alert

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            let alert = UIAlertView(title: "Delete People",
                                    message: "Are you sure you want to delete this people ?",
                                    delegate: self,
                                    cancelButtonTitle: "Cancel",
                                    otherButtonTitles: "OK")
            alert.show()
            objc_setAssociatedObject(alert, &PropertyKeys.DeleteKey, indexPath, .OBJC_ASSOCIATION_RETAIN)
        }
    }

Khi tiến hành xoá:

func alertView(alertView: UIAlertView, didDismissWithButtonIndex buttonIndex: Int) {
        if buttonIndex == 1 {
            guard let indexPath = objc_getAssociatedObject(alertView, &PropertyKeys.DeleteKey) as? NSIndexPath else {
                return
            }
            
            self.items.removeAtIndex(indexPath.row)
            self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .None)
        }
    }

3. Tuyệt vời hơn nếu sử dụng kèm với extension

Thay vì phải get và set indexPath tại view controller ta có thể tạo 1 thuộc tính lưu giá trị indexPath thông qua việc extension UIAlertView như sau

extension UIAlertView {
    struct PropertyKeys {
        static var DeleteKey = "Delete Key"
    }
    
    var indexPathToDelete: NSIndexPath? {
        get {
            if let indexPath = objc_getAssociatedObject(self, &PropertyKeys.DeleteKey) as? NSIndexPath {
                return indexPath
            }
            
            return nil
        }
        set {
            objc_setAssociatedObject(self, &PropertyKeys.DeleteKey, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

Qua đó khi cần gán giá trị indexPath cho alert ta chỉ cần gọi

alert.indexPathToDelete = indexPath

Việc lấy indexPath cũng tương tự như vậy

guard let indexPath = alertView.indexPathToDelete else {
    return
}

4.Demo:

https://github.com/pqhuy87it/MonthlyReport/tree/master/AssociatedObjects/AssociatedObjects