UIKitでたまに遭遇するけど忘れがちなTipsメモ

UIKitで、たまに遭遇するけど忘れがちな部分をメモっておきます。

# ViewやCell等でControlイベントが取れない場合のチェックリスト

- イベントを取りたいViewの `isInteractionEnabled` がtrueになってるか？
- イベントを取りたいViewの `alpha` が0になっていないか？
  - `backgroundColor = .clear`にして、UIView.alphaは0より大きく設定する
- **イベントを取りたいViewの上に、透明系のViewが被さってて、それがイベントを奪っていないか？**
  - storyboardでUIViewを作成すると、デフォルトで `isInteractionEnabled = true` になっているので、注意！
  - storyboardでなくても、コードで追加したUIViewも同様なので、注意！
  - イベントを透過させたいUIViewは、`isInteractionEnabled`を`false`にする
- 親のViewの大きさが意図せず小さくなっていないか？
  - 親Viewからはみ出てる領域では、tapなどのイベントが取れない
  - AutoLayoutの制約が足りてないとwidthやheightが0になる事がある
- そもそも`override func hitTest()`は取れてるか？
  - `pointInside()`
- 参考: [https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events](https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events)

# UICollectionViewのselection管理

UICollectionViewやUITableViewには、セルの選択状態を管理する機能が備わっています。

- isSelectedは、collectionView側で管理されている
- スクロールでCellが見えなくなり、(Cellが再利用され)、スクロールで再度表示した際には`cell.isSelected`が復元されている
- `collectionView.indexPathsForSelectedItems()`で、選択状態のindexPathの配列が取れる

Cellは画面外にスクロールすると再利用されるのに、選択状態は見えない範囲も全て管理してるのが、意外といえば意外でした。

- 参考
  - https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/changing_the_appearance_of_selected_and_highlighted_cells
  - https://developer.apple.com/documentation/uikit/uitableviewdelegate/selecting_multiple_items_with_a_two-finger_pan_gesture

# UITextFieldのshouldChangeCharactersInをRangeで扱う

- NSRangeはNSStringに対応している。(UTF-16)
- RangeはStringに対応する。(Unicode Scalar + 正規化)
- 無理に使うと、絵文字を入れた時等にクラッシュする事がある。(特に絵文字バリエーションシーケンスの扱いが違う)
- 参考
  - [https://qiita.com/doraTeX/items/4feea8d9221419b19954](https://qiita.com/doraTeX/items/4feea8d9221419b19954)
  - [https://moji-memo.hatenablog.jp/entry/20120802/1343876603](https://moji-memo.hatenablog.jp/entry/20120802/1343876603)
- (なぜここだけNSRangeのまま残ってしまってるのか…)

```swift
public func textField(_ textField: UITextField, shouldChangeCharactersIn nsRange: NSRange, replacementString string: String) -> Bool {
    guard let text = textField.text,
          let range = Range(nsRange, in: text) else {
        return false
    }
    let newText = text.replacingCharacters(in: range, with: string)
    /* ... */
}

```

# NavigationBarをスクロールの方向で隠す

比較的簡単でTwitterっぽい自然な感じの動きにしてみました。
バウンスに反応しないよう`isTracking`を見ています。

```swift
class MyViewController {

  @IBOutlet private var collectionView: UICollectionView!
  private var lastContentOffset: CGPoint = .zero
  private var lastTracking = false

  override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.delegate = self
  }

  // 画面遷移前には表示に戻しておく
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.setNavigationBarHidden(false, animated: true)
  }
}

extension MyViewController: UIScrollViewDelegate {

  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let dy = scrollView.contentOffset.y - lastContentOffset.y
    lastContentOffset = scrollView.contentOffset
    let didTouchUp = lastTracking && !scrollView.isTracking
    lastTracking = scrollView.isTracking
    // 下にスクロールしたら隠す
    if scrollView.isTracking && dy > 5 {
      navigationController?.setNavigationBarHidden(true, animated: true)
    }
    // 指を離したタイミングで上にスクロールしていたら表示
    if didTouchUp && dy < -10 {
      navigationController?.setNavigationBarHidden(false, animated: true)
    }
  }
}
```

# Darkモード対応やアイコンなど

- まずLight/Darkで色名を共通化し、Asset Colorで定義する (自動的に切り替わる)
  - コードからもAssetの色を使う
- 単色のIconのLight/Darkの変化や色バリエーションは、コードでtintColorで設定する(管理するIconの枚数が減る)
  - Light/Darkでイメージを変える場合は、Dark Appearanceに登録する
- UIColorの置き換えが無理な場合は、`UIView.traitCollectionDidChange()`で切り替えタイミングを検出できるので、そこで更新する
  - 現在のモードは、`UITraitCollection.current.userInterfaceStyle`で判別できる