NeumorphismなUIButton

※ **gifは、影の大きさを調節する関数を実装する前のものです。**
# はじめに
NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
![index.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/cbe42ea2-8a34-5137-9506-8e39d67b9071.gif)

## このButtonのしくみ
見てもらうとすぐわかるんですが、
<img width="451" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/97a21f62-2f20-5b9c-3003-5fd8a668c4cc.png">
クリックされていないときは、このボタンは

 - ボタンの外側の上と左に光があたっている。
 - ボタンの外側の下と右に影がついている。

というような特徴をもっています。
それだけでなく、

<img width="482" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/dd3d2b80-697a-e0ea-6296-3f51e162ac83.png">

 - 押されているときはボタンの内側の上と左に影がついている。
 - 押されているときはボタンの内側の下と右に光があたっている。

というような特徴をもっています。

この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。

## 実際のコード
実際のコードを公開します。

 - Colors.swift
 - PlainSquareButton.swift

という2つのファイルを使って実装しました。

```swift:Colors.swift
import Foundation
import UIKit

class Colors {
    
    static var plainColor = UIColor(hex: "ECF0F3")
}

extension UIColor {
    
    convenience init(hex: String, alpha: CGFloat = 1.0) {
        
        let v = Int("000000" + hex, radix: 16) ?? 0
        let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255
        let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255
        let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255
        self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1))
    }
    
    func brighter() -> UIColor {
        var hue: CGFloat = 0,
            saturation: CGFloat = 0,
            brightness: CGFloat = 0,
            alpha: CGFloat = 0
        if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
            return UIColor(hue: hue, saturation: saturation * 0.85, brightness: brightness * 1.1, alpha: alpha)
        } else {
            return self
        }
    }
    
    func darker() -> UIColor {
        var hue: CGFloat = 0,
            saturation: CGFloat = 0,
            brightness: CGFloat = 0,
            alpha: CGFloat = 0
        if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
            return UIColor(hue: hue, saturation: saturation * 1.25, brightness: brightness * 0.75, alpha: alpha)
        } else {
            return self
        }
    }
}
```

```swift:PlainSquareButton.swift
import Foundation
import UIKit

class PlainSquareButton: UIButton {
    
    private let highlightLayer = CALayer(),
                shadowLayer = CALayer()
    private let dentedHorizontalLayer = CAGradientLayer(),
                dentedVerticalLayer = CAGradientLayer()
    
    required init?(coder: NSCoder) {
        
        super.init(coder: coder)
        self.layer.cornerRadius = 5.0
        self.backgroundColor = Colors.plainColor
        self.putHighlight()
        self.putShadow()
        self.addTarget(self, action: #selector(self.onPushed), for: .touchDown)
        self.addTarget(self, action: #selector(self.onReleased), for: .touchUpInside)
    }
    
    private func putHighlight() {
        
        self.highlightLayer.masksToBounds = false
        self.highlightLayer.frame = self.bounds
        self.highlightLayer.backgroundColor = Colors.plainColor.cgColor
        self.highlightLayer.shadowColor = Colors.plainColor.brighter().cgColor
        self.highlightLayer.cornerRadius = 5.0
        self.highlightLayer.shadowOpacity = 0.75
        self.highlightLayer.shadowOffset = CGSize(width: -6, height: -6)
        self.highlightLayer.shadowRadius = 5.0
        self.layer.addSublayer(self.highlightLayer)
    }

    private func putShadow() {
        
        self.shadowLayer.masksToBounds = false
        self.shadowLayer.frame = self.bounds
        self.shadowLayer.backgroundColor = Colors.plainColor.cgColor
        self.shadowLayer.shadowColor = Colors.plainColor.darker().cgColor
        self.shadowLayer.cornerRadius = 5.0
        self.shadowLayer.shadowOpacity = 0.65
        self.shadowLayer.shadowOffset = CGSize(width: 6, height: 6)
        self.shadowLayer.shadowRadius = 5.0
        self.layer.addSublayer(self.shadowLayer)
    }
    
    @objc dynamic func onPushed() {
        
        self.highlightLayer.removeFromSuperlayer()
        self.shadowLayer.removeFromSuperlayer()
        self.putDentedVerticalLayer()
        self.putDentedHorizontalLayer()
    }
    
    private func putDentedVerticalLayer() {
        
        self.dentedVerticalLayer.cornerRadius = 5.0
        self.dentedVerticalLayer.frame = self.bounds
        self.dentedVerticalLayer.colors = [
            Colors.plainColor.darker().cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.brighter().cgColor
        ]
        self.dentedVerticalLayer.locations = [
            0,
            0.15,
            0.85,
            1
        ]
        self.dentedVerticalLayer.opacity = 1
        self.layer.insertSublayer(self.dentedVerticalLayer, at: 0)
    }
    
    private func putDentedHorizontalLayer() {
        
        self.dentedHorizontalLayer.cornerRadius = 5.0
        self.dentedHorizontalLayer.frame = self.bounds
        self.dentedHorizontalLayer.colors = [
            Colors.plainColor.darker().cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.brighter().cgColor
        ]

        self.dentedHorizontalLayer.locations = self.getProperWidthLocations(size: self.bounds.size)
        self.dentedHorizontalLayer.startPoint = CGPoint(x: 0, y: 0)
        self.dentedHorizontalLayer.endPoint = CGPoint(x: 1, y: 0)
        self.dentedHorizontalLayer.opacity = 0.5
        self.layer.insertSublayer(self.dentedHorizontalLayer, at: 1)
    }
    
    @objc dynamic func onReleased() {
        
        self.dentedVerticalLayer.removeFromSuperlayer()
        self.dentedHorizontalLayer.removeFromSuperlayer()
        self.putHighlight()
        self.putShadow()
    }
    
    private func getProperWidthLocations(size: CGSize) -> [NSNumber] {
        if (size.width >= size.height*2 &&
            size.width < size.height*3) {
            return [
                0,
                0.075,
                0.925,
                1
            ]
        } else if (size.width >= size.height*3 &&
            size.width < size.height*4) {
            return [
                0,
                0.05,
                0.95,
                1
            ]
        } else if (size.width >= size.height*4 &&
            size.width < size.height*5) {
            return [
                0,
                0.0325,
                0.9675,
                1
            ]
        } else {
            return [
                0,
                0.15,
                0.85,
                1
            ]
        }
    }
    
    private func getProperHeightLocations(size: CGSize) -> [NSNumber] {
        if (size.width*2 >= size.height &&
            size.width*3 < size.height) {
            return [
                0,
                0.075,
                0.925,
                1
            ]
        } else if (size.width*3 >= size.height &&
            size.width*4 < size.height) {
            return [
                0,
                0.05,
                0.95,
                1
            ]
        } else if (size.width*4 >= size.height &&
            size.width*5 < size.height) {
            return [
                0,
                0.0325,
                0.9675,
                1
            ]
        } else {
            return [
                0,
                0.15,
                0.85,
                1
            ]
        }
    }
}
```

## 解説
まず、イニシャライザで`putHighlight()`というメソッドと、`putShadow()`というメソッドを呼び出します。それぞれのメソッドで光と影をボタンの外側につけます。

`putHighlight()`では、`plainColor`を少し明るくしたものを、ボタンから(-6, -6)くらいの位置まで届くように表示しています。
逆に`putShadow()`では、`plainColor`を少し暗くしたものを、(6, 6)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。

<img width="451" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/97a21f62-2f20-5b9c-3003-5fd8a668c4cc.png">

凹ませた状態を作るのがちょっと厄介で、
まず`CAGradientLayer`を使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、`locations`の数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うように`getProperHeightLocations()`のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。

<img width="482" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/dd3d2b80-697a-e0ea-6296-3f51e162ac83.png">

これで、gifのようなボタンが再現できると思います。

# おわり
けっこう楽しいので、この調子で色々な部品を作ってみようと思います。