UIKitでNeumorphismのデザインを構築するライブラリ

Neumorphism（ニューモーフィズム）は、UIデザインの次のトレンドになるかもしれないと昨年末あたりから言われ出している表現手法です。本当にそうなるかどうかは分かりませんが、実験としてiOS用のライブラリ [EMTNeumorphicView](https://github.com/hirokimu/EMTNeumorphicView) を作ってみました。ここではその実装アプローチについて書きます。

#Neumorphismとは
フラットデザインやMaterial Designから、かつて流行したスキューモーフィックデザインに少し回帰したようなデザインといえばわかりやすいと思います。 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9042/2ebe8a9c-ba34-74e4-8ff6-2db5d3be4bb3.jpeg" width="50%" alt="EMTNeumorphicView screenshot">

詳細は下記にあります。

- [Neumorphism in user interfaces](https://uxdesign.cc/neumorphism-in-user-interfaces-b47cef3bf3a6)
- [Neumorphism will NOT be a huge trend in 2020](https://uxdesign.cc/neumorphism-will-not-be-a-huge-trend-in-2020-67a8c35e52cc)

自分もnoteを書きました。

- ["Neumorphism" なるUIデザインのトレンド](https://note.com/hironobukimura/n/n0431c73714e8)


#凸型 (convex) のUIを作成する

Neumorphismのデザインを実現するには、要素に角Rを付けたうえで明暗2色のシャドウを落とさなければなりません。

実装方法はいろいろあると思いますが、EMTNeumorphicViewではシャドウ用のサブレイヤーを生成するカスタムCALayerを作りました。ライブラリではそれをlayerClassにしたUIView、UIButton、UITableViewCellを提供します。

```EMTNeumorphicView.swift
    public override class var layerClass: AnyClass {
        return EMTNeumorphicLayer.self
    }
```

シャドウをCALayerで実装しているため、ボタンの状態変更にはCore Animationが効いています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9042/deff63bd-cf39-f3f5-4851-b9a393eb4bba.gif" width="50%" alt="toggle buttons">

カスタムCALayer内では、次のような感じで要素色、明るいシャドウ、暗いシャドウの3つのサブレイヤーを使っています。

```EMTNeumorphicLayer.swift
masksToBounds = false

colorLayer = CALayer()
shadowLayer = EMTShadowLayer()
lightLayer = EMTShadowLayer()

insertSublayer(colorLayer!, at: 0)
insertSublayer(lightLayer!, at: 0)
insertSublayer(shadowLayer!, at: 0)

colorLayer?.masksToBounds = true
shadowLayer?.masksToBounds = false
lightLayer?.masksToBounds = false
```

明るいシャドウと暗いシャドウのレイヤーに、shadowPathを使って影をつけます。それぞれ左上方向と右下方向にオフセットし、Neumorphismのシャドウを表現します。

```ShadowLayer.swift
let cornerRadius: CGFloat = 24
let shadowRadius: CGFloat = 8
let offsetWidth: CGFloat = shadowRadius / 2
let cornerRadii: CGSize = CGSize(width: cornerRadius - offsetWidth, 
                                 height: cornerRadius - offsetWidth)

var shadowX: CGFloat = offsetWidth
var shadowY: CGFloat = offsetWidth

// reverse offset direction
if mode == isLightSide {
    shadowX *= -1
    shadowY *= -1
}
        
// add shadow
let shadowBounds = bounds
let path = UIBezierPath(roundedRect: shadowBounds.insetBy(dx: offsetWidth, dy: offsetWidth),
                        byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
                        cornerRadii: cornerRadii)
shadowPath = path.cgPath
shadowOffset = CGSize(width: shadowX, height: shadowY)
```

#凹型 (concave) のUIを作成する
凹型はトグルボタンやテーブルなどを表現するのに向いていますが、明暗2色をインナーシャドウで表現しなければならないため、凸型よりもずっと実装が面倒です。

要点は次のような感じです

- 明暗2色のシャドウを2つのレイヤーで表現するのは凸型と同じ
- 必要な部分のみ表示されるようにマスクをかけ、明暗が綺麗に合成されるようにする

ポイントとなる部分のみ説明します。まずインナーシャドウを作成します。
EMTNeumorphicViewでは、要素の形状に沿った中空のUIBezierPathを作成し、それをレイヤーのshadowPathとして使うことでインナーシャドウを描画しています。

```ShadowLayer.swift
masksToBounds = true
cornerRadius = 24

let gap: CGFloat = 1
let cornerRadii = CGSize(width: cornerRadius + gap, height: cornerRadius + gap)
let innerRadius = cornerRadius - gap
let cornerRadiiInner = CGSize(width: innerRadius, height: innerRadius)

let outerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: -gap, dy: -gap),
                             byRoundingCorners: corners,
                             cornerRadii: cornerRadii)
let innerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: gap, dy: gap),
                             byRoundingCorners: corners,
                             cornerRadii: cornerRadiiInner).reversing()

outerPath.append(innerPath)
shadowPath = path.cgPath
```

問題は明暗2つのシャドウの合成です。凸型の場合はシャドウの重なる部分は要素の後ろ側に隠れてしまうので合成する必要はありませんでした。
凹型の場合、明暗が重なって入れ替わる部分（要素の右上と左下の角R部分）がうまく合成されるように個別にマスクをかけます。
マスク領域を説明するために色をつけると次のようになります。マゼンタがダークシャドウ用、シアンがライトシャドウ用です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9042/8825022d-d074-2259-b88f-a947e3a7d3ee.jpeg" width="25%" alt="mask area">


マスクを生成するためのカスタムレイヤーを作り、drawメソッド内でマスク領域を描画します。
以下はライトシャドウの右上の角R部分をマスクするためのコードです（黒→透明色のグラデーションを右下→左上へ描画）。
ダークシャドウの場合は同じ領域をグラデーションの方向を逆にして描画します。

```GradientMaskLayer.swift

override func draw(in ctx: CGContext) {
    // top-right corner
    let cornerRect = CGRect(x: frame.size.width - cornerRadius,
                            y: 0, 
                            width: cornerRadius, 
                            height: cornerRadius)

    let bottomRight = CGPoint(x: cornerRect.maxX, y: cornerRect.maxY)

    guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                    colors: [UIColor.black.cgColor, UIColor.clear.cgColor] as CFArray,
                                    locations: [0, 1]) else { return }
    ctx.saveGState()
    ctx.addRect(cornerRect)
    ctx.clip()
    ctx.drawLinearGradient(gradient, start: bottomRight, end: cornerRect.origin, options: [])
    ctx.restoreGState()
```
#まとめ
もっといい方法がある気もしますがとりあえずこんな感じです。