Help us understand the problem. What is going on with this article?

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

先日Neumorphism: 令和時代のスキューモーフィズムを読みました。

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

SwiftPMにしか対応していませんが、Xcode 11からとても使いやすくなりましたし、SwiftUIのOSSなのでこれで十分だと思います。スターつけてくださると嬉しいです!

完成形

Demoの画面です。SwiftUIということで全プラットフォームのDemoを一応作りましたが、iOS以外は図形が表示されるだけの手抜きです。
Demo view
Settings view

使い方

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

SceneDelegate.swift
let contentView = ContentView()
    .environment(\.nmBaseColor, Color(hex: "C1D2EB")
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についてはこの記事でひと通りわかると思います。簡単にいうと.fontとか.frameとかをまとめてViewに適合できるやつです。
SwiftUIでもshodowは1つしか追加できません。なのでZStackViewを2つ重ねてそれぞれに.shadowをつける必要があります。大嘘です。試したときに見えづらい色でやってたのが悪かった…。ver 1.1以降は直してあります。

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全てに値を伝えることができます。詳しくはこの記事を読んでください。
そしてこれは自作することもできます。Neumorphismでは基本的にViewに1色しか使わないためこれが非常に有効です。さらに、ConvexModifierbaseColorを基準に影の色を決めればよくなります。自作する方法はこちらをご覧ください。

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)できるようにし、RGBとHSLRGBとHSBの変換コードを用意します。

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

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

Colors
こうなりました。影として使わないとよくわかりませんね。

ConvexModifierを完成させる

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

struct ConvexModifier: ViewModifier {

    @Environment(\.baseColor) var baseColor: Color

    func body(content: Content) -> some View {
        ZStack {
            content
                .shadow(color: baseColor.darker(0.18), radius: 16, x: 9, y: 9)
            content
                .shadow(color: baseColor.lighter(0.12), radius: 16, x: -9, y: -9)
        }
    }
}
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"))
    }
}

.environmentbaseColorを伝えるの忘れないようにしましょう。
Image
いい感じですね!

environmentを使ったので当然

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でもいい気がします。押下時にボタンを小さくしたいときなどに使ってみてください。

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 }
                }
        )
    }
}
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が置けないからです。
GIF

FloatingTabViewも作ってみた

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

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です)
Screen Shot 2020-01-12 at 20.34.46.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などに送ってくださると嬉しいです!(もちろんissueでも)

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

いいねとスターつけてくださると嬉しいです!
Neumorphismic - GitHub

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした