※ gifは、影の大きさを調節する関数を実装する前のものです。
はじめに
NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
このButtonのしくみ
見てもらうとすぐわかるんですが、
クリックされていないときは、このボタンは
- ボタンの外側の上と左に光があたっている。
- ボタンの外側の下と右に影がついている。
というような特徴をもっています。
それだけでなく、

- 押されているときはボタンの内側の上と左に影がついている。
- 押されているときはボタンの内側の下と右に光があたっている。
というような特徴をもっています。
この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。
実際のコード
実際のコードを公開します。
- Colors.swift
- PlainSquareButton.swift
という2つのファイルを使って実装しました。
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
}
}
}
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)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。

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

これで、gifのようなボタンが再現できると思います。
おわり
けっこう楽しいので、この調子で色々な部品を作ってみようと思います。