ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI에서 Tooltip 구현하기
    SwiftUI 2023. 6. 22. 14:33

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 SwiftUI에서 간단히 Tooltip을 직접그려보며 Shape를 다뤄볼까 합니다🙋🏻

     

    ⚠️ 주의 - 아주 간단히 Shape를 다뤄보는것에 초점을 맞췄기에 간단할 수 있음!

     

    우선 두가지가 필요합니다.

    하나는 삼각형 모양의 Shape를 만드는것 그리고 하나는 툴팁 텍스트가 들어갈 라운드된 Rectangle 뷰를 만드는것!

     

    먼저 삼각형 모양의 엣지를 가진 Shape를 만들어볼께요.

     

    Custom Triangle Shape

    private struct CustomTriangleShape: Shape {
      private var width: CGFloat
      private var height: CGFloat
      private var radius: CGFloat
      
      fileprivate init(
        width: CGFloat = 24,
        height: CGFloat = 24,
        radius: CGFloat = 1
      ) {
        self.width = width
        self.height = height
        self.radius = radius
      }
      
      fileprivate func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: CGPoint(x: rect.minX + width / 2 - radius, y: rect.minY))
        path.addQuadCurve(
          to: CGPoint(x: rect.minX + width / 2 + radius, y: rect.minY),
          control: CGPoint(x: rect.minX + width / 2, y: rect.minY + radius)
        )
        path.addLine(to: CGPoint(x: rect.minX + width, y: rect.minY + height))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + height))
        
        path.closeSubpath()
        
        return path
      }
    }

    요렇게 먼저 삼각형을 그려봐요!

    widht와 height 그리고 위 꼭지점만 자연스러움을 나타내기 위해 radius를 줍니다.

    우선 Shape를 상속받아 커스텀한 삼각형을 구성합니다.

    path 메서드를 이용해 직접 그려주는 방식이죠🙋🏻

    여기서 move는 그릴 포인트를 움직여 위치시키는 것이고, addLine은 실제 선을 그리는것입니다.

    그리고 addQuadCurve라는 메서드가 있는데요.

    컨트롤 포인트를 기준으로 베지어 곡선을 이용해 그릴 수 있습니다.

    주로 그래픽 요소 등을 그릴때도 많이 사용되죠!

     

    해당 타입을 통해 만들어진 삼각형은 아래와 같은 모양을 나타내줍니다.

    여기서 radius가 1이라 꼭지점에서 완전 패이지 않고 뭉그러진 꼭지점을 가지죠!

     

    만약 radius의 값이 1보다 크면 어떨까요?

    요렇게 addCurve에서 곡선을 그려주고 있기때문에 radius값이 클수록 더 많이 패인 위 꼭지점을 갖습니다.

     

    현재 자연스러운건 1이니 샘플에서는 1로 해보겠습니다🕺🏻

     

    삼각형이 그려졌으면 이제 삼각형과 겹쳐서 밑에 깔릴 툴팁 메시지를 포함한 Rectangle을 만들어 볼께요🙌

     

    Custom Rectangle Shape

    private struct CustomRectangleShape: View {
      private var text: String
      
      fileprivate init(text: String) {
        self.text = text
      }
      
      fileprivate var body: some View {
        VStack {
          Spacer()
          
          Text(text)
            .font(.title)
            .foregroundColor(.white)
            .padding(.vertical, 8)
            .padding(.horizontal, 16)
            .background(
              RoundedRectangle(cornerRadius:20)
                .fill(.green)
            )
        }
      }
    }

    요렇게 어떤 텍스트를 받습니다.

    물론 프로퍼티로 padding 값과 cornerRadius 및 color들도 외부에서 커스텀하게 가져와서 결정 지어줄 수도 있죠!

    예시에서는 편의상 내부에 넣었습니다.

     

    요렇게 간단한 Tooltip 하단 모양이 완성되었어요.

     

    그럼 이 두개를 자연스럽게 겹쳐보면서 툴팁처럼 구성해봐야겠죠?

     

    Tooltip Shape

    만들어진 두 모양을 ZStack으로 위치시키며 프레임을 각 구현하려는 디자인에 맞게 잡아줍니다.

    private struct TooltipShape: View {
      fileprivate var body: some View {
        ZStack {
          CustomTriangleShape()
            .fill(.green)
            .padding(.leading, 178)
          
          CustomRectangleShape(text: "Tooltip 입니다!")
        }
        .frame(width: 216, height: 45)
      }
    }

    단순하죠?

    그럼 요렇게 삼각형 하단 부분은 조금 들어가고 자연스러운 Tooltip이 만들어집니다.

     

    그럼 이 Tooltip Shape를 전역적으로 올리기 위한 TooltipView를 구성해볼께요.

     

    TooltipView

    private struct TooltipView: View {
      @Binding var isDisplayTooltip: Bool
      
      fileprivate var body: some View {
        HStack {
          Spacer()
          
          TooltipShape()
            .onTapGesture {
              isDisplayTooltip = false
            }
        }
      }
    }

    영역을 잡아주고 만들어진 TooltipShape를 올려줍니다.

     

    그런데 당연히 툴팁이 계속 보이거나 몇초 뒤에 사라지더라도 바로 눌러서 툴팁을 노출 시키지 않고 싶을 수 있잖아요?

     

    그 구현을 위해 isDisplayTooltip 바인딩 변수를 둡니다.

    그리고 탭 제스쳐에서 해당 값을 변화시켜주죠.

    물론, 이 TooltipView를 사용하는 상위에서 @State 변수로 이 변수와 바인딩 시켜줄거에요!

     

    그럼 마지막으로 이 TooltipView를 올려볼까요?

     

    ContentView

    struct ContentView: View {
      @State var isDisplayTooltip: Bool = true
      
      var body: some View {
        VStack(spacing: 4) {
          HStack {
            Spacer()
            
            Text("Tooltip 안내")
              .padding(.trailing, 16)
          }
          
          if isDisplayTooltip {
            TooltipView(isDisplayTooltip: $isDisplayTooltip)
          }
          
          Spacer()
        }
        .padding()
      }
    }

    실제 툴팁이라는걸 안내해주기 위한 영역에 텍스트를 구성하고 isDisplayTooltip State 프로퍼티를 가지고 툴팁 뷰를 보여줄지 말지 결정해줍니다.

     

    그럼 한번 동작시켜 볼까요?

    이렇게 아주 간단히 툴팁을 구성할 수 있습니다.

     

    그런데 여기서 잠깐 🙋🏻
    보통의 툴팁은 나타나고 탭을 안하더라도 몇초가 지나면 사라지는 경우가 흔합니다.
    그 구현도 한번 녹여볼까요?

     

    Tooltip 자동 사라짐 구현

    struct ContentView: View {
      @State var isDisplayTooltip: Bool = true
      @State private var tooltipOpacity: Double = 1
      @State private var tooltipScale: CGFloat = 1
      
      var body: some View {
        VStack(spacing: 4) {
          HStack {
            Spacer()
            
            Text("Tooltip 안내")
              .padding(.trailing, 16)
          }
          
          if isDisplayTooltip {
            TooltipView(isDisplayTooltip: $isDisplayTooltip)
              .onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
                  isDisplayTooltip = false
                }
              }
          }
          
          Spacer()
        }
        .padding()
      }
    }

    간단히 TooltipView에 onAppear 호출 시 DispatchQueue로 4초 후 사라지도록 구현을 추가하면 해당 기능을 쉽게 표현할 수 있습니다😃

     

    마무리

    자 이렇게 아주아주아주 간단하게 Tooltip을 그리고 동작시키는것에 대해 구현해봤어요!

    해당 샘플 프로젝트는 아래 깃헙 레포에서 확인하실 수 있습니다🙋🏻

    https://github.com/GREENOVER/playground/tree/main/Tooltip

     

    GitHub - GREENOVER/playground: 학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터

    학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

    github.com

Designed by Tistory.