관리 메뉴

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

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

iOS 실전 (swift)

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

jake-kim 2022. 5. 27. 22:38

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

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

n-Section, n-Item 구현
구조

예제에 사용한 프레임워크

cf) 만약 1-Section, n-item의 데이터 소스를 처리할 때 RxSwift를 사용할 경우, tableView.rx.items 를 사용하면 편리

 (n-section, n-item에서는 RxDataSources를 사용하여 처리)

self.dataSource
  .bind(to: self.tableView.rx.items) { tableView, row, index in
  	let indexPath = IndexPath(index: index)
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCell
    cell.prepare(text: item)
    return cell
  }
  .disposed(by: self.disposeBag)​

1-Section, n-Item 처리

  • 이전 포스팅 글에서 알아본대로 Section을 따로 모델링 할 필요 없이 enum으로 정의
    • associative type으로 배열을 넣지 않고 하나만의 모델만을 넣는것이 핵심
// 모델 정의
enum MyItem {
  case normal
  case special(MyModel)
}

// 사용하는쪽
var items: [MyItem]

...

let item = items[indexPath.row]

n-Section, n-Item 처리 방법

  • 1-section, n-item일때는 enum안에 case를 여러개 두어서 Item을 여러개 표시했지만, Section이 생기면 모델을 다르게 정의
  • Section과 Item을 감싸고 있는 Wrapping 모델로 생성
struct DiarySectionItemModel {
  enum DiarySection: Equatable {
    case morning(TimeModel) // associative value에 들어가는 값은 section에서의 데이터 값
    case afternoon(TimeModel)
  }
  
  enum Item: Equatable {
    case oneLineSummary(String)  // associative value에 들어가는 값은 item에서 데이터 값
    case memory(DiaryModel)
  }
  
  let model: DiarySection
  let items: [Item]
}
  • 데이터 채워지는 쪽 (RxDataSources를 사용한 경우 아래처럼 입력이 가능)
    • model과 items부분을 나누어, model부분에는 section을 넣고 items부분에는 아이템들을 대입
  var data: [DiarySectionItemModel.Model] = [
    .init( // morning 섹션
      model: .morning(섹션 데이터...),
      items: [ // morning 섹션 안에 item들
        .oneLineSummary(아이템 데이터...),
        .memory(...),
        .memory(...)
      ]
    ),
    .init( // afternoon 섹션
      model: .afternoon(섹션 데이터...),
      items: [ // afternoon 섹션 안에 item들
        .oneLineSummary(아이템 데이터...),
        .memory(...),
        .memory(...)
      ]
    ),
  ]

예제 내용

  • 섹션이 여러개 있고, 섹션 안에 아이템도 여러개 존재
  • 헤더의 스타일이 두 가지가 있으므로, 섹션도 두 가지가 필요
  • 아이템도 여러가지가 있으므로 n-Section, n-Item으로 구현 필요

  • 스크롤할때 Header가 상단에 걸쳐져야하므로, 이 기능은 시스템에서 제공해주고 Section의 헤더만 가능하므로 n-Section으로 필수 구현

스크롤 시 Header가 상단에 고정되는 시스템 지원 기능

모델링

  • 셀의 UI에 표시될 모델 정의
import Foundation
import UIKit

struct DiaryModel {
  let date: Date?
  let memory: String?
  let picture: UIImage?
}
  • ExDataSources를 사용하려면 모델이 Equtable을 따라야 하므로 수정
struct DiaryModel: Equatable { // <-
  let date: Date?
  let memory: String?
  let picture: UIImage?
}
  • Section에서 사용하는 데이터 모델 정의 (RxDataSources 사용하려면 역시 Equatable 필요)
struct TimeModel: Equatable { // <-
  let image: UIImage?
  let name: String?
}
  • Section 타입 정의 (Section과 Item을 wrapping하고 있는 하나의 모델로 생성 - 핵심)
    • RxDataSources 형식으로 사용하기위해서 Section과 Item을 하나의 struct안에 넣고 관리하도록 정의 (Wrapping 작업) 
    • 또한 typealias로 Model을 정의 (RxDataSources를 사용하기위해 선언)
import RxDataSources

struct DiarySectionItem {
  typealias Model = SectionModel<DiarySection, Item>
  
  enum DiarySection: Equatable {
    case morning(TimeModel) // associative value에 들어가는 값은 section에서의 데이터 값
    case afternoon(TimeModel)
  }
  
  enum Item: Equatable {
    case oneLineSummary(String)  // associative value에 들어가는 값은 item에서 데이터 값
    case memory(DiaryModel)
  }
}
  • 데이터 준비
    • model과 items부분을 나누어, model부분에는 section을 넣고 items부분에는 아이템들을 대입
    • RxDataSources에서는 model이라는 이름을 쓰는데, 이 model이 section과 동일하다고 기억할것
