Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] UIBezierPath, CAShapeLayer으로 말풍선 구현 본문

iOS 응용 (swift)

[iOS - swift] UIBezierPath, CAShapeLayer으로 말풍선 구현

jake-kim 2021. 3. 25. 02:19

 

구현된 말풍선

View 사용하여 구현 (가장 간편)


  • 사각형 View위에 작은 tip 추가
  • topView에 path와 layer설정하여 구현
import UIKit
import SnapKit

class ViewController: UIViewController {
  
  private let width = 120.0
  private let height = 120.0
  private let someView = UIView()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.someView.backgroundColor = .systemOrange
    
    self.view.addSubview(self.someView)
    
    self.someView.snp.makeConstraints {
      $0.size.equalTo(120)
      $0.center.equalToSuperview()
    }
    
    let path = CGMutablePath()

    path.move(to: CGPoint(x: self.width / 2 - 10, y: 0)) // 시작 위치
    path.addLine(to: CGPoint(x: self.width / 2, y: -10))
    path.addLine(to: CGPoint(x: self.width / 2 + 10, y: 0))
    path.addLine(to: CGPoint(x: 0, y: 0))

    let shape = CAShapeLayer()
    shape.path = path
    shape.fillColor = UIColor.gray.cgColor

    self.someView.layer.insertSublayer(shape, at: 0)
  }
}

 

Only code 구현 (새로운 뷰 사용 x)


UIView위에 그림을 그리는 원리

  • UIBezierPath로 테두리 밑그림
  • CASahpeLayer에 UIBezierPath객체를 이용해서 색칠 후 UIView.layer.addSublayer
  • 말풍선을 그리기 위해, custom view (xib) 안에 tip 그림을 그려서 구현
  • UIView에서 override할수 있도록 제공하는 draw(_ rect:CGRect) 이용
  • UIBezierPath, CAShapeLayer 개념 참고: ios-development.tistory.com/265

UIBezierPath로 밑그림

 

//
//  BalloonView.swift
//  Balloon
//
//  Created by 김종권 on 2021/03/25.
//

import Foundation
import UIKit

class BalloonView: UIView {

    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var containerStackView: UIStackView!

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setUp()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }

    override class func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
    }

    private func setUp() {
        guard let balloonView = loadViewFromNib(nib: "BalloonView") else {
            return
        }
        addSubview(balloonView)

        // 생성된 뷰의 위치 설정: bottom + centerX
        balloonView.translatesAutoresizingMaskIntoConstraints = false
        balloonView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        balloonView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true

        // 생성된 뷰의 내부 내용에 따른 자동 크기 조절: 현재 view의 width, height 동적으로 조정
        translatesAutoresizingMaskIntoConstraints = false
        widthAnchor.constraint(equalTo: balloonView.widthAnchor).isActive = true
        heightAnchor.constraint(equalTo: balloonView.heightAnchor).isActive = true
    }
}

extension UIView {
    func loadViewFromNib(nib: String) -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nib, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}
  • 말풍선 밑 tip의 높이와 width 정의
class BalloonView: UIView {
    ...
    
    let tipHeight: CGFloat = 6.0
    let tipWidth: CGFloat = 10.0
    
    ...
}
  • draw(_ rect: CGRect) 함수를 override하여 tip 이미지 구현
class BalloonView: UIView {
   
    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        // 위치 정의
        let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
        let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: containerStackView.bounds.size.height + tipHeight)
        let heightWithoutTip = containerStackView.bounds.size.height - 1

        // path 객체로 선분을 잇는 작업
        let path = UIBezierPath()
        path.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
        path.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
        path.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
        path.close()

        // 만든 path 객체를 이용하여 shapeLayer로 색칠, layer.addSublayer에 사용
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.fillColor = containerStackView.backgroundColor?.cgColor
        containerStackView.layer.addSublayer(shapeLayer)

        // masksToBounds = true가 되면 tip이미지가 사라지는 현상
        containerStackView.layer.masksToBounds = false

        // layer의 zPositino default값은 0이므로, 다른 객체보다 앞에 위치하여 보이게끔 설정
        containerStackView.layer.zPosition = 1
    }
    
    ...
    
}

 

Comments