UICollectionView에 대해서 알아보기 4편 (Rx + FlexLayout + PinLayout)
이번에는 RxSwfit + MVVM + FlexLayout + PinLayout을 사용해서 구성해보자.
FlexLayout 및 PinLayout을 학습하기 위해서 구글링으로 프리뷰를 하고, 공식문서의 예제를 참고해서 어떻게 사용하는지 알아본 후 공부해 보았다.
이미지 처리는 Kingfisher와 SDWebImage는 사용해 본 경험이 있어서 Nuke를 학습하면서 적용해 보았다.
근데 Nuke 뭔가 좋다,,
무엇보다도 오랜만에 MVVM + Rx를 사용하고 있어서 차근차근 되짚어보자.
<학습코드>
(공식문서 예제)
🌿 결과물 🌿
우선 MVVM + Rx를 학습하기 위해서 위와같은 구조로 작성했다.
APIService의 코드는 다음과 같이 작성했다.
아래는 APIService의 StringSet.URLSet.member의 urlString
"https://my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0"
//
// APIService.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/08/23.
//
import UIKit
import RxSwift
class APIService {
static let urlString = StringSet.URLSet.member
/// APIService Load
static func load<T: Codable>(_ t: T.Type, urlString: String = urlString) -> Observable<[T]> {
return Observable.create { emitter in
let urlRequest = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error { // 에러가 존재한다면
emitter.onError(error)
return
}
guard let data = data else {
let httpResponse = response as! HTTPURLResponse
emitter.onError(
NSError(domain: "🚨 데이터가 존재하지 않습니다",
code: httpResponse.statusCode,
userInfo: nil)
)
return
}
// 올바르게 성공했다면!
let result = try! JSONDecoder().decode([T].self, from: data)
emitter.onNext(result)
emitter.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel() // dispose되는 경우
}
}
}
}
viewModel에서는 이렇게 처리하고 있다.
//
// MembeViewModel.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/08/23.
//
import UIKit
import RxSwift
import RxCocoa
final class MembeViewModel {
let disposeBag = DisposeBag()
/// 서버로부터 받아온 멤버의 정보들
var membersSubject = BehaviorSubject<[Member]>(value: [])
// MARK: - OutPut
var membersDriver: Driver<[Member]>
init() {
membersDriver = membersSubject
.asDriver(onErrorJustReturn: [])
loadMembers()
}
/// 멤버를 로드합니다.
func loadMembers() {
APIService.load(Member.self, urlString: StringSet.URLSet.member)
.take(1)
.subscribe(onNext: { self.membersSubject.onNext($0) })
.disposed(by: disposeBag)
}
}
viewController에서는 ReactorKit을 적용하기 위한 사전단계로 아래처럼 작성했다.
//
// MemberViewController.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/08/23.
//
import UIKit
import RxSwift
import RxCocoa
import SnapKit
class MemberViewController: UIViewController {
let disposeBag = DisposeBag()
let viewModel = MembeViewModel()
private let loadButton = UIButton()
lazy var collectionView: UICollectionView = {
var layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.scrollDirection = .vertical
layout.sectionInset = .zero
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupLoadButton()
setupCollectionView()
bindAction(with: viewModel)
bindState(with: viewModel)
}
private func bindAction(with viewModel: MembeViewModel) {
loadButton.rx.tap
.debug("로드버튼 클릭")
.bind { viewModel.loadMembers() }
.disposed(by: disposeBag)
collectionView.rx.modelSelected(Member.self)
.bind {
let viewController = MemberDetailViewController(member: $0)
self.navigationController?.pushViewController(viewController, animated: true)
}
.disposed(by: disposeBag)
}
private func bindState(with viewModel: MembeViewModel) {
viewModel.membersDriver
.asObservable()
.bind(to: collectionView.rx.items(
cellIdentifier: MemberCell.identifier,
cellType: MemberCell.self)
) { index, item, cell in
cell.configureCell(with: item)
}
.disposed(by: disposeBag)
}
}
extension MemberViewController {
private func setupLoadButton() {
view.addSubview(loadButton)
loadButton.setTitle("Load API", for: .normal)
loadButton.backgroundColor = .black
loadButton.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide)
$0.leading.trailing.equalToSuperview()
}
}
private func setupCollectionView() {
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.register(
MemberCell.self,
forCellWithReuseIdentifier: MemberCell.identifier
)
collectionView.snp.makeConstraints {
$0.top.equalTo(loadButton.snp.bottom)
$0.leading.trailing.bottom.equalToSuperview()
}
}
}
extension MemberViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(
width: collectionView.bounds.width,
height: 120
)
}
}
다음은 Cell 부분이다.
이번에 Nuke을 학습하고 적용해 보았다.
//
// MemberCell.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/08/23.
//
import UIKit
import SnapKit
import Nuke
class MemberCell: UICollectionViewCell {
var avatarImage = UIImageView()
var nameLabel = UILabel()
var jobLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: .zero)
setupAvatarImage()
setupNameLabel()
setupJobLabel()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 셀 세팅
func configureCell(with item: Member) {
if let url = URL(string: item.avatar) {
let options = ImageLoadingOptions(failureImageTransition: .fadeIn(duration: 0.5))
Nuke.loadImage(
with: url,
options: options,
into: avatarImage
)
}
nameLabel.text = item.name
jobLabel.text = item.job
}
}
extension MemberCell {
private func setupAvatarImage() {
addSubview(avatarImage)
avatarImage.layer.cornerRadius = 12
avatarImage.backgroundColor = .gray
avatarImage.snp.makeConstraints {
$0.top.leading.equalToSuperview().inset(12)
$0.width.height.equalTo(80)
}
}
private func setupNameLabel() {
addSubview(nameLabel)
nameLabel.adjustsFontSizeToFitWidth = true
nameLabel.snp.makeConstraints {
$0.top.equalTo(avatarImage)
$0.leading.equalTo(avatarImage.snp.trailing).offset(10)
$0.trailing.equalToSuperview().inset(10)
}
}
private func setupJobLabel() {
addSubview(jobLabel)
jobLabel.font = .systemFont(ofSize: 16, weight: .medium)
jobLabel.textColor = .purple
jobLabel.numberOfLines = 0
jobLabel.textAlignment = .left
jobLabel.snp.makeConstraints {
$0.top.equalTo(nameLabel.snp.bottom)
$0.leading.equalTo(nameLabel)
$0.trailing.equalToSuperview().inset(10)
}
}
}
마지막으로 MemberDetailViewController의 전체코드이다.
여기서 주의할 점은 init()에
viewDidLayoutSubviews()에 들어가는 값들 작성하면 레이아웃 안맞음
//
// MemberDetailViewController.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/08/30.
//
import UIKit
import FlexLayout
import PinLayout
import Nuke
class MemberDetailViewController: UIViewController {
// MARK: - Properties
var member: Member // 외부에서 전달되는 값
// MARK: - View
let avatarImage = UIImageView()
let nameLabel = UILabel()
let idLabel = UILabel()
let jobLabel = UILabel()
let ageLabel = UILabel()
// MARK: - FlexLayout
let rootFlexContainer: UIView = UIView()
// MARK: - Life Cycle
init(member: Member) {
self.member = member
super.init(nibName: nil, bundle: nil)
view.addSubview(rootFlexContainer)
rootFlexContainer.flex.define { flex in
flex.addItem()
.alignItems(.center)
.define { flex in
flex.addItem(avatarImage)
.width(300)
.aspectRatio(1)
}
.marginBottom(10)
flex.addItem(idLabel)
.marginLeft(10%)
flex.addItem()
.alignItems(.center)
.define { flex in
flex.addItem(nameLabel).grow(2)
flex.addItem(jobLabel).grow(1)
flex.addItem(ageLabel).grow(1)
}
.grow(1)
flex.addItem().grow(3)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.rootFlexContainer.pin.all(view.safeAreaInsets)
self.rootFlexContainer.flex.layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupAvatarImage()
setupNameLabel()
setupIdLabel()
setupJobLabel()
setupAgeLabel()
}
}
extension MemberDetailViewController {
func setupAvatarImage() {
avatarImage.backgroundColor = .gray
// let urlString = member.avatar.replacingOccurrences(of: "size=50x50&", with: "")
Nuke.loadImage(
with: URL(string: member.avatar)!,
into: avatarImage
)
}
func setupNameLabel() {
nameLabel.font = .systemFont(ofSize: 22, weight: .bold)
nameLabel.text = member.name
nameLabel.textAlignment = .center
}
func setupIdLabel() {
idLabel.font = .systemFont(ofSize: 16, weight: .medium)
idLabel.textColor = .gray
idLabel.text = "# \(member.id)"
}
func setupJobLabel() {
jobLabel.font = .systemFont(ofSize: 16, weight: .medium)
jobLabel.textColor = .black
jobLabel.textAlignment = .center
jobLabel.text = member.job
}
func setupAgeLabel() {
ageLabel.font = .systemFont(ofSize: 16, weight: .regular)
ageLabel.textColor = .gray
ageLabel.textAlignment = .center
ageLabel.text = "\(member.age)"
}
}
(참고)
https://colinch4.github.io/2021-05-14/FlexLayout/
'apple > iOS' 카테고리의 다른 글
[iOS] Swift random String 생성 (0) | 2022.09.01 |
---|---|
[iOS] UICollectionView에 대해서 알아보기 5편 (북마크 구현 및 모델 관리) (1) | 2022.09.01 |
FlexLayout 'YGEnums.h' file not found Error (0) | 2022.08.30 |
[iOS] SwiftUI SceneDelegate, AppDelegate 변경 (0) | 2022.08.25 |
[iOS] UICollectionView에 대해서 알아보기 3편 (동적 Cell) (0) | 2022.08.22 |