10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Last updated at Posted at 2023-07-20

はじめに

私はArkというブラウザーを気に入って使っています。
ArkはなんといってもUIがとてもかっこいいです。
この記事では、Arkのボタンにカーソルを合わせると出てくるヒントUIを再現します(iPadOSとmacOSが対象)。
(これ👇)
スクリーンショット 2023-07-19 20.55.26のコピー.png

Shapeを作る

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

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

いい感じです😆

ボタンの右下に表示する

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

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

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

マウスオーバーしたときにヘルプUIを表示する

  1. onHoverでマウスオーバーしているかどうかを取得できます。
  2. マウスオーバーした時は少し遅延させてヘルプUIを表示し、カーソルを外した時はすぐにヘルプUIを非表示にします。
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
                )
        }
    }
}

おわりに

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

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?