try! Swift 2018 第一天筆記 (1)

IMG_1421

Swift Secret Tour

Yuka Ezura

  • 一些有關 Swift 的 type 和 closure 的你不知道的小知識
  • 用 AST 在這些情況了解 Swift 運作的秘訣

Slide

SIL for first time learners

Yusuke Kita

  • 基本上就是說 swiftc -emit-sil -O ... 可以看到 swift 的 Swift Intermediate Language (SIL)。

Slide

Exploring Clang Modules

Samuel E. Giddins @segiddins

CocoaPods 貢獻者 Samuel E. Giddins 在開發 CocoaPods 時所學會關於 clang modules 的心得。

  • 一開始有 c headers 和 #include
    • 但是 header 只是簡單的替換,有效能的問題
    • 同一個檔案可能會 include 多次,其效果可能是未定義的
    • 人們被迫用 #ifndef 之類的 hacks
  • 接著有 Objective-C 的 #import 嘗試解決問題
  • 接著有 Xcode 的 module map 的 @import
    • 在開發 cocoapods 時了解到 module map 的難處:
      • Umbrella Directories - 把一整個路徑作 umbrella header
      • Explicit Submodules - Submodule,一些可選的 module 功能
      • Private Headers - 如名字所述 -- 私有的 header
      • Textual Headers - 一些如 macro 的 header
      • Requires - 指定 module 所需要的編譯器功能如 objc_arc. blocks 等
      • Conflicts - 指定和另一個 framework 有 conflict
      • 自動尋找 Module Map - 自動產生 module map
      • 相對路徑
  • 就算你只寫 Swift ,你的程式碼也會有 header -- swiftmodule
  • 在 cocoapods 裡使用 static library: https://github.com/CocoaPods/CocoaPods/pull/6966
  • 總的來說 modules 的設計仍然基於 C 的 headers,它的 module 還沒穩定,很多時很難明,但總算是個比 C 的 header 好的方案

Slide

Optimising Swift Code for separation of concern and simplicity

Javier Soto @javi

Pebble, Twitter (Fabric) 和現在 Twitch 的開發者 Javier Soto 關於讓 Swift 提高源碼可讀性的演講,有很充實的例子。

  • 編程的原則:簡單、簡潔和明確
  • 關注分離:閱讀源碼的機會比編寫源碼多,分離目的和實作可以增加可讀性
  • 作者舉了十個例子,其中幾個:

個案:Twitter app 裡計算因長度限制輸入

(前)
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange,
              replacementText text: String) -> Bool {
  sendButton.isEnabled =
    textView.text.utf16.count
    + text.utf16.count
    - range.length <= 140
  return true
}
(後)

抽出概念 characterCountUsingBackendPolicycharacterLimit,讓人不必知道實作或注解也明白__為何__要那樣做。

private extension String {
  var characterCountUsingBackendPolicy: Int {
    return utf16.count
  }
}

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange,
              replacementText text: String) -> Bool {
  let characters =
    textView.text.characterCountUsingBackendPolicy
    + text.characterCountUsingBackendPolicy
    - range.length
  let characterLimit = 140
  sendButton.isEnabled = characters <= characterLimit
  return true
}

個案:Twitter app 裡過漏已封鎖用戶的回應

(前)
api.requestReplies(postID: 4815162342) { [weak self] result in
  switch result {
  case .success(let replies):
    var filteredReplies: [Reply] = []
    for reply in replies {
      if !user.isBlocking(reply.author) {
        filteredReplies.append(reply)
      }
    }
    self?.replies = filteredReplies
  case .failure:
    // ...
  }
}
(後)

同樣把 filteringBlockedContent 的概念抽出來,特別是 Collection 的 extension,可以直接用 filter 把目的明確地寫出來。

extension Collection where Element == Reply {
  var filteringBlockedContent: [Reply] = {
    return filter { !user.isBlocking($0.author) }
  }
}

api.requestReplies(postID: 4815162342) { [weak self] result in
  switch result {
  case .success(let replies):
    self?.replies = replies.filteringBlockedContent
  case .failure:
    // ...
  }
}

個案:Auto Layout

