관리 메뉴

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

[iOS - swift] 동그란 썸네일 이미지 구현 방법, 그라데이션 테두리 (UIBezierPath, CAGradientLayer, CAShapeLayer) 본문

UI 컴포넌트 (swift)

[iOS - swift] 동그란 썸네일 이미지 구현 방법, 그라데이션 테두리 (UIBezierPath, CAGradientLayer, CAShapeLayer)

jake-kim 2022. 5. 14. 22:41

* 이 글보다 더 깔끔하게 UI를 구현하고, 온라인 상태를 암시해주는 썸네일을 구현한 포스팅 글은 이 링크 참고

동그랗고 테두리가 있는 thumbnail 이미지 구현

구현 아이디어

  • 원에 강아지 넣기
    • UIView안에 UIImageView를 넣고, UIImageView의 autolayout을 통해 superview와 일정 간격 떨어뜨려서 흰색 여백을 생성
  • 테두리 그라데이션
    • UIImageView의 superview인 containerView.layer에 CAGradientLayer()를 넣어서 그라데이션 입력
    • 그라데이션 색상은 CAGradientLayer()에서 설정하고, 테두리 윤곽선을 따라서 그려지는 레이아웃은 CAShapeLayer()를 통해 레이아웃을 구해서 사용

원에 강아지 넣기

원에 강아지 넣기

  • VC 준비
import UIKit

class ViewController: UIViewController {
}
  • Contant 준비
    • thumbnailSize: 썸네일의 width, height 값 (테두리까지 합한 너비 길이)
    • borderWidth: 테두리의 굵기
    • spacing: 강아지 이미지와 containerView사이의 간격
private enum Constant {
  static let thumbnailSize = 100.0
  static let thumbnailCGSize = CGSize(width: Constant.thumbnailSize, height: Constant.thumbnailSize)
  static let borderWidth = 2.0
  static let spacing = 4.0
}
  • containerView와 imageView 준비
    • cornerRadius값은 길이의 반이되면 정확히 원이 되므로, 길이의 반이 되도록 구현
    • imageView의 길이는 spacing에 의하여 줄어들기 때문에 전체 길이에서 spacing의 2재만큼뺀 길이라고 생각
private let containerView: UIView = {
  let view = UIView()
  view.backgroundColor = .white
  view.translatesAutoresizingMaskIntoConstraints = false
  
  view.layer.cornerRadius = Constant.thumbnailSize / 2.0
  view.layer.borderWidth = Constant.borderWidth
  view.layer.borderColor = UIColor.green.cgColor
  
  return view
}()
private let imageView: UIImageView = {
  let view = UIImageView()
  view.image = UIImage(named: "dog")
  view.layer.cornerRadius = (Constant.thumbnailSize - Constant.spacing * 2) / 2.0
  view.clipsToBounds = true
  view.translatesAutoresizingMaskIntoConstraints = false
  return view
}()
  • 레이아웃
override func viewDidLoad() {
  super.viewDidLoad()
  
  self.view.addSubview(self.containerView)
  self.containerView.addSubview(self.imageView)
  
  NSLayoutConstraint.activate([
    self.containerView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
    self.containerView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
    self.containerView.heightAnchor.constraint(equalToConstant: Constant.thumbnailSize),
    self.containerView.widthAnchor.constraint(equalToConstant: Constant.thumbnailSize),
  ])
  NSLayoutConstraint.activate([
    self.imageView.leftAnchor.constraint(equalTo: self.containerView.leftAnchor, constant: Constant.spacing),
    self.imageView.rightAnchor.constraint(equalTo: self.containerView.rightAnchor, constant: -Constant.spacing),
    self.imageView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor, constant: -Constant.spacing),
    self.imageView.topAnchor.constraint(equalTo: self.containerView.topAnchor, constant: Constant.spacing),
  ])
}

그라데이션 넣기

그라데이션이 들어간 썸네일 이미지

  • 구현하기에 앞서 주의할 점
    • gradient의 영역은 containerView의 외부로 뻗어나가지 않고, 내부로 뻗어나가므로 내부 spacing 처리에 주의
  • 그라데이션 layer를 containerView에 넣을것이기 때문에, containerView에 존재하던 layer관련 설정 삭제
