先日Neumorphism: 令和時代のスキューモーフィズムを読みました。
これが流行る頃にはSwiftUIも使えるようになってるだろう…ということでSwiftUIでNeumorphismのライブラリを作ってみました。
SwiftPMにしか対応していませんが、Xcode 11からとても使いやすくなりましたし、SwiftUIのOSSなのでこれで十分だと思います。スターつけてくださると嬉しいです!
完成形
Demoの画面です。SwiftUIということで全プラットフォームのDemoを一応作りましたが、iOS以外は図形が表示されるだけの手抜きです。
使い方
簡単な使い方はこんな感じです。2層目以降はZStack
で背景色をつける必要もないですし、.environment
も必要ありません。下のコードだと少し複雑に見えるかもしれませんが、各Viewで増えるのは.modifier(NMConvexModifier())
のみです。下のコードもSwiftUIを少し触った方ならすぐわかると思います。
let contentView = ContentView()
.environment(\.nmBaseColor, Color(hex: "C1D2EB")
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つしか追加できません。なのでZStack
でView
を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色しか使わないためこれが非常に有効です。さらに、ConvexModifier
もbaseColor
を基準に影の色を決めればよくなります。自作する方法はこちらをご覧ください。
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とHSL、RGBと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))
}
}
ConvexModifierを完成させる
材料は揃ったので合わせてみましょう。
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)
}
}
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
を伝えるの忘れないようにしましょう。
いい感じですね!
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
が置けないからです。
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です)
ああ、影の色が汚い…。まあ色変えるとこうなるよっていう悪い例と思ってください。
ちなみに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