【SwiftUI/JetpackCompose】ドロップシャドウをつくる　~比較で覚える宣言的モバイルUI~

SwiftUIとJetpackComposeで、同セグメントの機能を両フレームワークでつくろうとした時に
- きれいに対応関係がまとまっている
- コピペで動く
- 最もシンプルな実装

そんな記事があったらいいなと思い、備忘録も兼ねて自分が実装してみた範囲でまとめていきたいと思います。初心者ですので、より良い実装をご存知の方がいらっしゃいましたら、ご教示ください。

今回は**ドロップシャドウ編**です。

# 対応関係

【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な公式.shadow修飾子　②自作の修飾子　③自分自身をぼかして重ねる

# SwiftUI

## 公式の.shadow修飾子
```swift:https://developer.apple.com
func shadow(
    color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
    radius: CGFloat,
    x: CGFloat = 0,
    y: CGFloat = 0
) -> some View
```

### 具体例
修飾子として、ビューにつけます。


<details><summary>コードはこちら</summary>

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            // 塗りつぶし
            RoundedRectangle(cornerRadius: 40)
                .fill(Color.green)
                .frame(width: 200, height: 120)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
            
            Spacer().frame(height: 50)
            
            // 枠線のみ
            RoundedRectangle(cornerRadius: 30)
                .stroke(Color.green, lineWidth: 20)
                .frame(width: 180, height: 100)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
            
            Spacer().frame(height: 50)
            
            // 図形
            Image(systemName: "car")
                .resizable()
                .scaledToFit()
                .foregroundColor(Color.green)
                .frame(width: 150)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
        }
    }
}
```
</details>

### 実行結果
塗りつぶし・枠線・図形のどの場合も、いい感じの影ができています。

![swiftui_shadow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201911/44cfffff-a4d0-3f56-57af-6960579db49f.png)



# JetpackCompose
## 1. 公式の.shadow修飾子
こちらにも、公式の出している.shadow修飾子があります。

**.shadow修飾子(Compose 1.2.0-alpha06以降) -> 色変更できる([参考](https://stackoverflow.com/questions/66203515/how-do-i-set-a-shadow-color-for-jetpack-compose#:~:text=Compose%201.2.0%2Dalpha06%20now%20supports%20shadow%20color%20attribute))**
```kotlin:https://developer.android.com
fun Modifier.shadow(
    elevation: Dp,
    shape: Shape = RectangleShape,
    clip: Boolean = elevation > 0.dp,
    ambientColor: Color = DefaultShadowColor,
    spotColor: Color = DefaultShadowColor
): Modifier
```
**.shadow修飾子(上記以前) -> 色変更できない**
```kotlin:https://developer.android.com
fun Modifier.shadow(
    elevation: Dp,
    shape: Shape = RectangleShape,
    clip: Boolean = elevation > 0.dp,
): Modifier
```

### 具体例

<details><summary>コードはこちら</summary>

```kotlin
@Composable
fun ExampleScreen(name: String) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // 塗りつぶし
        Box(
            modifier = Modifier
                .shadow(8.dp, shape = RoundedCornerShape(40.dp))
                .clip(RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Green)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 枠線のみ
        Box(
            modifier = Modifier
                .shadow(elevation = 8.dp, shape = RoundedCornerShape(40.dp))
                .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Transparent)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 図形
        Icon(
            imageVector = Icons.Outlined.DirectionsCar,
            contentDescription = null,
            tint = Color.Green,
            modifier = Modifier
                .shadow(elevation = 8.dp,)
                .size(150.dp)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 図形 with Surface
        Surface(
            elevation = 9.dp,
        ) {
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Green,
                modifier = Modifier
                    .size(150.dp)
            )
        }

    }
}