(前)
NSLayoutConstraint.activate([
  subview.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left),
  subview.topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top),
  view.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: insets.right),
  view.bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: insets.bottom)
])
(後)
NSLayoutConstraint.activate([
  subview.leadingAnchor = view.leadingAnchor + insets.left,
  subview.topAnchor = view.topAnchor + insets.top,
  view.trailingAnchor = subview.trailingAnchor + insets.right,
  view.bottomAnchor = subview.bottomAnchor + insets.bottom
])

// 用 infix operator 去實作
(後 2)

利用自訂 opeartor 和 extension 把重覆的工作簡化。

NSLayoutConstraint.activate(NSLayoutConstraint.anchroing(subview, within: view))

// 實作
extension NSLayoutConstraint {
  static func anchroing(_ subview: UIView, within view: UIView, insets: UIEdgeInset = .zero) -> [NSLayoutConstraint] {
    // ...
  }
}

個案:Table View Cell

(前)
func tableView(_ tableView: UITableVIew, numberOfRowsInSection section: Int) -> Int {
  return 3
}

func tableView(_ tableView: UITableVIew, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "ID", for: indexPath)
  if indexPath.row == 0 {
    cell.textLabel.text = "general"
  } else if indexPath.row == 1 {
    cell.textLabel.text = "notifications"
  } else if indexPath.row == 2 {
    cell.textLabel.text = "logout"
  }
  return cell
}
(後)

用 enum 取代不同地方的 if 和 switch。


enum Row {
  case general, notification, logout
  var title: String {
  case .general: return "general"
  // ...
  }
}

let rows: [Row] = [.general, .notification, .logout]
func tableView(_ tableView: UITableVIew, numberOfRowsInSection section: Int) -> Int {
  return rows.count
}

func tableView(_ tableView: UITableVIew, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "ID", for: indexPath)
  cell.textLabel.text = rows[indexPath.row].title
  return cell
}

個案:應對不同 SDK 的新 API

(前)

不同的地方有很多 #available ,重覆且不易閱讀

if #available(iOS 11.0, *) {
  constraints = [
   subview.topAnchor = view.safeAreaLayoutGuide.topAnchor,
   // ...
  ]
} else {
  constraints = [
   subview.topAnchor = view.topAnchor,
   // ...
  ]
}
(後)

自己做一個兼容的 API ,讓不同版本也可以使用同一特源碼,對應不同版本的部份只在需要的地方出現

extension UIView {
  var tw_safeAreaLayoutGuide: UILayoutGuide {
    if #available(iOS 11.0, *) {
      return safeAreaLayoutGuide
    }
    
    // 自己實作一個 UILayoutGuide
  }
}

個案:UIViewController 的狀態

(前)

由於載入 user info 是非同步的,有些資料和 view 可能有不同狀態,也有可能是 nil.

final class ProfileViewController: UIViewController {
  var userInfo: UserInfo?
  var headerView: ProfileHeaderView?
  var spinner: UIActivityIndicatorView?
  var retryButton: UIButton?
}
(後)

使用 enum 表達不同的 UI 狀態,直接用 enum value 去代表本來需要用 optional 的部份,這樣 UI Code 就可以不用使用 optional。

final class ProfileViewController: UIViewController {

  enum State {
    case pending
    case loading(spinner: UIActivityIndicatorView)
    case failed(retryButton: UIButton)
    case loaded(userInfo: UserInfo, headerView: ProfileHeaderView)
  }
  
  var state: State = .pending
  
  func loadUserDetails() {
    state = .loading(spinner: UIActivityIndicatorView())
    
    api.requestUserDetails(userID) { [weak self] result in
      switch result {
      case let .success(userInfo):
        self?.state = .loaded(userInfo: userInfo, headerView: createHeaderView(userInfo))
      case let .failure:
        self?.state = .failed(createRetryButton())
      }
    }
  }
}

總結:

  • 利用 local scope 的extension 和 value 去優化可讀性
  • DRY -- Don't Repeat Yourself
  • Swift enum 是個好東西

Slide

Getting to Know the Responder Chain

Samuel Goodwin

  • Responder Chain 簡介
  • Responder Chain 的應用
    • Send action 去 nil target 就可以把 message 跑一遍整個 responder chain,讓懂得那 message 的 responder 去回應。這種實作讓不同元件可以互相溝通而不需要知道對方存在

(待續)