관리 메뉴

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

[iOS - swift] 동적인 Custom View, Custom Cell 구현 방법 (xib, dynamic width, dynamic height) 본문

UI 컴포넌트 (swift)

[iOS - swift] 동적인 Custom View, Custom Cell 구현 방법 (xib, dynamic width, dynamic height)

jake-kim 2021. 12. 1. 04:03

내용에 따라 row 높이가 동적으로 변하는 셀

Custom View (xib) 기본 개념


  • custom view 인스턴스를 사용하기까지의 개념 이해
    • xib -> nib -> instance
      (아래 구현부에서 계속 상세히 설명)
    • instance는 UIView를 상속한 커스텀 뷰를 만들때 필요하고, UITableViewCell과 같은 커스텀 셀에서는 불필요
      > tableView.register(nib, forCellReuseIdentifier:)할 때 nib파일을 넣어주므로 커스텀 셀에서는 불필요

Custom Cell 준비


  • Cocoa Touch Class로 UITableViewCell 생성

  • cf) cell이 아닌 일반적인 view를 만들때는 .swift파일과 .xib파일 생성
    주의) UITableViewCell을 아래처럼 swift, views 파일 따로 만들면 런타림 오류 발생
    "this class is not key value coding-compliant for the key"
  • .swift 클래스
// MyTableViewCell.swift

class MyTableViewCell: UITableViewCell {
    
}
  • (Custom Cell이 아닌 Custom View인 경우) .xib 파일 설정
    • safe Area Layout Guide 체크 해제 (좌)
    • Size를 Freeform으로 변경 (우)
  • (Custom Cell이 아닌 Custom View인 경우) .xib
    • Files's Owner를 설정하여 해당 .xib를 소유할 클래스를 지정

  • (Custom Cell인 경우) Cell의 id 입력

  • .xib: .swift파일이 아니므로 다른 형식의 파일이고, 컴파일러를 통해 Unarchive되면서 nib파일로 변경
    • 압축해제된 nib데이터를 가지고 앱에서 사용될 instance로 변경
    • custom cell에서는 register 시에 nib파일을 사용
    • 주의) UIView를 상속한 커스텀뷰에서는 아래처럼 커스텀 뷰에서 작업 수행
// UIView를 상속받은 커스텀 뷰를 만들때 nib파일로 인스턴스 작업이 필요

class MyView: UIView {
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setUpView()
    }

    private func setUpView() {
        guard let myView = loadViewFromNib(nib: "MyView") else {
            return
        }
        addSubview(myView)
    }
}

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
    }
}

Custom Cell 구현


  • MyTableViewCell.xib에 UI 컴포넌트 생성 (위치는 코드에서 layoutSubViews에서 계산할 것이므로 오토레이아웃 설정 x)

  • MyTableViewCell.swift에 상수 선언, IBOutlet 연결
import UIKit

class MyTableViewCell: UITableViewCell {
    
    // MARK: Constants
    
    struct LabelConstant {
        static let maximumNumberOfLines = 5
        static let font = UIFont.systemFont(ofSize: 16)
    }
    
    struct Metric {
        static let cellPadding = 16.0
    }
    
	
    // MARK: UI
    
    @IBOutlet weak var messageLabel: UILabel!
    
}
  • 바인딩 model - UI
    // MARK: Binding
    
    func bind(myModel: MyModel) {
        messageLabel.text = myModel.message
        accessoryType = myModel.isDone ? .checkmark : .none
    }
  • 동적 사이즈 적용 방법
    • origin.y, origin.x, width: layoutSubviews()를 재정의하여 계산
      // MARK: Layout
      
      override func layoutSubviews() {
          super.layoutSubviews()
          
          setupDynamicLayout()
      }
      
      private func setupDynamicLayout() {
          messageLabel.numberOfLines = LabelConstant.maximumNumberOfLines
          messageLabel.font = LabelConstant.font
          messageLabel.layer.frame.origin.y = Metric.cellPadding // top
          messageLabel.layer.frame.origin.x = Metric.cellPadding // left
          messageLabel.layer.frame.size.width = contentView.layer.frame.size.width - Metric.cellPadding * 2
          messageLabel.sizeToFit()
      }​
    • height: 뷰 컨트롤러에서 tableView(_:heightForRowAt:)에서 Cell쪽에 접근하여, Cell에서 계산된 height값을 리턴
      // MARK: Cell Height
      
      class func height(width: CGFloat, myModel: MyModel) -> CGFloat {
          let message = myModel.message
          let contentWidth = width - Metric.cellPadding * 2
          let contentsHeight = message.getCalculatedHeight(contentWidth: contentWidth,
                                                           font: LabelConstant.font,
                                                           maximumNumberOfLines: LabelConstant.maximumNumberOfLines)
          let heightWithPadding = contentsHeight + Metric.cellPadding * 2
          return heightWithPadding
      }​
    • 위에서 getCalculatedHeight는 contentWidth와 font, maximumNumberOfLines 값을 인수로 받아서 높이를 리턴하는 함수 정의
      // String+Height
      
      import UIKit
      
      extension String {
          func getCalculatedHeight(contentWidth width: CGFloat, font: UIFont, maximumNumberOfLines: Int = 0) -> CGFloat {
              let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
              let height = self.size(padding: size, font: font, maximumNumberOfLines: maximumNumberOfLines).height
              return height
          }
          
          private func size(padding size: CGSize, font: UIFont, maximumNumberOfLines: Int = 0) -> CGSize {
            let attributes: [NSAttributedString.Key: Any] = [.font: font]
            var size = self.boundingRect(with: size, attributes: attributes).size
            if maximumNumberOfLines > 0 {
              size.height = min(size.height, CGFloat(maximumNumberOfLines) * font.lineHeight)
            }
            return size
          }
          
          private func boundingRect(with size: CGSize, attributes: [NSAttributedString.Key: Any]) -> CGRect {
            let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
            let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil)
            return rect
          }
      }​
    • height 값 사용하는 쪽
      // ViewController.swift
      
      func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
          let message = messageDataSource[indexPath.row]
          let myModel = MyModel(message: message)
          
          let calculatedHeight = MyTableViewCell.height(width: tableView.layer.frame.size.width, myModel: myModel)
          return calculatedHeight
      }​

* 전체 소스 코드: https://github.com/JK0369/ExDynamicHeightCell

Comments