```
</details>


### 実行結果
塗りつぶし図形はうまくいっていますが、枠線の内側の影がうまくいっていません。図形の影も[こちら](https://stackoverflow.com/questions/71801345/how-to-add-elevation-just-in-the-bottom-of-an-element-jetpack-compose)などを参考に２通りやってみましたが、矩形の影になってしまいます。.shadowのshape引数にimageVectorを代入できたらいいのですが。
![jetpack_compose_shadow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201911/9ef2d088-5490-1475-66a9-90272872ab52.png)


## ではどうするか
1. 自作の修飾子をつくる -> 枠線のみの影にも対応。Modifierとしての取り回しの良さ。
1. ぼかしたそれ自身を後ろに重ねる -> 何にでも使える。Modifier的には使えない。


## 2. 自作の修飾子
[こちら](https://stackoverflow.com/questions/68996525/x-and-y-offset-blur-opacity-and-colour-of-shadow-in-jetpack-compose "StackOverflow")を参考にさせていただきました。
基本的にCanvasで作成したぼかした矩形を背後に追加します。枠線のみの場合にも対応できるように、一部変更を加えています。Canvasで作成することで、修飾子として実装できるのが特徴です。便利な反面、図形には対応できません。

```kotlin
fun Modifier.advancedShadow(
    color: Color = Color.Black,
    alpha: Float = 0.1f,
    cornersRadius: Dp = 0.dp,
    strokeWidth: Dp? = null, // <- 枠線のみの影をつけたい時は値を入れる
    shadowBlurRadius: Dp = 8.dp,
    offsetX: Dp = 3.dp,
    offsetY: Dp = 3.dp,
) = drawBehind {

    val shadowColor = color.copy(alpha = alpha).toArgb()
    val transparentColor = color.copy(alpha = 0f).toArgb()

    drawIntoCanvas {
        val paint = Paint()
        strokeWidth?.let { // <- 枠線のみの影をつけるため追加
            paint.style = PaintingStyle.Stroke
            paint.strokeWidth = strokeWidth.toPx()
        }
        val frameworkPaint = paint.asFrameworkPaint()
        frameworkPaint.color = transparentColor
        frameworkPaint.setShadowLayer(
            shadowBlurRadius.toPx(),
            offsetX.toPx(),
            offsetY.toPx(),
            shadowColor
        )
        val strokeOffset: Float = (strokeWidth?.toPx())?.div(2f) ?: 0f
        it.drawRoundRect(
            strokeOffset,
            strokeOffset,
            this.size.width - strokeOffset,
            this.size.height - strokeOffset,
            cornersRadius.toPx(),
            cornersRadius.toPx(),
            paint
        )
    }
}
```

## 3. 自分自身をぼかして重ねる
シンプルですが、なんだかんだ万能です。.blurが偉大です。
実装の見た目は、あまりスマートではないのが玉にキズです。以下は、「DirectionsCar」アイコンの例です。

:::note warn
.blur()は、Android12以上でないと非対応でした。
:::



```kotlin
Box {
    // 影の部分
    Icon(
        imageVector = Icons.Outlined.DirectionsCar,
        contentDescription = null,
        tint = Color.Gray,
        modifier = Modifier
            .offset(x = 3.dp, y = 3.dp)
            .blur(8.dp)
            .alpha(0.3f)
            .size(150.dp)
    )
    // 本体
    Icon(
        imageVector = Icons.Outlined.DirectionsCar,
        contentDescription = null,
        tint = Color.Green,
        modifier = Modifier
            .size(150.dp)
    )
}
```

### 具体例

<details><summary>コードはこちら</summary>


```kotlin
fun ExampleScreen(name: String) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // 塗りつぶし 自作の修飾子
        Box(
            modifier = Modifier
                .advancedShadow(
                    strokeWidth = 20.dp,
                    cornersRadius = 40.dp,
                )
                .size(width = 200.dp, height = 120.dp,)
                .clip(RoundedCornerShape(40.dp))
                .background(Color.Green)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 枠線のみ 自作の修飾子
        Box(
            modifier = Modifier
                .advancedShadow(
                    strokeWidth = 20.dp,
                    cornersRadius = 40.dp,
                )
                .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Transparent)
        )

        Spacer(modifier = Modifier.height(10.dp))

        // 枠線のみ 自分自身をぼかして重ねる
        Box(contentAlignment = Alignment.Center) {
            // 影の部分
            Box( // 二重構造にしないと、影がクリップされて汚くなる
                modifier = Modifier
                    .offset(x = 3.dp, y = 3.dp)
                    .blur(8.dp)
                    .size(width = 300.dp, height = 200.dp,), // 本体より少し大きくする
                contentAlignment = Alignment.Center
            ) {
                Box(
                    modifier = Modifier
                        .alpha(0.1f)
                        .border(20.dp, Color.Black, RoundedCornerShape(40.dp))
                        .size(width = 200.dp, height = 120.dp,)
                        .background(Color.Transparent)
                )
            }
            // 本体
            Box(
                modifier = Modifier
                    .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                    .size(width = 200.dp, height = 120.dp,)
                    .background(Color.Transparent)
            )
        }

        // 図形 自分自身をぼかして重ねる
        Box {
            // 影の部分
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Black,
                modifier = Modifier
                    .offset(x = 3.dp, y = 3.dp)
                    .blur(8.dp)
                    .alpha(0.1f)
                    .size(180.dp)
            )
            // 本体
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Green,
                modifier = Modifier
                    .size(180.dp)
            )
        }
    }
}
```
</details>

### 実行結果
SwiftUIのように、全てに対応できました。個人的には、シンプルな形は自作の修飾子、複雑な図形には自分自身をぼかして重ねる手法が合っているように思います。
![jetpack_compose_shadow_revised.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201911/b2d9aafc-fe89-e65d-ca94-d226a85f08e3.png)

# まとめ
**ドロップシャドウ**
対応関係を再掲します。
【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な.shadow修飾子　②自作の修飾子　③自分自身をぼかして重ねる

この記事ではSwiftUIびいきな感じになってしまいましたが、JetpackComposeではマテリアルデザインのガイドライン上、自由に影をつけるのをあまり推奨していないのかもしれません。それぞれのフレームワークを比べることで、両社のイデオロギーが垣間見えるのが興味深いです。

ImageVectorからPath/Shapeにできるライブラリがあれば、それでCanvas内で図形も描画するか、公式shadow修飾子のshape引数に入れることで、もしかするとSwiftUIと同じ使い心地の自作の修飾子がつくれるかもしれません。機会があれば挑戦してみたいと思います。