관리 메뉴

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

[iOS - swift] 1. Section, Item 모델링 - 단일 Section, 다중 Item 모델링 방법 (UITableView, UICollectionView) 본문

iOS 실전 (swift)

[iOS - swift] 1. Section, Item 모델링 - 단일 Section, 다중 Item 모델링 방법 (UITableView, UICollectionView)

jake-kim 2022. 5. 26. 22:56

1. Section, Item 모델링 - 단일 Section, 다중 Item 모델링 방법 (UITableView, UICollectionView)

2. Section, Item 모델링 - 다중 Section, 다중 Item 모델링 방법 (UITableView, UICollectionView)

하나의 tableView안에 2개의 셀이 존재 (Cell이 비슷하여 Cell은 하나만 사용)

예제에 사용한 framework

  • Cell안의 UI 인터렉션 처리를 위해 일반적으로 많이 사용하는 비동기 처리 방식 예제를 보이기 위해 아래 프레임워크 사용
  • RxSwift와 RxCocoa 사용

다중 Item 모델링 아이디어

  • ViewController에서 UITableView를 가지고 있고 이 tableView의 셀 처리를 쉽게 하고싶은 경우 모델링이 중요
  • tableView에서 사용할 데이터 소스의 형태는 아래와 같은 형태
var items = [MyItem]()
  • MyItem은 enum으로 정의하고 associative type으로 각 셀에 표출할 데이터 모델 사용
    • associative type에 모델을 배열 타입으로 넣어서 받도록 구현할 수 있지만, cell을 처리하는 쪽에서 불편하므로 배열이 아닌 단일 Model로 정의
enum MyItem {
  case normal
  case special(text: String)
}

위같은 형태로 사용할때의 장점)

  • 장점 1) cellForRowAt 델리게이트에서 셀을 정의할 때 items[indexPath.row]로 놓고 switch문으로 셀 타입을 쉽게 구분이 가능
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
  switch self.items[indexPath.item] {
  case .normal:
    cell.prepare(text: nil)
    return cell
  case let .special(text):
    cell.prepare(text: text)
    cell.buttonTapObservable
      .throttle(.microseconds(500), scheduler: MainScheduler.asyncInstance)
      .bind { print("tap button!") }
      .disposed(by: cell.disposeBag)
    return cell
  }
}
  • 장점 2) 셀이 탭되면 호출되는 didSelectRowAt 델리게이트 메소드에서도 쉽게 구분이 가능
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    switch self.items[indexPath.row] {
    case .normal:
      print("did tap normal cell")
    case .special:
      print("did tap normal special")
    }
  }

뷰와 사용하는 쪽

  • 셀 정의
import UIKit
import RxSwift

final class MyCell: UITableViewCell {
  private let myImageView: UIImageView = {
    let image = UIImageView()
    image.image = UIImage(named: "dog")
    image.isUserInteractionEnabled = false
    image.translatesAutoresizingMaskIntoConstraints = false
    return image
  }()
  private let label: UILabel = {
    let label = UILabel()
    label.text = "special 셀"
    label.font = .systemFont(ofSize: 24)
    label.numberOfLines = 0
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()
  private let button: UIButton = {
    let button = UIButton()
    button.setTitleColor(.systemBlue, for: .normal)
    button.setTitleColor(.blue, for: .highlighted)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
  }()

  var buttonTapObservable: Observable<Void> {
    self.button.rx.tap.asObservable()
  }
  var disposeBag = DisposeBag()
  
  @available(*, unavailable)
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    self.contentView.addSubview(self.myImageView)
    self.contentView.addSubview(self.label)
    self.contentView.addSubview(self.button)
    
    NSLayoutConstraint.activate([
      self.myImageView.bottomAnchor.constraint(lessThanOrEqualTo: self.contentView.bottomAnchor),
      self.myImageView.topAnchor.constraint(greaterThanOrEqualTo: self.contentView.topAnchor),
      self.myImageView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor),
      self.myImageView.widthAnchor.constraint(equalToConstant: 120),
      self.myImageView.heightAnchor.constraint(equalToConstant: 120)
    ])
    NSLayoutConstraint.activate([
      self.label.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor),
      self.label.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor),
    ])
    NSLayoutConstraint.activate([
      self.button.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -16),
      self.button.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor),
    ])
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    
    self.disposeBag = DisposeBag()
    self.prepare(text: nil)
  }
  
  func prepare(text: String?) {
    self.button.isHidden = text == nil
    self.label.isHidden = text == nil
    print(self.label.isHidden)
    self.button.setTitle(text, for: .normal)
  }
}
  • 예제 데이터 준비
  // ViewController.swift
  
  var items: [MyItem] = [
    .normal,
    .special(text: "버튼1"),
    .normal,
    .special(text: "버튼2"),
    .special(text: "버튼3"),
    .normal,
  ]
  • tableView 준비
  //  ViewController.swift
  
  private let tableView: UITableView = {
    let view = UITableView()
    view.allowsSelection = false
    view.backgroundColor = .clear
    view.separatorStyle = .none
    view.bounces = true
    view.showsVerticalScrollIndicator = true
    view.contentInset = .zero
    view.register(MyCell.self, forCellReuseIdentifier: "MyCell")
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  • 테이블 뷰 레이아웃과 델리게이트 할당
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.view.addSubview(self.tableView)
    NSLayoutConstraint.activate([
      self.tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
      self.tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
      self.tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
      self.tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
    ])
    
    self.tableView.dataSource = self
    self.tableView.delegate = self
  }
  • 델리게이트 정의
extension ViewController: UITableViewDataSource, UITableViewDelegate {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    self.items.count
  }
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
    switch self.items[indexPath.item] {
    case .normal:
      cell.prepare(text: nil)
      return cell
    case let .special(text):
      cell.prepare(text: text)
      cell.buttonTapObservable
        .throttle(.microseconds(500), scheduler: MainScheduler.asyncInstance)
        .bind { print("tap button!") }
        .disposed(by: cell.disposeBag)
      return cell
    }
  }
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    switch self.items[indexPath.row] {
    case .normal:
      print("did tap normal cell")
    case .special:
      print("did tap normal special")
    }
  }
}

cf) 만약 다중 Section을 사용하면 아래처럼 Item들을 배열로 넣고 처리..?

-> 다음 포스팅 글에서 확인

// 모델 정의
enum MySection {
  case profile([MyItem1])
  case option([MyItem2])
  case title([MyItem3])
  
  enum MyItem1 {
    case normal
    case special(text: String)
  }
  
  enum MyItem2 {
    case someItem
    case someSpecialItem
  }
  
  enum MyItem3 {
    case otherItem
    case someOtherItem
  }
}

// 사용하는 쪽
let sections: [MySection]

 

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

Comments