// ViewController.swift
// 3개의 섹션 생성

var data: [DiarySectionItemModel.Model] = [
  .init( // morning 섹션
    model: .morning(섹션 데이터...),
    items: [ // morning 섹션 안에 item들
      .oneLineSummary(아이템 데이터...),
      .memory(...),
      .memory(...)
    ]
  ),
  .init( // afternoon 섹션
    model: .afternoon(섹션 데이터...),
    items: [ // afternoon 섹션 안에 item들
      .oneLineSummary(아이템 데이터...),
      .memory(...),
      .memory(...)
    ]
  ),
]
  • 데이터소스 적용하는 쪽
    • 데이터가 변경될때마다 tableView.rx.items(dataSource:)를 통해 셀 업데이트
    • dataSources는 RxDataSources에서 지원하는 데이터 형태를 미리 정의하여 주입
  Observable.just(self.data)
    .bind(to: self.tableView.rx.items(dataSource: dataSource))
    .disposed(by: self.disposeBag)
  • dataSource 정의
    • item들을 분류하여 cell에 prepare()
let dataSource = RxTableViewSectionedReloadDataSource<DiarySectionItem.Model> { dataSource, tableView, indexPath, item in
  // item
  switch item {
  case let .memory(diaryModel):
  case let .oneLineSummary(text):
  }
}
  • Header, Footer는 원래 UITableViewDataSource에 없으므로, RxDataSources에서 지원 x
    • rx를 사용하지 않고 delegate를 할당한 후 적용이 필요
      * 주의) 예제 코드에서는 reusable을 사용안했지만 실제 사용할땐 아래처럼 사용할것 tableView.dequeueReusableHeaderFooterView(withIdentifier: "myKey")
self.dataSource.delegate = self

extension ViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    switch self.data[section].model {
    case let .morning(model):
    case let .afternoon(model):
    }
  }
}

DataSource 처리

  • 테이블 뷰 정의
    • Header와 Cell을 register
private let tableView = UITableView(frame: .zero).then {
  $0.allowsSelection = false
  $0.backgroundColor = UIColor.clear
  $0.separatorStyle = .none
  $0.bounces = true
  $0.showsVerticalScrollIndicator = true
  $0.contentInset = .zero
  $0.register(MorningHeaderView.self, forHeaderFooterViewReuseIdentifier: "MorningHeaderView")
  $0.register(AfternoonHeaderView.self, forHeaderFooterViewReuseIdentifier: "AfternoonHeaderView")
  
  $0.register(DiaryCell.self, forCellReuseIdentifier: "DiaryCell")
  $0.register(OneLineMemoryCell.self, forCellReuseIdentifier: "OneLineMemoryCell")
  $0.estimatedRowHeight = UITableView.automaticDimension
}
  • Header, Footer처리
    • UITableViewDataSource가 아닌 UITableViewDelegate에 위치해있으므로, delegate 할당하여 직접 정의
self.tableView.delegate = self

extension ViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    switch self.data[section].model {
    case let .morning(model):
      return (tableView.dequeueReusableHeaderFooterView(withIdentifier: "MorningHeaderView") as! MorningHeaderView).then {
        $0.prepare(image: model.image, text: model.name)
      }
    case let .afternoon(model):
      return (tableView.dequeueReusableHeaderFooterView(withIdentifier: "AfternoonHeaderView") as! AfternoonHeaderView).then {
        $0.prepare(image: model.image, text: model.name)
      }
    }
  }
}
  • Item은 RxDataSources로 편하게 처리가 가능
    • RxTableViewSectionedReloadDataSource<정의한모델> { 여기서 셀 처리 }
let dataSource = RxTableViewSectionedReloadDataSource<DiarySectionItem.Model> { dataSource, tableView, indexPath, item in
  switch item {
  case let .memory(diaryModel):
    return (tableView.dequeueReusableCell(withIdentifier: "DiaryCell", for: indexPath) as! DiaryCell).then {
      $0.prepare(
        picture: diaryModel.picture,
        date: diaryModel.date,
        memory: diaryModel.memory
      )
    }
  case let .oneLineSummary(text):
    return (tableView.dequeueReusableCell(withIdentifier: "OneLineMemoryCell", for: indexPath) as! OneLineMemoryCell).then {
      $0.prepare(text: text)
    }
  }
}

Observable.just(self.data)
  .bind(to: self.tableView.rx.items(dataSource: dataSource))
  .disposed(by: self.disposeBag)

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

Comments