dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

AppleのUI実装をさぐる

こんにちは。delyデザインエンジニアのJohn(@johnny__kei)です。
本記事はdely Advent Calendar 2018の10日目の記事です。

Qiita: https://qiita.com/advent-calendar/2018/dely
Adventar: https://adventar.org/calendars/3535

前日は、プロダクトデザイナーのkassyが「ユーザーの声に振り回されないデザインの改善プロセス」という記事を書きました。いいプロダクトを作るには、ユーザーの声を鵜呑みにするのではなく、きちんと判断する必要がありますよね。

はじめに

みなさんは、iOSアプリ開発をするときに、XcodeのDebugging View Hierarchiesを使用していますか?

Debugging View Hierarchiesを使用すると、アプリが現在の状態で停止され、Viewの階層や、プロパティを確認できます。

https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/Art/dwx-sw-dvh-1_2x.png 出典: Apple Specialized Debugging Workflows

AutoLayoutが効いていないのを調査するときに、使用したりする方もいらっしゃると思います。

Debugging View HierarchiesはUIKitのUINavigationBarなどのクラスにも適用され、どのようなView階層か見ることができます。
アプリ独自のUIパーツを作成するときに、できるだけ、View構造や、メソッド、プロパティなどを、UIKitにそろえると、使いやすくなると思います。

そこで
前半は、UIKitのいくつかのクラスのView階層について書きます。
後半は、前半をふまえて、サンプルの実装について書きます。

UIKitのView階層

- UINavigationBar

UINavigationBarのsubviewには_UIBarBackground(非公開クラス)があります。
下の画像を見るとわかるように、UIVisualEffectViewやshadowImageが設定されるimageViewなどがあることがわかります。

f:id:JohnnyKei:20181205151301p:plain

UINavigationControllerのnavigationBarでは、StatusBarまでnavigationBarが伸びているように見えるのは、この_UIBarBackgroundがはみ出しているからです。 自分で、UINavigationBarをViewControllerのviewにaddSubviewする場合は、適応されないので、UINavigationControllerの方で、そういった実装がされると推測できます。

imageViewも、Viewから高さ0.5の分だけ下にはみ出ています。これを発見したとき、ビビりました。

f:id:JohnnyKei:20181205151202p:plain

- UIPageControl

横スクロールでページングがあるときに、よく使用されたりします。 UIPageControlはdotのサイズやdot間のマージンは変更できません。

f:id:JohnnyKei:20181205180702p:plain f:id:JohnnyKei:20181205180204p:plain

dot自体は、単なるUIViewであることがわかります。 また、dotは7ptで、dot間のマージンは9ptということがわかりました。
結構シンプルな作りになっています。

また、UIPageControlは、InterfaceBuilderとCodeでの初期状態に違いがあったのも、新しい発見でした。

--- currentPageIndicatorTintColor pageIndicatorTintColor
InterfaceBuilder UIColor.white UIColor.white.withAlphaComponent(0.2)
Code nil nil

サンプル実装

- BottomBar

SafeAreaの登場で、下部に、Viewを配置したいときに、どういう風に実装したらいいか、悩む場合がありますよね。
自分なりに、こうしたらいいんじゃないかという実装を書いていきます。

f:id:JohnnyKei:20181205175842p:plain

static let viewHeight: CGFloat = 49.0のように本来表示したい高さを定義します。 そして、実際にViewControllerのviewにのせるのはこんな感じに制約をつけます。

NSLayoutConstraint.activate([
    bottomBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    bottomBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    bottomBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -BottomBar.viewHeight),
    bottomBar.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])

こうすることで、safeAreaのbottomがある場合は、contentの高さと、safeArea分伸びた状態で表示することができます。

このBottomBarの上に、Viewをのせる場合は、contentViewにのせます。 contentViewは高さは以下のように固定されているので、ボタンはcontentViewに対して、制約をつけることで、いい感じに表示することができるようになります。

contentView.heightAnchor.constraint(equalToConstant: BottomBar.viewHeight)

さらに、前述した、UINavigationBarのshadowImageを表示するimageViewのように、border部分は、はみ出して作ってあります。 これも、contentViewにのせるようにしていて、UINavigationBarのようにしてあります。

contentView.addSubview(topBorder)

topBorder.heightAnchor.constraint(equalToConstant: 0.5)
topBorder.bottomAnchor.constraint(equalTo: contentView.topAnchor)

f:id:JohnnyKei:20181205173613p:plain

- PageControl

前述したように、UIPageControlはdotのサイズやdot間のマージンは定義されていないので、変更することができません。 そこで、UIPageControlとほぼ同じ、プロパティやメソッドを持ち、dotのサイズやdot間のマージンを設定できる、PageControlを実装してみます。

f:id:JohnnyKei:20181205181725p:plain

UIControlは、UIViewのサブクラスなので、普通にViewをのせていくことで大丈夫です。 さらに、UIPageControlのような実装にするには、タップしてPageが変化したときに、UIControl.Event.valueChanged イベントを発火する必要があります。 UIPageControlの説明にも書いてあります。

その実装は以下のようになっています。

override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        guard let touchPoint = touch?.location(in: self), bounds.contains(touchPoint) else {
            return
        }
       // touchPointが、currentPageのdotの左右どちらかでなどのロジックで判断し、変更があれば発火する
      sendActions(for: .valueChanged)
}

dotのサイズやマージンを定義することで、サイズの計算も簡単になります。

// UIPageControlに合わせる。
var dotSize: CGFloat = 7.0
var dotMargin: CGFloat = 9.0

// pageCountに応じた、size計算
func size(forNumberOfPages pageCount: Int) -> CGSize {
      let height = (15.0 * 2) + dotSize
      guard pageCount > 0 else {
          // UIPageControlがこんな感じの値
          return CGSize(width: dotSize, height: height)
      }
      let width = dotSize * CGFloat(pageCount) + dotMargin * CGFloat(pageCount - 1)
      return CGSize(width: width, height: height)
}

numberOfPages, currentPageなどのプロパティの実装に関しても、結構シンプルになっているので、ぜひサンプルの実装をみてもらえばと思います。
UIPageControlに実装されていることは、全て実装しています。(たぶん)
実装のサンプルは置いておきます。
GitHub Sample Code

まとめ

いかがでしたでしょうか?
Debugging View Hierarchiesを使用すると、AppleのUIKit内部の実装が見れておもしろいですよね。 UI部分の階層構造しかみれませんが、そこから滲み出る、ロジック部分も想像すると、なお面白いと思います。
また、View階層をデザイナーに見せることで、内部構造を理解してもらえるので、次から、そこを考えて、デザインを作ってくれるようになるかもしれませんね。

明日は、サーバーサイドエンジニアのjoeによる「好きな技術を使って作る!くだらないslackBot運用のすヽめ」です。お楽しみに!