[譯]iOS開發者在Swift中應避免過度使用@objc

donghuan1發表於2019-05-07

就在前幾天,我終於把專案遷移到了Swift2.2,在使用SE-0022建議的#selector語句時,我遇到了一些問題。如果在protocol extension中使用#selector,這個protocol必須新增@Objc修飾符。而之前的Selector("method:")語句則不需要新增。

通過協議的擴充套件配置檢視控制器

為了達到本文的目的,我簡化了工作中專案的程式碼,但所有核心的思想都保留著。一種我經常在swift裡用的模式是:為了重用的配置寫protocols(協議)和extensions(擴充套件),特別是有Uikit的時候

假設我們有一組檢視控制器,每個控制器都需要一個 view model 和 一個“取消”按鈕。每一個控制器需要各自響應 “cancel”按鈕的點選事件。我們可以這樣寫:

struct ViewModel {
    let title: String
}

protocol ViewControllerType: class {
    var viewModel: ViewModel { get set }

    func didTapCancelButton(sender: UIBarButtonItem)
}
複製程式碼

如果就寫成這樣,那每個控制器都需要自己去新增和寫一個一樣的取消按鈕。這樣就會有很多一樣的程式碼。我們可以通過擴充套件(用老的 Selector("") 語句)來解決:

extension ViewControllerType where Self: UIViewController {
    func configureNavigationItem() {
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .Cancel,
            target: self,
            action: Selector("didTapCancelButton:"))
    }
}
複製程式碼

現在每個符合協議的控制器都可以通過在viewDidLoad()裡呼叫協議的configureNavigationItem() 方法來配置取消按鈕,是不是好多了~我們的控制器看起來是這樣的:

class MyViewController: UIViewController, ViewControllerType {
    var viewModel = ViewModel(title: "Title")

    override func viewDidLoad() {
        super.viewDidLoad()
        configureNavigationItem()
    }

    func didTapCancelButton(sender: UIBarButtonItem) {
        // handle tap
    }
}
複製程式碼

這僅是一個簡單的例子,但我們可以想象通過這個方式製造更多複雜的配置。

把以上程式碼段升級到 Swift 2.2後,是這樣的:

extension ViewControllerType where Self: UIViewController {
    func configureNavigationItem() {
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .Cancel,
            target: self,
            action: #selector(didTapCancelButton(_:)))
    }
}
複製程式碼

但現在我們有了個問題,一個新的編譯錯誤

Argument of '#selector' refers to a method that is not exposed to Objective-C.

Fix-it   Add '@objc' to expose this method to Objective-C
複製程式碼

@objc試圖破壞所有的東西

因為一系列的原因, 在原始的ViewControllerType協議中,我們並不能簡單的給這個方法新增一個@objc修飾符。如果我們這麼做了,那麼所有的protocol都需要用@objc來標記,這將意味著:

  • 所有這個protocol的父protocol都需要用@objc來標記。
  • 所有繼承自這個protocol的protocol都會被自動新增@objc
  • 我們在protocol中的結構體(ViewModel)不能用Objective-C來表示。

到目前,@objc在這裡的唯一功能就是定義了一個普通的target-action selectors。儘管我們可以使用swift的強大功能,但是因為Cocoa依然貫穿我們的程式碼Cocoa all the way down,我們並沒有正真的在寫純粹的swift - 除非我們開始在各個地方引入@objc。

我們在這的例子很簡單,但是想象一下更復雜的類依賴關係圖,大量使用Swift的值型別和當這個協議處在多個協議的中間層時。把引入@objc作為解決方案真是app的末日。如果我們這樣做,@objc這種做法會讓我們的Swift程式碼毫無美感並變得亂糟糟。這會毀了所有的東西。

但是希望還是有的。

不使用@objc來避免亂糟糟

我們大可不必讓為了讓我們的Swifit程式碼能使用Objcetive-C的語法而使用@objc

我們可以把protocol分解成多個protocol來去除@objc,然後我們再重組這些protocol。事實上,我們可以讓編譯器順利編譯和避免更改任何檢視控制器的程式碼。

第一步,我們把protocol拆成2個。ViewModelConfigurableNavigationItemConfigurable。把ViewControllerType裡的extension放到NavigationItemConfigurable

protocol ViewModelConfigurable {
    var viewModel: ViewModel { get set }
}

@objc protocol NavigationItemConfigurable: class {
    func didTapCancelButton(sender: UIBarButtonItem)
}
複製程式碼

最終,我們可以把原ViewControllerType protocol定義成typealias

typealias ViewControllerType = protocol<ViewModelConfigurable, NavigationItemConfigurable>
複製程式碼

和遷移到Swift2.2之前比一切都很正常,而且我們定義的原檢視控制器也沒有發生任何改變,沒有東西被破壞。如果你曾經遇到類似的情況,或者你也想阻止@objc帶來的破壞(你應該這麼做),我強烈建議採用這個策略。

這並不是顯而易見的

現在的程式碼,我還是覺得有點不爽,當然,針對這個問題,這就是最Swift化的答案。當Xcode突然開始提示你並且很快的應用它的修復方案依然會把所有都破壞掉。特別是當Xcode提供的修復方案正中你下懷的時候,這個時候,上面說的到的這類解決方案並不能立即很清楚。

最後,在做了以上那些更改之後,我意識到總的來說這其實是一個很好的解決方案。。沒有什麼理由在一個地方只用一個協議。像ViewModelConfigurableNavigationItemConfigurable這兩個協議分工明確。把不同的協議組合在一起始終都是最優雅、最適當的設計。

相關文章