본문 바로가기

IOS Swift

[UIKit] ToolTipView 만들기 (iOS-Swift)

툴팁 컴포넌트

 

툴팁 기능

  1. Tip의 위치 조정 가능할 것 (위, 아래, 왼쪽, 오른쪽, 중간)
  2. Label의 Padding 변경 가능할 것

 

툴팁 구현 아이디어

UILabel과 삼각형View를 ContainerView에 포함시키기

 

구현

1. TriangleView

TriangleView가 다시 그려지는 것과 같은 업데이트되는 시점에 BezierPath로 삼각형을 그려주었습니다.

그리고 Tip의 위치가 변경됨에 따라 다시 그려지도록 했습니다.

// Tip의 Y축 위치 조정
enum TipYPosition {
    case top
    case bottom
}
    
// Tip의 X축 위치 조정
enum TipXPosition {
    case left(constant: CGFloat)
    case right(constant: CGFloat)
    case center
}

final class TriangleView: UIView {
    
    var color: UIColor = .white
    
    // 팁의 Y축이 변경되면 다시 그려지도록 함
    var tipYPosition: TipYPosition = .bottom {
        didSet {
            setNeedsDisplay()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isOpaque = false // 뷰 전체 또는 일부가 투명한 경우 false로 설정해야한다고 합니다. (false로 설정 안하니까 TriangleView의 색상이 정상적으로 안나왔습니다.)
                              // 애플문서 참고 - https://developer.apple.com/documentation/uikit/uiview/1622622-isopaque
    }
    
    init(frame: CGRect, tipYPosition: TipYPosition = .bottom) {
        self.tipYPosition = tipYPosition
        super.init(frame: frame)
        self.isOpaque = false
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        
        if tipYPosition == .top {
            let path = UIBezierPath()
            path.move(to: CGPoint(x: 0, y: rect.height))
            path.addLine(to: CGPoint(x: rect.width / 2, y: 0))
            path.addLine(to: CGPoint(x: rect.width, y: rect.height))
            path.close()
            
            color.set()
            path.fill()
        } else {
            
            let path = UIBezierPath()
            path.move(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: rect.width / 2, y: rect.height))
            path.addLine(to: CGPoint(x: rect.width, y: 0))
            path.close()
            
            color.set()
            path.fill()
        }
        
    }
    
}

 

 

2. PaddingLabel

Label의 Padding을 조정 기능을 추가한 Custom PaddingLabel을 만들겠습니다.

@IBDesignable
final class PaddingLabel: UILabel {
    @IBInspectable var topInset: CGFloat = 8.0
    @IBInspectable var bottomInset: CGFloat = 8.0
    @IBInspectable var leftInset: CGFloat = 16.0
    @IBInspectable var rightInset: CGFloat = 16.0

    // 지정된 사각형에 레이블의 텍스트나 그림자를 그립니다.
    override func drawText(in rect: CGRect) {
        let padding = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
        super.drawText(in: rect.inset(by: padding))
    }
    
    // PaddingLabel의 본질적인 크기 설정합니다.
    override var intrinsicContentSize: CGSize {
        let padding = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
        var contentSize = super.intrinsicContentSize
        contentSize.width += padding.left + padding.right
        contentSize.height += padding.top + padding.bottom
        
        return contentSize
    }
}

 

  1. drawText에서 PaddingLabel의 텍스트의 크기를 재정의
  2. 텍스트의 크기가 패딩을 포함하도록 변경되었기 때문에 기존 UILabel 패딩까지 포함하는 크기로 intrinsicContentSize도 재정의

 

3. ContainerView

Tip의 위치가 변경됨에 따라 다시 그려지도록 했습니다.

그리고 backgroundColor 변경 시 툴팁의 색상이 변경되도록 backgroundColor를 재정의 했습니다.

class ToolTipView: UIView {
    
    var paddingLabel: PaddingLabel = {
        let view = PaddingLabel()
        view.text = "안녕하세요?"
        view.numberOfLines = 0
        view.textColor = .white
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private var trianleView: TriangleView = {
        var view = TriangleView(frame: .zero, tipYPosition: .bottom)
        return view
    }()

    /*
    ToolTipView의 backgroundColor 변경 시 containerView의 전체 배경이 아닌
    툴팁 모양의 색깔이 변경되도록 함
    */ 
    override var backgroundColor: UIColor? {
          get {
              return paddingLabel.backgroundColor
          }
          set {
              paddingLabel.backgroundColor = newValue
              trianleView.color = newValue ?? .white
          }
      }
    
    // 팁의 Y축 위치 조정
    var tipYPosition: TipYPosition = .bottom {
        didSet {
            
            updateAutoLayout()
            trianleView.tipYPosition = tipYPosition
        }
    }
    
    // 팁의 X축 위치 조정
    var tipXPosition: TipXPosition = .center {
        didSet {
            updateAutoLayout()
        }
    }
    
    override init(frame: CGRect) {
        self.trianleView = TriangleView(frame: frame, tipYPosition: self.tipYPosition)
        super.init(frame: frame)
        setup()
    }
 
    required init?(coder: NSCoder) {
        self.trianleView = TriangleView(frame: .zero, tipYPosition: self.tipYPosition)
        super.init(coder: coder)
        setup()
    }
    
    private func setup() {
        self.trianleView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(paddingLabel)
        self.addSubview(trianleView)
        
        updateAutoLayout()
    }
    
    private func layoutTopTip() {

        switch tipXPosition {
        case .center:
            trianleView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
            
        case .left(let constant):
            trianleView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: constant).isActive = true
            
        case .right(constant: let constant):
            trianleView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: constant).isActive = true
        }
        
        NSLayoutConstraint.activate([
            trianleView.widthAnchor.constraint(equalToConstant: 20),
            trianleView.heightAnchor.constraint(equalToConstant: 20),
            trianleView.topAnchor.constraint(equalTo: self.topAnchor),
            
            paddingLabel.topAnchor.constraint(equalTo: trianleView.bottomAnchor),
            paddingLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            paddingLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            paddingLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
    }
    
    private func layoutBottomTip() {
        
        switch tipXPosition {
        case .center:
            trianleView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
            
        case .left(let constant):
            trianleView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: constant).isActive = true
            
        case .right(constant: let constant):
            trianleView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: constant).isActive = true
        }
        
        NSLayoutConstraint.activate([
            paddingLabel.topAnchor.constraint(equalTo: self.topAnchor),
            paddingLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            paddingLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            paddingLabel.bottomAnchor.constraint(equalTo: trianleView.topAnchor),
            
            trianleView.widthAnchor.constraint(equalToConstant: 20),
            trianleView.heightAnchor.constraint(equalToConstant: 20),
            trianleView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
    }
    
    private func removeAutoLayout() {
        self.removeConstraints(self.constraints)
    }
    
    private func updateAutoLayout() {
        removeAutoLayout()
        
        if tipYPosition == .top {
            layoutTopTip()
        } else {
            layoutBottomTip()
        }
    }
    
    func update(text: String) {
        paddingLabel.text = text
    }
}

 

 

 

참고

반응형