Playground で（擬似）iPhone X の画面を作ってみる！

この記事は [Swift 愛好会 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/love_swift) 7 日目の記事です。

たまたま 7 日目が空いてたので登録しました。ちなみに本日（20 日）の記事もすでに登録してあるのになんで投稿しないの？？なんて野暮な質問をしてはいけません（おい

というわけで、Swift がせっかく Playground が使えて、しかも Playground で画面の構成まで確認できて、Playground でも iPhond X の擬似画面を作って遊んでみたいと思ったことありません？

というわけで、作ってみた！

![スクリーンショット 2017-12-20 0.34.52.png](https://qiita-image-store.s3.amazonaws.com/0/63998/175be928-6a79-365f-d244-e0ee7dd1cf2c.png)

元々は自作の Auto Layout を使わずにレイアウトをしてくれるエンジン [NotAutoLayout](https://www.github.com/el-hoshino/NotAutoLayout) の Playground 用に[作ってます](https://github.com/el-hoshino/NotAutoLayout/issues/27)。せっかくなので作り方を公開したいと思います。（ちなみに縦画面のみです；今のところ横画面はまだ作ってません。時間あったら作るかもしれません）

まず画面を作るにあたって、一番面倒なのは上のノッチの部分ですね。というわけでまずノッチを作ります。寸法は[ここ](https://www.paintcodeapp.com/news/iphone-x-screen-demystified)を参考にしています。

```swift
private final class Notch: UIView {
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		self.initialize()
	}
	
	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		self.initialize()
	}
	
	convenience init() {
		self.init(frame: .zero)
	}
	
	override func draw(_ rect: CGRect) {
		super.draw(rect)
		self.drawNotch(topRadius: 6, bottomRadius: 20)
	}
	
	// `sizeThatFits` メソッドをオーバーライドしてノッチのサイズを返しておくと後で `sizeToFit()` を呼び出すだけで正しいサイズになるので便利です。
	override func sizeThatFits(_ size: CGSize) -> CGSize {
		return CGSize(width: 215, height: 30)
	}
	
	private func initialize() {
		
		self.backgroundColor = .clear
		
	}
	
	// ノッチをパスで描画
	private func drawNotch(topRadius: CGFloat, bottomRadius: CGFloat) {
		
		let leftTopArcCenter = CGPoint(x: 0, y: topRadius)
		let rightTopArcCenter = CGPoint(x: self.bounds.width, y: leftTopArcCenter.y)
		let leftBottomArcCenter = CGPoint(x: topRadius + bottomRadius, y: self.bounds.height - bottomRadius)
		let rightBottomArcCenter = CGPoint(x: self.bounds.width - leftBottomArcCenter.x, y: leftBottomArcCenter.y)
		
		let leftBottomArcLeftPoint = CGPoint(x: topRadius, y: leftBottomArcCenter.y)
		let rightBottomArcBottomPoint = CGPoint(x: self.bounds.width - topRadius - bottomRadius, y: self.bounds.height)
		let rightTopArcLeftPoint = CGPoint(x: self.bounds.width - topRadius, y: topRadius)
		
		let path = UIBezierPath()
		path.addArc(withCenter: leftTopArcCenter, radius: topRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: true)
		path.addLine(to: leftBottomArcLeftPoint)
		path.addArc(withCenter: leftBottomArcCenter, radius: bottomRadius, startAngle: .pi, endAngle: .pi * 0.5, clockwise: false)
		path.addLine(to: rightBottomArcBottomPoint)
		path.addArc(withCenter: rightBottomArcCenter, radius: bottomRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
		path.addLine(to: rightTopArcLeftPoint)
		path.addArc(withCenter: rightTopArcCenter, radius: topRadius, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)
		path.addLine(to: .zero)
		
		let fillColor = UIColor.black
		fillColor.setFill()
		
		path.close()
		path.fill()
		
	}
	
}
```

ノッチは別に外から見れるようにする必要はないのでとりあえず `private` で定義します。そして一番肝心な部分は `drawNotch(topRadius: CGFloat, bottomRadius: CGFloat)` ですね。ここでノッチのパスを書いてます。 `draw(_ rect: CGRect)` をオーバーライドしてこの中に `drawNotch` すればいいです。ちなみに `drawNotch` の中身の具体的なパスの書き方について、`addLine` はわかるんですが、`addArc` の `startAngle` と `endAngle` て具体的に一体どういうとりかたなのか結局イマイチわからなかったです。ここのソースコードは色々試行錯誤してできたものです。詳しい人教えてくださると大変喜びます。まあ肝は要するに `UIBezierPath` を使ってパスを書くんですね。[@marty-suzuki さんの記事](https://qiita.com/marty-suzuki/items/a28269ee39b6e0ec0830)がかなり参考になるかもしれません

ノッチができたらあとは簡単です。スクリーン用のビューを作って、コーナーを角丸にして、そしてノッチを追加すればいいです。ここで注意したいのはビューに新しいビューを追加した時、ノッチは常に一番上にないとダメなので、ここを工夫する必要があります。例えばビューを追加するための専門の `contentsView` を作るとか、ビューを追加したらもう一回ノッチを一番上に乗っかるとかしないといけないのですが、まあどのみち `addSubview` メソッドをオーバーライドする必要があります。ここは後者の方法ビューを追加した後にノッチの上に乗っけるようにします：

```swift
public final class IPhoneXScreen: UIView {
	
	private let notch = Notch()
	
	public override init(frame: CGRect) {
		super.init(frame: frame)
		self.initialize()
	}
	
	public required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		self.initialize()
	}
	
	public convenience init() {
		self.init(frame: .zero)
	}
	
}

extension IPhoneXScreen {
	
	public override func sizeThatFits(_ size: CGSize) -> CGSize {
		return CGSize(width: 375, height: 812)
	}
	
	// NotAutoLayout でレイアウトを決めます
	public override func layoutSubviews() {
		super.layoutSubviews()
		
		self.nal.layout(self.notch) { $0
			.pinTopCenter(to: self, s: .topCenter)
			.fitSize()
		}
	}
	
	public override func addSubview(_ view: UIView) {
		super.addSubview(view)
		assertNotch(with: view)
		super.addSubview(self.notch)
	}
	
	public override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
		super.insertSubview(view, aboveSubview: siblingSubview)
		assertNotch(with: view)
		super.addSubview(self.notch)
	}
	
	public override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
		super.insertSubview(view, belowSubview: siblingSubview)
		assertNotch(with: view)
		super.addSubview(self.notch)
	}
	
	public override func insertSubview(_ view: UIView, at index: Int) {
		super.insertSubview(view, at: index)
		assertNotch(with: view)
		super.addSubview(self.notch)
	}
	
}

extension IPhoneXScreen {
	
	// そんなことはないはずだが一応アサートサブビュー追加するとき追加しているのはノッチではないことを確認します
	private func assertNotch(with view: UIView) {
		assert((view == self.notch) == false)
	}
	
	private func setupVisual() {
		
		self.backgroundColor = .white
		self.contentScaleFactor = 3
		
		self.layer.cornerRadius = 40
		self.clipsToBounds = true
		
	}
	
	private func setupNotch() {
		
		self.notch.sizeToFit()
		// ノッチを追加するときは、通常のビューの処理と違って自分を再度追加する必要がないので `super.addSubview` で対応します。
		super.addSubview(self.notch)
		
	}
	
	private func initialize() {
		
		self.setupVisual()
		self.setupNotch()
		
	}
	
}
```

これで Playground の Source に `IPhoneXScreen.swift` ファイルを作って、上の画面とノッチのソースコードを中にコピペすれば Playground の本体で `let view = IPhoneXScreen(); view.sizeToFit()` で iPhone X の擬似画面が作れました。

レイアウトの部分は NotAutoLayout で作ってます。見ての通り、`layoutSubviews()` をオーバーライドして、ノッチを真ん中の上に固定（`pinTopCenterTo(_:,s:`）してサイズを自動でフィット（`fitSize()`）させています。この `fitSize` の動きはまさに `sizeToFit` とほぼ同じです。そう、**NotAutoLayout を使えば、Auto Layout なんか使わなくても、こんなに手軽にレイアウトを決められるのです（直球宣伝）**。まあもし NotAutoLayout 使わなければ、ここのソースコードはこのようなものに置き換えれば大丈夫でしょう（Auto Layout 使うバージョンは知りません…Auto Layout 絶対殺すマンですから）

```swift
	public override func layoutSubviews() {
		super.layoutSubviews()
		
		self.notch.sizeToFit()
		self.notch.frame.origin.y = 0
		self.notch.center.x = self.bounds.width / 2
	}
```

さて、ここまで作ったらビジュアル面では iPhone X に見えるようになったが、まだ一つ肝心なものが足りない：そう、iPhone X 対応で一番の肝である Safe Area です。これがシミュレーションできなければ Playground で iPhone X の画面作っても意味がないのです。というわけでこれからは `UIViewController` の出番です。

`UIViewController` には `additionalSafeAreaInsets` というプロパティーがあります。このプロパティーのおかげで、`view` が Safe Area を参照できるようになるのです。というわけで作ります。

```swift
public final class IPhoneXScreenController: UIViewController {
	
	private lazy var iPhoneXScreen: IPhoneXScreen = {
		let screen = IPhoneXScreen()
		screen.sizeToFit()
		return screen
	}()
	
	public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
		super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
		self.initialize()
	}
	
	public required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		self.initialize()
	}
	
	public convenience init() {
		self.init(nibName: nil, bundle: nil)
	}
	
}

extension IPhoneXScreenController {
	
	// 自分のビューを IPhoneXScreen に
	public override func loadView() {
		self.view = self.iPhoneXScreen
	}
	
}

extension IPhoneXScreenController {
	
	// `additionalSafeAreaInsets` で iPhone X の Safe Area を再現します
	private func initialize() {
		self.additionalSafeAreaInsets = .init(top: 44, left: 0, bottom: 34, right: 0)
	}
	
}
```

ここまで作れば、もう Playground で iPhone X の擬似画面を使って色々レイアウトを試すことができるようになりました。例えば NotAutoLayout を使って試しにセーフエリアを全部真っ赤にしてみます：

```swift
let controller = IPhoneXScreenController()

PlaygroundPage.current.liveView = controller.view

let testView = UIView()
testView.backgroundColor = .red
controller.view.addSubview(testView)

controller.view.nal.layout(testView) { $0
	.setTopLeft(by: { $0.safeOrigin })
	.setSize(by: { $0.safeSize })
}
```
![スクリーンショット 2017-12-20 0.57.03.png](https://qiita-image-store.s3.amazonaws.com/0/63998/8f2f86c2-2f5d-96f1-6dc2-4cb45bd31f34.png)

もちろん、Auto Layout 愛用の方は、Auto Layout 制約でも使えるので大丈夫です（SnapKit とか使わないとクッソ面倒いですが）：

```swift
let controller = IPhoneXScreenController()

PlaygroundPage.current.liveView = controller.view

let testView = UIView()
testView.backgroundColor = .red
controller.view.addSubview(testView)

testView.translatesAutoresizingMaskIntoConstraints = false

testView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor).isActive = true
testView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
testView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
testView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
```
![スクリーンショット 2017-12-20 0.55.10.png](https://qiita-image-store.s3.amazonaws.com/0/63998/b0c02b14-501f-59b4-a90f-6dfffca6c04b.png)

というわけで、皆さんもぜひ Playground で iPhone X の画面作って色々遊んでみましょー、これで iPhone X の画面対応に少しでも楽になれたらいいなと思います（トオイメ

あ、一応最後に宣伝します：冬コミに Auto Layout 殺す本として、この記事でも登場した自作ライブラリー NotAutoLayout の解説本を出します。表紙入れて全 48 ページのボリュームで現金価格 1000 円で頒布します；また去年みたいに Alipay とかのキャッシュレス決済も可能にしたいと思いますが今のところ対応するプラットフォームと価格はまだ未定です（現金価格よりは安くなる予定です）。100 部刷ったので多分間違いなく余るはずだから興味ある方はぜひ先に嫁の確保をしてから遊びに来て下さい、1 日目（29 日）の東キ-11b です。

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">【宣伝】C93友人が受かりましたので自分も本出します。Auto Layout○す本として自作フレームワーク「NotAutoLayout」の解説をする「レイアウトを、もっと Swifty に」の名前の本です。1日目の東キ-11a（Augmented 2次元 Lab）でお待ちしております。多分40ページくらいで1000円で配布する予定です。 <a href="https://t.co/GoHaWOwipD">pic.twitter.com/GoHaWOwipD</a></p>&mdash; 星野恵瑠@1日目東キ-11a#今日も1日フレンズ㌠ (@lovee) <a href="https://twitter.com/lovee/status/940462645295038464?ref_src=twsrc%5Etfw">December 12, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

---
追記：[NotAutoLayout 2.2.1 公開しました](https://github.com/el-hoshino/NotAutoLayout/releases/tag/version%2F2.2.1)。このバージョンでは Playground サンプルがこの記事で書いた iPhone X の画面になります（というよりこの Playground サンプルを作るためにこの記事を書いたのです）
![スクリーンショット 2017-12-21 2.23.46.png](https://qiita-image-store.s3.amazonaws.com/0/63998/91ba286a-634e-e9ce-5b6b-1787173b8c96.png)