7fdf7445a2d041c0a5ebe54a50e92532
在 Playground 里模拟一个 iPhone X 的屏幕

本文同上一篇文章一样,是原载于 Qiita 上的本人的日文文章;不过相比于原文原载时,本文增加了一个屏幕旋转的实现并优化了一部分代码;同时文本内容针对本文应用场景有一定改动

相比较于以前的 Objective-C,Swift 可以说是一门对于新手非常有吸引力的语言;不仅是因为其有更加现代化更加易于上手的语法,而且更加重要的是 Xcode 提供了一个可以让我们快速尝试一段代码是否可用的强大功能:Playground。因为 Playground 同样可以用来预览一个 View,所以为何我们不来尝试用 Playground 来创建一个 iPhone X 的屏幕呢?

所以,马上就让我们来创建吧!

创建一个 iPhone X 的屏幕主要有两个难点:一是屏幕上方的刘海部分,二是 iPhone X 特有的 Safe Area 的设置。这两点就让我们逐一解决。

首先是刘海部分,因为我们需要的只是一个“看起来是刘海”的东西,所以这个东西只需要在 View 的层面就能解决。因此我们首先需要在 Playground 的 Source 里面创建一个刘海的 UIView。包括刘海在内的各项尺寸我们可以从这里找到。

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(in: rect, topRadius: 6, bottomRadius: 20)
    }

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        return CGSize(width: 215, height: 30)
    }

    private func initialize() {

        self.backgroundColor = .clear

    }

    private func drawNotch(in rect: CGRect, topRadius: CGFloat, bottomRadius: CGFloat) {

        let topArcCenterXMargin: CGFloat = 0
        let topArcCenterY = rect.minY + topRadius
        let bottomArcCenterXMargin = topRadius + bottomRadius
        let bottomArcCenterY = rect.maxY - bottomRadius

        let leftTopArcCenter = CGPoint(x: rect.minX + topArcCenterXMargin, y: topArcCenterY)
        let rightTopArcCenter = CGPoint(x: rect.maxX - topArcCenterXMargin, y: topArcCenterY)
        let leftBottomArcCenter = CGPoint(x: rect.minX + bottomArcCenterXMargin, y: bottomArcCenterY)
        let rightBottomArcCenter = CGPoint(x: rect.maxX - bottomArcCenterXMargin, y: bottomArcCenterY)

        let leftTopArcTopPoint = CGPoint(x: leftTopArcCenter.x, y: leftTopArcCenter.y - topRadius)
        let leftBottomArcLeftPoint = CGPoint(x: leftBottomArcCenter.x - bottomRadius, y: leftBottomArcCenter.y)
        let rightBottomArcBottomPoint = CGPoint(x: rightBottomArcCenter.x, y: rightBottomArcCenter.y + bottomRadius)
        let rightTopArcLeftPoint = CGPoint(x: rightTopArcCenter.x - topRadius, y: rightTopArcCenter.y)

        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: leftTopArcTopPoint)

        let fillColor = UIColor.black
        fillColor.setFill()

        path.close()
        path.fill()

    }

}

这里我们创建了一个名为 NotchUIView,在 Notch 里面我们重写 sizeThatFits 方法来返回一个普通的 iPhone X 的刘海的尺寸;然后我们有重写 draw,在里面我们通过 UIBezierPath 来根据当前的 draw 的尺寸来绘制刘海的图形。这样的话我们只需要调用 instance 的 sizeToFit() 就可以得到一个正确的刘海图形;这样写的好处是,即使这个刘海不是 iPhone X 的刘海的尺寸,我们还是能得到一个看起来像是 iPhone X 的刘海的图形

绘制出刘海以后,我们还需要它在 iPhone X 的屏幕上显示,因此接下来我们还需要创建一个 IPhoneXScreen

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)
    }

    public override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutNotch()
    }

    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() {

        super.addSubview(self.notch)

    }

    private func layoutNotch() {

        self.notch.sizeToFit()

        self.notch.center.x = self.bounds.midX
        self.notch.frame.origin.y = self.bounds.minY

    }

    private func initialize() {

        self.setupVisual()
        self.setupNotch()

    }

}

这里的代码比较长,不过我们主要干了两件事:一是在 IPhoneXScreen 里添加刚刚做好的 Notch 作为 subview 并对其进行布局,二是重写了 addSubview 相关的方法,在每次添加完一个 subview 之后重新将 notch 添加至自己的最顶端以保证刘海不会被别的 subview 遮挡住,一次来形成一种刘海的位置真的没有显示的错觉;此外在细节方面,我们对整个 View 还添加了四周的圆角,设置背景色为白色,并且将 同刘海一样,我们在这里也同样重写了sizeThatFits方法来返回一个普通的 iPhone X 的屏幕大小,这样我们就只需要调用sizeToFit()` 就能得到一个模拟出来的 iPhone X 的屏幕了

不过光这样还不够,虽然屏幕看上去是有圆角有刘海的 iPhone X 的屏幕了,但是他还没有 Safe Area,还不能用来给我们做布局。因此接下来我们需要用到的就是 UIViewControlleradditionalSafeAreaInsets 了。虽然苹果并没有直接告诉我们 iPhone X 的 Safe Area 的各个数值是多少,不过只要简单 Google 一下 我们就能轻松得知在普通的竖屏模式下,上面是 44pt,下面是 34pt,左右分别是 0pt 的答案,非常简单

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.updateSafeAreaInsets()
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.updateSafeAreaInsets()
    }

    public convenience init() {
        self.init(nibName: nil, bundle: nil)
    }

}

extension IPhoneXScreenController {

    public override func loadView() {
        self.view = self.iPhoneXScreen
    }

}

extension IPhoneXScreenController {

    private func updateSafeAreaInsets() {
        self.additionalSafeAreaInsets = .init(top: 44, left: 0, bottom: 34, right: 0)
    }

}

在这里我们很简单地重写了 loadView 方法,在里面我们将 IPhoneXScreenControllerview 设置为 iPhoneXScreen,并适应尺寸之后添加合适的 additionalSafeAreaInsets。这样,一个简单的 iPhone X 的屏幕就完成了。我们只需要在 PlaygroundPage 里面调用这个 IPhoneXScreenController 就能快速在 Playground 里面预览一个布局。为了方便计算 CGRect ,我们可以再在 Playground 的 Source 里面添加以下代码:

extension CGRect {

    public func rect(inside insets: UIEdgeInsets) -> CGRect {

        return UIEdgeInsetsInsetRect(self, insets)

    }

}

extension UIView {

    public enum Insets {
        case layoutMargins
        case safeArea
    }

    public func bounds(inside insets: Insets) -> CGRect {

        switch insets {
        case .layoutMargins:
            return self.bounds.rect(inside: self.layoutMargins)

        case .safeArea:
            return self.bounds.rect(inside: self.safeAreaInsets)
        }

    }

}

这样我们就可以简单地通过调用 view.bounds(inside: .safeArea) 来获得一个 View 的 Safe Area 的布局,然后在 Playground 的主文件里,我们就可以进行这样的操作了:

```swift:Playground.swift
import UIKit

top Created with Sketch.