【SwiftUI】ArcのかっこいいUIを再現してみた

# はじめに
私はArkというブラウザーを気に入って使っています。
ArkはなんといってもUIがとてもかっこいいです。
この記事では、Arkのボタンにカーソルを合わせると出てくるヒントUIを再現します(iPadOSとmacOSが対象）。
（これ👇）
![スクリーンショット 2023-07-19 20.55.26のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366249/14860dbe-55e8-2949-c077-e8a6caf6c6ba.png)

# Shapeを作る
まず、ArkのヒントUIで使われている特徴的な図形を作ります。

```swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            GeometryReader { geometryProxy in
                let height = geometryProxy.size.height
                
                RoundedCornerShape(
                    topLeftRadius: height / 8,
                    topRightRadius: height / 2,
                    bottomRightRadius: height / 2,
                    bottomLeftRadius: height / 2
                )
            }
            .frame(width: 64, height: 32)
        }
        .padding()
    }
}

struct RoundedCornerShape: Shape {
    let topLeftRadius: CGFloat
    let topRightRadius: CGFloat
    let bottomRightRadius: CGFloat
    let bottomLeftRadius: CGFloat
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // 左上の角
        let topLeftCenter = CGPoint(x: rect.minX + topLeftRadius, y: rect.minY + topLeftRadius)
        path.move(to: CGPoint(x: rect.minX, y: rect.minY + topLeftRadius))
        path.addArc(center: topLeftCenter, radius: topLeftRadius,
                    startAngle: .radians(-Double.pi), endAngle: .radians(-Double.pi / 2), clockwise: false)
        
        // 右上の角
        let topRightCenter = CGPoint(x: rect.maxX - topRightRadius, y: rect.minY + topRightRadius)
        path.addArc(center: topRightCenter, radius: topRightRadius,
                    startAngle: .radians(-Double.pi / 2), endAngle: .zero, clockwise: false)
        
        // 右下の角
        let bottomRightCenter = CGPoint(x: rect.maxX - bottomRightRadius, y: rect.maxY - bottomRightRadius)
        path.addArc(center: bottomRightCenter, radius: bottomRightRadius,
                    startAngle: .zero, endAngle: .radians(Double.pi / 2), clockwise: false)
        
        // 左下の角
        let bottomLeftCenter = CGPoint(x: rect.minX + bottomLeftRadius, y: rect.maxY - bottomLeftRadius)
        path.addArc(center: bottomLeftCenter, radius: bottomLeftRadius,
                    startAngle: .radians(Double.pi / 2), endAngle: .radians(Double.pi), clockwise: false)
        
        return path
    }
}
```

![スクリーンショット 2023-07-20 18.49.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366249/46350910-d925-20d5-b5e3-1644c3c4f4e5.png)

いい感じです😆

# ボタンの右下に表示する
次に、ボタンの右下に表示できるようにします。

1. `GeometryReader`でボタンのサイズを取得し、ヘルプUIをボタンの高さと横幅分より少しボタンに近い位置にずらします。
1. `fixedSize`を使って、与えられたサイズに収まらないテキストが省略されてしまうことを防ぎます。

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            Button("Button") {}
                .buttonStyle(.plain)
                .overlay {
                    helpMenu()
                }
        }
        .padding()
    }
}

extension ContentView {
    private func helpMenu() -> some View {
        GeometryReader { geometryProxy in
            let buttonWidth = geometryProxy.size.width
            let buttonHeight = geometryProxy.size.height
            
            Text("This is Button")
            
                // 2
                .fixedSize(horizontal: true, vertical: false)
                .padding(10)
                .background {
                    GeometryReader { geometryProxy in
                        let width = geometryProxy.size.width
                        let height = geometryProxy.size.height
                        
                        RoundedCornerShape(
                            topLeftRadius: height / 8,
                            topRightRadius: height / 2,
                            bottomRightRadius: height / 2,
                            bottomLeftRadius: height / 2
                        )
                        .fill(.background)
                        .shadow(
                            color: .black.opacity(0.2),
                            radius: 10
                        )
                    }
                }
            
                // 1
                .offset(
                    x: buttonWidth - 2,
                    y: buttonHeight - 2
                )
        }
    }
}
```

![スクリーンショット 2023-07-20 19.13.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366249/b203212c-f53b-26b9-e4c9-0393941e0c2d.png)

ここまでくればほぼ完成です。

# マウスオーバーしたときにヘルプUIを表示する
1. `onHover`でマウスオーバーしているかどうかを取得できます。
1. マウスオーバーした時は少し遅延させてヘルプUIを表示し、カーソルを外した時はすぐにヘルプUIを非表示にします。

```swift
import SwiftUI

struct ContentView: View {
    @State private var showHelpPopup = false
    
    var body: some View {
        VStack {
            Button("Button") {}
                .buttonStyle(.plain)

                // 1
                .onHover { hover in

                    // 2
                    let delay: Double = hover ? 0.5 : 0
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                        showHelpPopup = hover
                    }
                }
                .overlay {
                    if showHelpPopup {
                        helpPopup()
                    }
                }
        }
        .padding()
    }
}

extension ContentView {
    private func helpPopup() -> some View {
        GeometryReader { geometryProxy in
            let buttonWidth = geometryProxy.size.width
            let buttonHeight = geometryProxy.size.height
            
            Text("This is Button")
            
                // 2
                .fixedSize(horizontal: true, vertical: false)
                .padding(10)
                .background {
                    GeometryReader { geometryProxy in
                        let height = geometryProxy.size.height
                        
                        RoundedCornerShape(
                            topLeftRadius: height / 8,
                            topRightRadius: height / 2,
                            bottomRightRadius: height / 2,
                            bottomLeftRadius: height / 2
                        )
                        .fill(.background)
                        .shadow(
                            color: .black.opacity(0.2),
                            radius: 10
                        )
                    }
                }
            
                // 1
                .offset(
                    x: buttonWidth - 2,
                    y: buttonHeight - 2
                )
        }
    }
}
```

https://twitter.com/shota_appdev/status/1681977547159126016?s=61&t=lrBwCNc_DCFzZ5IP1r0zlw

# おわりに
mac標準のヘルプUIは少し地味なので、今後自分のアプリに取り入れていきたいです。この記事が参考になったという方は、いいねとフォローしていただけると嬉しいです☺️