[SwiftUI] Custom Tooltip view 만들기

 

오랜만입니다!!!

 

지금까지 취준 + 회사일로 정신이 없었는데, 조금씩 적응도 되어가고 있고

 

남는 시간에 기록과 공부를 하기 위해 블로그를 다시 써보려고 합니다!

 

아마도 지금 회사에서 코딩을 하면서 다루는 SwiftUI와 TCA에 대해 앞으로 포스팅을 많이 할 것 같은데

 

오늘은 오랜만에 돌아온 기념으로 간단한 것부터 포스팅해보려고 합니다. (포스팅 내에서는 편한 말투로!)

 

 많은 앱에서 어떤 버튼 혹은 뷰에 대한 설명을 해주기 위해 툴팁뷰 혹은 툴킷을 보여주는 것을 심심치 않게 볼 수 있다. 오늘은 이 부분을 구현해보려고 한다.

 

 

1. Tool tip이란?

SCR-20241017-ngxk.png

 

"사람들이 앱의 기능을 발견하는 데 도움이 되는 을 표시합니다."

 

 iOS 17부터 공식적으로 생긴 TipKit이라는 프레임워크를 보면 위와 같은 설명이 적혀있다.

즉, 어디까지나 도움을 주는 역할이며, 앱의 핵심 기능을 사용하는 데 필수적이어서는 안되고,

사용자가 앱의 숨겨진 기능이나 덜 알려진 기능을 발견할 수 있도록 돕는 도구이다.

 

SCR-20241017-nevn.png

 

하지만 TipKit은 최소버전이 17이다.

즉, 17보다 최소버전이 낮은 앱에는 적용이 불가능하다. 

또한 간단한 텍스트만 담을 툴팁이 필요한 경우가 있을 수 있다.

 

이런 경우를 위해 커스텀 툴팁을 만들어보려고 한다.

 

2. 구현 아이디어

SCR-20241017-nkqh.png

 

구현 아이디어는 간단하다.

 

삼각형을 그리고, 툴팁에 들어갈 Text의 백그라운드 색을 적용한 뒤

라운드한 사각형처럼 보이게 하기 위해서 cornerRadius를 적용한 것이다.

 

이때 조금 더 유연하게 위치를 시킬 수 있도록 삼각형이 거꾸로 있는 경우와

삼각형의 위치를 6개로 나누어서 구현을 하였다.

 

 

3. 코드

 

1. 삼각형 만들기

검색을 해보면 삼각형을 만드는 방법이 나오는데

그 방법을 활용해서 삼각형을 만들었다.

 

하지만 위에서 언급했듯

삼각형이 거꾸로 있는 경우를 고려하여 만들었다.

 

struct Triangle: Shape {
    var pointingUp: Bool
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        if pointingUp {
            path.move(to: CGPoint(x: rect.midX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        } else {
            path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
        }
        path.closeSubpath()
        return path
    }
}

 

 

2. 삼각형 위치 고려하기

그리고 또 하나 고려했다는 포인트가 바로

삼각형의 위치이다.

 

enum ToolTipAlignment {
    case topLeft
    case topRight
    case topCenter
    case bottomLeft
    case bottomRight
    case bottomCenter
}

 

열거형을 사용해서

6가지 경우로 나누었고

이를 활용해서 위치를 조정할 예정이다.

 

 

3. Custom Tool Tip View

이제 위의 열거형과 삼각형을 가지고 

툴팁뷰를 만들어보자.

struct ToolTipView: View {
    let text: String
    let color: Color
    let alignment: ToolTipAlignment
    
    init(_ text: String, color: Color = .white, alignment: ToolTipAlignment = .topLeft) {
        self.text = text
        self.color = color
        self.alignment = alignment
    }
    
    var body: some View {
        VStack(alignment: horizontalAlignment, spacing: -6) {
        // 삼각형이 위에 있을지, 아래에 있을지 나눈 코드
            if alignment == .bottomLeft || alignment == .bottomRight || alignment == .bottomCenter {
                tooltipContent
                tooltipTriangle
            } else {
                tooltipTriangle
                tooltipContent
            }
        }
    }
    
    // 삼각형이 앞쪽에 있을지, 뒤쪽에 있을지, 가운데에 있을지 결정하는 코드
    private var horizontalAlignment: HorizontalAlignment {
        switch alignment {
        case .topLeft, .bottomLeft:
            return .leading
        case .topRight, .bottomRight:
            return .trailing
        case .topCenter, .bottomCenter:
            return .center
        }
    }
    
    // 툴팁에 들어갈 text와 백그라운드 뷰에 관한 코드
    private var tooltipContent: some View {
        Text(text)
            .foregroundColor(.black)
            .font(.system(size: 14))
            .fontWeight(.semibold)
            .padding(8)
            .background(color)
            .cornerRadius(8)
    }
    
    // 일반적인 삼각형과 역삼각형 중 어떤 거를 쓸지 고려하는 코드
    private var tooltipTriangle: some View {
        Triangle(pointingUp: alignment == .topLeft || alignment == .topRight || alignment == .topCenter)
            .fill(color)
            .frame(width: 20, height: 15)
            .padding(.leading, horizontalAlignment == .leading ? 10 : 0)
            .padding(.trailing, horizontalAlignment == .trailing ? 10 : 0)
    }
}

 

우선,

var body부터 보자면 열거형에 따라 

삼각형의 위치를 아래에 둘 건지, 위에 둘건지 나눴다.

 

 그리고 연산 프러퍼티인 tooltipTriangle에서 

case에 따라 일반 삼각형 혹은 역삼각형이 그려지게 만들었다.

 

또 tooltipContent에서는 툴팁 안에 들어갈

string 값을 넣어주고 색을 채워주었고, 코너도 동그랗게 만들었다.

 

 

4. 사용 예시

구현 아이디어에서 보여준 이미지처럼

쓰고 싶은 곳에 ToolTipView를 호출하여 사용하면 끝!!이다.

struct TestToolTipView: View {
    var body: some View {
        ZStack {
            Color.green.ignoresSafeArea()
            VStack {
                HStack {
                    ToolTipView("Top Left", alignment: .topLeft)
                    Spacer()
                    ToolTipView("Top Right", alignment: .topRight)
                }
                .padding()
                
                ToolTipView("Top Center", alignment: .topCenter)
                
                Spacer()
                
                ToolTipView("Bottom Center", alignment: .bottomCenter)
                
                HStack {
                    ToolTipView("Bottom Left", alignment: .bottomLeft)
                    Spacer()
                    ToolTipView("Bottom Right", alignment: .bottomRight)
                }
                .padding()
            }
        }
    }
}

 

 

4. 부족한 부분

아직 해당 코드만 만들어놓고

앱에 적용하지 않았다.

 

그렇기 때문에 anchor와 같은 것은 미처 생각하지 못했다.

즉 실제로 툴팁뷰를 뷰에 적용한다고 하였을 때,

어떤 위치에 어떤 방식으로 적용시켜야 할지

지금 코드로는 어려울 수 있다.

 

이 부분이 해결된다면 더 보완해서 코드를 올려보겠다.