從 Notification.Name 看 Swift 如何優雅的解決 String 硬編碼

韓元旭發表於2019-03-03
從 Notification.Name 看 Swift 如何優雅的解決 String 硬編碼

前面

初學 Swift 中相關 NSNotification 的程式碼時, 發現了之前熟悉的 name 引數的型別由 Objective-C 中的 NSString 變成了 Notification.Name 型別. 並不是我期望的 String 型別…這是怎麼回事呢?

Swift 中如何使用 Notification

那麼, 在 Swift 中如何使用 Notification 呢, 以 post 為例.

NotificationCenter.default.post(name: Notification.Name.UIApplicationDidFinishLaunching, object: nil)
複製程式碼

其中, Notification.Name 是可以省略的, 就變為了

NotificationCenter.default.post(name: .UIApplicationDidFinishLaunching, object: nil)
複製程式碼

檢視定義發現了 UIApplicationDidFinishLaunching 實際上是定義在結構體 NSNotification.Name 擴充套件(extension)中的的一個靜態常量 (static let), 型別是 NSNotification.Name

extension NSNotification.Name {

    @available(iOS 4.0, *)
    public static let UIApplicationDidEnterBackground: NSNotification.Name

    @available(iOS 4.0, *)
    public static let UIApplicationWillEnterForeground: NSNotification.Name

    public static let UIApplicationDidFinishLaunching: NSNotification.Name
    
    ...
}
複製程式碼

所以我們才可以省略前面的 Notification.Name 直接使用 .UIApplicationDidFinishLaunching (Notification.Name 是 NSNotification.Name 的別名)

那我們如果想自定義一個通知怎麼辦呢, 直接可以仿照系統的方式, 我們自己為其增加一個 extension

extension Notification.Name {
	static let LoginStatusChanged = Notification.Name("LoginStatusChanged")
}
複製程式碼

其中 Notification.Name("LoginStatusChanged") 是其初始化方法, 可檢視文件說明, 使用時, 可直接

NotificationCenter.default.post(name: .LoginStatusChanged, object: nil)
複製程式碼

因為這個通知 LoginStatusChanged 是定義在 Notification.Name 中的了, 所以也沒必要在名稱後面增加 Notification 等字樣來表示這是一個通知了. 所以 Swift 中很多定義的名稱都是非常簡潔的.

對比 Objective-C 中的使用

對比之前在 Objective-C 中的使用

[[NSNotificationCenter defaultCenter] postNotificationName:"xxxxxxxxxx" object:nil

這樣是非常容易出錯的, 查這樣的錯誤經常也是非常費時費力的, 也讓人看來是非常不優雅的, 所以我們經常會進行巨集定義或者是常量來防止字串硬編碼的問題.

但這實際上也是會帶來一些令人頭疼的問題的:

  1. 為了表明定義的字串常量是一個通知名, 還要為其增加冗長的字首或者是字尾
  2. 在開發中還經常會在程式碼補全中, 看到根本不和場合的一些常量名
  3. 通常為了使用方便和易於維護, 還會在將所有的通知定義在一個 xxDefine.h 的標頭檔案中, 並在 pch 檔案中引用, 此時如果增刪或者修改了任意通知. 將會引起工程的全量重新編譯. 也很是頭疼.

所以, Swift 這種使用方式可謂是十分優雅.

舉一反三

在開發中, 其實類似於 Notification 這種需要傳遞字串的場景還有很多, 我們都可以使用這類使用方法進行優化.

場景

假設有這樣一個場景, 定義一個類 EventReporter 用來處理埋點請求.

class EventReporter {

	static let shared = EventReporter()

	func reportEvent(_ eventId: String, withParams params: [String:Any]?) {
		// 埋點上報邏輯
	}
}
複製程式碼

相信這樣的場景是很多人都見過的, 其中 eventId 是我們埋點的事件的ID, 那麼該如何使用類似 Notification.Name 的方式來優化這類場景呢?

原理

從文件中看出 Notification.Name 實際上是遵從了一個協議 RawRepresentable

Overview

With a RawRepresentable type, you can switch back and forth between a custom type and an associated RawValue type without losing the value of the original RawRepresentable type. Using the raw value of a conforming type streamlines interoperation with Objective-C and legacy APIs and simplifies conformance to other protocols, such as Equatable, Comparable, and Hashable.

The RawRepresentable protocol is seen mainly in two categories of types: enumerations with raw value types and option sets.

簡單的說就是, 使用 RawRepresentable 型別, 可以在自定義型別和其關聯的 RawValue 型別之間來回切換, 可簡化與 Objective-C 和傳統 API 的互動, 兩類:具有原始值型別和選項集的列舉(OptionSet, 其實 Swift 中的選項集列舉就是整合自 RawRepresentable 這個 Protocol 實現的), 說白了. 就是用一個型別封裝一下我們想要使用的型別比如說 String, 來方便互動.

實現

使用起來很簡單, 定義一個結構體來管理所有的埋點事件

struct EventID: RawRepresentable {
	
}
複製程式碼

根據編譯器提示, 補全協議程式碼

struct EventID: RawRepresentable {
	typealias RawValue = String
	
	var rawValue: String
	
	init?(rawValue: String) {
		
	}
}
複製程式碼

從這就更容易看出其原理, 實際上內部的 rawValue 屬性就是我們需要使用的 String 型別的事件名, 初始化方法傳入該 String 對其賦值即可, 返回 EventID 型別的結構體

這裡發現初始化方法返回的是一個 Optional 型別, 這樣使用起來還需要解包, 不太方便, 可以看到 Notification.Name 的初始化方法返回並不是 Optional, 因為定義都是非常確定的事件名(通知名), 而且 init 方法中也不會產生異常, 所以此處沒什麼必要使用 Optional, 去掉 ? 即可

struct EventID: RawRepresentable {
	typealias RawValue = String
	
	var rawValue: String
	
	init(rawValue: String) {
		self.rawValue = rawValue
	}
}
複製程式碼

那麼, 我們的上報類的程式碼可以修改如下, 這裡還可以給 params 一個預設值, 這樣如果沒有引數時, 可以只傳遞 eventId 一個引數即可.

class EventReporter {

	static let shared = EventReporter()

	func reportEvent(_ eventId: EventID, withParams params: [String:Any]? = nil) {
		let event = eventId.rawValue
		// 埋點邏輯
	}
}
複製程式碼

最後, 定義一個埋點事件看看吧~, 推薦寫到 extension 中易於維護.

extension EventID {
	static let LoginPageExposure = EventID(rawValue: "login_page_exposure")
}
複製程式碼

那麼使用的時候,

EventReporter.shared.reportEvent(.LoginPageExposure)
複製程式碼

當我們打出 . 的時候, 程式碼補全就已經將 LoginPageExposure 提示給我們了.

總結

使用這種方式優化程式碼, 不僅可以讓程式碼意圖容易理解, 使用也更加簡單不會出錯. 而且也不會使得 LoginPageExposure 事件名在不想要出現的時候被程式碼補全功能強行彈出來.

Reference

相關文章