private let containerView: UIView = {
  let view = UIView()
  view.backgroundColor = .white
  view.translatesAutoresizingMaskIntoConstraints = false
  
  // layer 관련 코드 삭제 (3줄)
  view.layer.cornerRadius = Constant.thumbnailSize / 2.0
  view.layer.borderWidth = Constant.borderWidth
  view.layer.borderColor = UIColor.green.cgColor
  
  return view
}()

 

  • 그라데이션의 종류는 linear를 사용하여 구현

* 그라데이션의 개념 (linear, radial, conic)은 이전 포스팅 글 참고 

 

[iOS - swift] CAGradientLayer, Gradation의 종류, axial, radial, conic (linear, circle, sweep)

CAGradientLayer CALayer의 subclass이며, background 색상이나 layer의 색상을 gradient으로 만들 수 있는 인스턴스 gradient 종류는 3가지가 존재 axial (linear) radial (circle) conic (sweep) gradient를..

ios-development.tistory.com

  • 그라데이션을 넣을때 테두리 윤곽선을 알기위해서 뷰가 런타임에 표출된 상태여야 구할 수 있기 때문에 viewDidLayoutSubviews()메소드에서 구현
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
  }
  • 이미 CAGreadientLayer가 속해 있을때는 다시 그라데이션을 넣지 않도록 설정
    guard
      self.containerView.bounds != .zero,
      self.containerView.layer.sublayers?.contains(where: { $0 is CAGradientLayer }) == false
    else { return }
  • gradientLayer 생성
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: Constant.thumbnailCGSize)
gradient.colors = [UIColor.blue, UIColor.green].map(\.cgColor)
  • containerView의 테두리를 알기 위해서 CAShapeLayer와 UIBVezierPath 사용
    • UIBezierPath에서는 insetBy를 통해 containerView
let shape = CAShapeLayer()
shape.lineWidth = Constant.borderWidth
shape.path = UIBezierPath(
  roundedRect: self.containerView.bounds.insetBy(dx: Constant.borderWidth, dy: Constant.borderWidth),
  cornerRadius: Constant.thumbnailSize / 2.0
).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
  • containerView에 삽입
self.containerView.layer.addSublayer(gradient)

* Tip)

  • CAGradientLayer과 CAShapeLayer를 전역에 선언해놓고, viewDidLayoutSubviews()에서 frame값만 바꾸어주면 더욱 단순화가 가능
// 전역에 선언
private var gradientLayer: CAGradientLayer = {
  let layer = CAGradientLayer()
  layer.colors = Color.gradationColors.map(\.cgColor)
  return layer
}()
private var shapeLayer: CAShapeLayer() = {
  let layer = CAShapeLayer()
  layer.lineWidth = Metric.borderWidth
  layer.strokeColor = Color.black.cgColor
  layer.fillColor = Color.clear.cgColor
  return layer
}()

// 한번만 실행
self.imageContainerView.layer.addSublayer(self.gradientLayer)
self.gradientLayer.mask = self.shapeLayer

// viewDidLayoutSubviews 메소드와 cell에서 shouldShowGradientLayer값이 변경될때 didSet에서 아래 메소드 호출 - frame값만 업데이트
private func updateGradationLayerIfNeeded() {
  guard
    self.shouldShowGradientLayer,
    self.containerView.bounds != .zero
  else { return }
  
  self.gradientLayer.frame = self.containerView.bounds
  self.shapeLayer.path = UIBezierPath(
    roundedRect: self.containerView.bounds.insetBy(dx: Constant.borderWidth, dy: Constant.borderWidth),
    cornerRadius: Constant.containerViewCornerRadius
  ).cgPath
}
  • UITableViewCell과 같은 곳은 셀이 화면에 보였다가 안보이는 경우, 셀의 레이아웃이 다시 그려지는데 셀이 저 위 커스텀뷰 자체를 가지고 있을때 CALayer의 frame값이 사라지므로 gradientLayer가 사라지는 버그가 존재
    • layoutSubviews에서만이 아닌 셀이 다시 그려질때도 updateGradationLayerIfNeeded()메소드를 호출할것

* 전체 코드: https://github.com/JK0369/ExGradientThumbnail

Comments