NeumorphismをSwiftUIで簡単に実装できるライブラリ、Neumorphismicを作ってみた

先日[Neumorphism: 令和時代のスキューモーフィズム](https://qiita.com/touyoubuntu/items/bf887093ec7265efe201)を読みました。

これが流行る頃にはSwiftUIも使えるようになってるだろう…ということでSwiftUIでNeumorphismのライブラリを作ってみました。

SwiftPMにしか対応していませんが、Xcode 11からとても使いやすくなりましたし、SwiftUIのOSSなのでこれで十分だと思います。[スターつけてくださると嬉しいです！](https://github.com/mcasashi-aso/Neumorphismic)

# 完成形

Demoの画面です。SwiftUIということで全プラットフォームのDemoを一応作りましたが、iOS以外は図形が表示されるだけの手抜きです。
<img width="375" alt="Demo view" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/da601bf5-0387-9c0d-0e2a-de6cc5e1e38f.png">
<img width="375" alt="Settings view" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/9b5782fc-9f20-53f5-ac8c-9d4c7049e9b5.png">

# 使い方
簡単な使い方はこんな感じです。2層目以降は`ZStack`で背景色をつける必要もないですし、`.environment`も必要ありません。下のコードだと少し複雑に見えるかもしれませんが、各Viewで増えるのは`.modifier(NMConvexModifier())`のみです。下のコードもSwiftUIを少し触った方ならすぐわかると思います。

```swift:SceneDelegate.swift
let contentView = ContentView()
    .environment(\.nmBaseColor, Color(hex: "C1D2EB")
```


```swift:ContentView.swift
struct ContentView: View {

    @Environment(\.nmBaseColor) var baseColor: Color
    
    var body: some View {
        ZStack {
            baseColor
                .edgesIgnoringSafeArea(.all)
            
            Circle()
                .fill(baseColor)
                .frame(width: 300, height: 200)
                .modifier(NMConvexModifier())
        }
    }
}
```
# 作り方

実際のコードとは多少違います。

## ModifierでShadowを実装する

Modifierについては[この記事](https://qiita.com/takaf51/items/8fc8834b2fe03848222e)でひと通りわかると思います。簡単にいうと`.font`とか`.frame`とかをまとめて`View`に適合できるやつです。
~~SwiftUIでもshodowは1つしか追加できません。なので`ZStack`で`View`を2つ重ねてそれぞれに`.shadow`をつける必要があります。~~大嘘です。試したときに見えづらい色でやってたのが悪かった…。ver 1.1以降は直してあります。

```swift
struct ConvexModifier: ViewModifier {
    let lightShadowColor: Color
    let darkShadowColor: Color
    func body(content: Content) -> some View {
        content
            .shadow(color: darkShadowColor, radius: 16, x: 9, y: 9)
            .shadow(color: lightShadowColor, radius: 16, x: -9, y: -9)
    }
}
```

しかし、これでは影の色をいちいち入力する必要があります。

## 色をEnvironmentで伝搬する

SwiftUIでは`@Environment`を使うことでその子`View`全てに値を伝えることができます。詳しくは[この記事](https://qiita.com/1amageek/items/59c6bb32a6627b4fb712)を読んでください。
そしてこれは自作することもできます。Neumorphismでは基本的に`View`に1色しか使わないためこれが非常に有効です。さらに、`ConvexModifier`も`baseColor`を基準に影の色を決めればよくなります。自作する方法は[こちら](https://qiita.com/shiz/items/3b829b1521f9723aa875)をご覧ください。

```swift
struct BaseColorKey: EnvironmentKey {
    static let defaultValue: Color = .gray
}

extension EnvironmentValues {
    var baseColor: Color {
        get { self[BaseColorKey.self] }
        set { self[BaseColorKey.self] = newValue }
    }
}
```

## さて、色を変換したいわけだけど…

パッと見、SwiftUIの`Color`からはrgbやhueや取得できそうにありません。しかし`#C1D2EBFF`のようにカラーコードを返してくれる`.description`があります。ということで[カラーコードからColorを生成(このリンクではUIColor)](https://qiita.com/Kyome/items/eae6216b13c651254f64)できるようにし、[RGBとHSL](https://www.peko-step.com/tool/hslrgb.html)、[RGBとHSB](https://www.peko-step.com/tool/hsvrgb.html)の変換コードを用意します。

`Color`から色情報を取れるとわかったので、早速`lighterColor`を実装します。

```swift
func getRGBA() -> (r: Double, g: Double, b: Double, a: Double) {
    let string = String(self.description.dropFirst())
    let v = Int(string, radix: 16) ?? 0

    let r = Double(v / Int(powf(256, 3)) % 256) / 255
    let g = Double(v / Int(powf(256, 2)) % 256) / 255
    let b = Double(v / Int(powf(256, 1)) % 256) / 255
    let a = Double(v / Int(powf(256, 0)) % 256) / 255

    return (r, g, b, a)
}
    
func getHSLA() -> (h: Double, s: Double, l: Double, a: Double) {
    let (r, g, b, a) = getRGBA()
    let (h, s, l) = ColorTransformer.rgbToHsl(r: r, g: g, b: b)
    return (h, s, l, a)
}

func lighter(_ value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    return Color(hue: h, saturation: s, lightness: min(l + value, 1), opacity: a)
}

func darker(_ value: Double) -> Color {
    let (h, s, l, a) = getHSLA()
    return Color(hue: hsb.h, saturation: hsb.s, lightness: max(l - value, 0), opacity: a)
}
```

```swift
struct ColorExtension_Previews: PreviewProvider {
    static var previews: some View {
        let color = Color(hex: "C1D2EB")
        
        return Group {
            ColorPreview(color)
            ColorPreview(color.lighter(0.12))
            ColorPreview(color.darker(0.18))
        }
        .previewLayout(.fixed(width: 200, height: 100))
    }
}
```

<img width="271" alt="Colors" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/7336a8ef-3e23-aba5-4755-4e5080538fe3.png">
こうなりました。影として使わないとよくわかりませんね。

## ConvexModifierを完成させる

材料は揃ったので合わせてみましょう。

```swift
struct ConvexModifier: ViewModifier {
    
    @Environment(\.baseColor) var baseColor: Color
    
    func body(content: Content) -> some View {
        content
            .shadow(color: baseColor.darker(0.18), radius: 16, x: 9, y: 9)
            .shadow(color: baseColor.lighter(0.12), radius: 16, x: -9, y: -9)
    }
}
```

```swift
struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)
            
            Circle()
                .fill(Color(hex: "C1D2EB"))
                .modifier(ConvexModifier())
                .frame(width: 300, height: 300)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}
```

`.environment`で`baseColor`を伝えるの忘れないようにしましょう。
<img width="244" alt="Image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/abfe5341-885f-6aa7-c54e-ba3365b57929.png">
いい感じですね！

`environment`を使ったので当然

```swift
struct ConvexModifier_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color(hex: "C1D2EB")
                .edgesIgnoringSafeArea(.all)
            
            Circle()
                .fill(Color(hex: "C1D2EB"))
                .frame(width: 300, height: 300)
                .modifier(ConvexModifier())
                // `environment`で影の色を変える
                .environment(\.baseColor, Color.red)
        }
        .environment(\.baseColor, Color(hex: "C1D2EB"))
    }
}
```

とできるだろうと思っていたのですができませんでした。`.red`の時は黒い影が出てきて、その他の場合は影がなくなりました。`Color(hex:)`を使ったら行けたのでよくわかりません。

## Highlight時に表示を変えられるButtonも作ってみた

ハイライト時にNeumorphismではどうするのが正解なんだろうと考えるためにとりあえず作ってみました。個人的に標準`Button`は ハイライト時に色が薄すぎる気がするのでこれを使ってますが、標準の`Button`でもいい気がします。押下時にボタンを小さくしたいときなどに使ってみてください。

```swift
struct HighlightableButton<Label>: View where Label: View {
    
    private let action: () -> Void
    private let label: (Bool) -> Label
    
    public init(
        action: @escaping () -> Void,
        label: @escaping (Bool) -> Label
    ) {
        self.action = action
        self.label = label
    }
    
    @State private var isHighlighted = false
    
    public var body: some View {
        label(isHighlighted)
            .animation(.easeOut(duration: 0.05))
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    withAnimation { self.isHighlighted = true }
                }
                .onEnded { _ in
                    self.action()
                    withAnimation { self.isHighlighted = false }
                }
        )
    }
}
```

```swift
struct ConvexModifier_HighlightableButton_ForPreviews: View {
    @State var isSelected = false
    var body: some View {
        HighlightableButton(action: {
            self.isSelected.toggle()
        }) { isH in
            Image(systemName: self.isSelected ? "house.fill" : "house")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 60)
                .foregroundColor(Color(hex: "C1D2EB").darker(value: 0.18))
                .background(
                    Circle()
                        .fill(Color(hex: "C1D2EB"))
                        .frame(width: 100, height: 100)
                        .modifier(ConvexModifier())
                )
                .opacity(isH ? 0.6 : 1)
        }
    }
}
```

`.modifier(ConvexModifier())`は`Button`の中に書いてください(標準の`Button`でも同じです)。外に書くときちんと適用されません。`ForPreviews`みたいな名前になっているのは、`PreviewProvider`の中には`@State`が置けないからです。
<img width="244" alt="GIF" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/d6e9f5ff-159d-47e6-bb2e-ea82adc181f9.gif">

## FloatingTabViewも作ってみた

[@touyou](https://qiita.com/touyoubuntu)さんもタブを作ってることだし作るかーみたいな感じで作ってみました。`ViewBuilder`を読み解こうとまでは思わなかったので(そもそもできるんですかね)、標準の`TabView`のような綺麗さはありませんが、まあ使えるのではないでしょうか。タブの数は4つまでにしてみました。標準の`TabView`にあるその他を実装する気力はなかったので切り捨てています。
使い方はこんな感じです。

```swift
struct FloatingTabView_ForPreviews: View {
    enum Season: String, CaseIterable {
        case spring, summer, fall, winter
        var color: Color {
            switch self {
            case .spring: return .pink
            case .summer: return .blue
            case .fall:   return .orange
            case .winter: return .white
            }
        }
    }
    @State var season: Season = .spring
    var body: some View {
        FloatingTabView(selection: $season, labelText: { s in
            s.rawValue
        }, labelImage: { _ in Image(systemName: "camera") }) { s in
            s.color.edgesIgnoringSafeArea(.all)
        }
    }
}
```

例を作るのが面倒なのが目に見えますね…。`labelImage`なんてカメラだけですし。この例では色を変えていますが、Neumorphismでは色を変えるのはご法度なので注意。(もちろん局所的にアクセントとして使うのはOKです)
<img width="299" alt="Screen Shot 2020-01-12 at 20.34.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186526/90ff0397-6c79-f68c-7d22-55c8a5eee049.png">
ああ、影の色が汚い…。まあ色変えるとこうなるよっていう悪い例と思ってください。
ちなみに`GeometryReader`のせいかLive PreviewにしていないとTabが下に落ちてしまいました。SwiftUIは7不思議どころじゃありません。そこら辺に穴が一杯です。全く理由がわからず何となくLive Previewにしてみたところ合っていたことがわかりました。恐ろしや。

## 凹も作りたかったけど

凹凸どちらも作りたかったのですが、凹の方はいい案が思い浮かばず、凸だけになってしまいました。適合したい`aView`よりひとまわり大きい`bView`を作って、そこから`aView`の大きさを切り抜いて、`aView`の上に`bView`を2つ置いて影をつければ行けそうだなとは思ったんですが、くり抜く方法がわかりませんでした。`Path`を使えば何とかなりそうですが、`View`の形を取る方法もないですし…。


# 初OSS & 初Qiita🎉

cocoapodsなどにも対応したかったのですが、SwiftUIのOSSですし、自分の学ぶ量としてもSwiftPMだけで十分だろうとSPMのみになっています。それでも十分つまづきまくりましたし。
初めてのOSSなので、改善点などたくさんあると思います。もしなんかあったら[Twitter](https://mobile.twitter.com/MasasaM_shi)などに送ってくださると嬉しいです！(もちろんissueでも)

Qiitaも初投稿ですが、そろそろ開発から離れて受験勉強しないと浪人する未来しか見えないので当分記事を書くことはないでしょう。1年後に戻って来れるように頑張ります。

いいねとスターつけてくださると嬉しいです！
[Neumorphismic - GitHub](https://github.com/mcasashi-aso/Neumorphismic)