➰ 이전 코드 내용
Apple Framework List(3) : Modal
➰ Combine 개념 정리
Combine(1) : Overview
Combine(2) : Publisher/Subscriber/Operator
💡 Apple Framework List(4)에서 할 것은 Combine 적용!
- Combine을 적용할 View Controller에 Combine을 Import 해야함!
일단, 이전 코드를 정리해볼까?
이전코드
// FrameworkListViewController.swift
import UIKit
class FrameworkListViewController: UIViewController {
// CollectionView 자체를 연결
@IBOutlet weak var collectionView: UICollectionView!
// 데이터를 일단 가져오기
let list: [AppleFramework] = AppleFramework.list
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
typealias Item = AppleFramework
enum Section {
case main
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self // collectionView의 위임을 나에게 하겠다. 내가 담당하겠다!
navigationController?.navigationBar.topItem?.title = "🎀 Apple Frameworks"
// Data, Presentation, Layout
// (1) presentation -> diffable datasource
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FrameworkCell", for: indexPath) as? FrameworkCell else {
return nil
}
cell.configure(item) // item이 AppleFramework와 같음
return cell
})
// (2) data -> snapshot
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(list, toSection: .main)
// dataSource에 Snapshot을 적용시키기
dataSource.apply(snapshot)
// (3) layout -> compositional layout
collectionView.collectionViewLayout = layout()
}
private func layout() -> UICollectionViewCompositionalLayout {
// item, group, section, layout 만들기
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1)) // item의 width는 group너비의 1/3, height는 group높이과 같음
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.33)) // group의 width는 section너비, height는 section너비의 1/3
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3) // item을 3등분으로 균일하게 쓰겠다!
let section = NSCollectionLayoutSection(group: group)
// section 좌우에 안쪽패딩주기
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let framework = list[indexPath.item] // 몇번째 item인지?
// FrameworkDetailViewController 띄우기
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController // FrameworkDetailViewController로 강제 캐스팅
vc.framework = framework // FrameworkDetailView가 떴을 때, 이미 업데이트가 완료된 상태로 뜨게 됨.
// vc.modalPresentationStyle = .fullScreen // fullScreen으로 모달이 뜨게 -> 제스쳐로 모달을 닫을 수 없음
present(vc, animated: true) // present 메소드로 띄워주기
}
}
1️⃣ CollectionView 설정을 위한 Presentation, Layout, Data 따로 빼두기
- Presentation, Layout → configureCollectionView()
- Data → applySectionItems()
// FrameworkListViewController.swift
import UIKit
import Combine
class FrameworkListViewController: UIViewController {
// CollectionView 자체를 연결
@IBOutlet weak var collectionView: UICollectionView!
// 데이터를 일단 가져오기
let list: [AppleFramework] = AppleFramework.list
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
typealias Item = AppleFramework
enum Section {
case main
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self // collectionView의 위임을 나에게 하겠다. 내가 담당하겠다!
navigationController?.navigationBar.topItem?.title = "🎀 Apple Frameworks"
// Collection View Presentation, Layout 설정
configureCollectionView()
// Collection View Data 설정
}
private func configureCollectionView() {
// presentation -> diffable datasource
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FrameworkCell", for: indexPath) as? FrameworkCell else {
return nil
}
cell.configure(item) // item이 AppleFramework와 같음
return cell
})
// layout -> compositional layout
collectionView.collectionViewLayout = layout()
}
private func applySectionItems(_ items: [Item], to section: Section = .main) {
// data -> snapshot
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([section])
snapshot.appendItems(items, toSection: section)
// dataSource에 Snapshot을 적용시키기
dataSource.apply(snapshot)
}
private func layout() -> UICollectionViewCompositionalLayout {
// item, group, section, layout 만들기
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1)) // item의 width는 group너비의 1/3, height는 group높이과 같음
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.33)) // group의 width는 section너비, height는 section너비의 1/3
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3) // item을 3등분으로 균일하게 쓰겠다!
let section = NSCollectionLayoutSection(group: group)
// section 좌우에 안쪽패딩주기
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let framework = list[indexPath.item] // 몇번째 item인지?
// FrameworkDetailViewController 띄우기
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController // FrameworkDetailViewController로 강제 캐스팅
vc.framework = framework // FrameworkDetailView가 떴을 때, 이미 업데이트가 완료된 상태로 뜨게 됨.
// vc.modalPresentationStyle = .fullScreen // fullScreen으로 모달이 뜨게 -> 제스쳐로 모달을 닫을 수 없음
present(vc, animated: true) // present 메소드로 띄워주기
}
}
2️⃣ For Combine!
(1) Publisher 만들기 : PassthroughSubject 이용
let didSelect = PassthroughSubject<AppleFramework, Never>()
💡PassthroughSubject?
- Publisher의 일종인 Subject의 Built-in Type
- Subscriber가 달라고 요청하면, 그때부터 받은 값을 전달해주기만 함 → 가장 최근에 전달한 값을 들고 있지 않음
(2) Subscription set 만들기
var subscriptions = Set<AnyCancellable>()
💡Subscription?
- Subscriber가 Publisher와 연결됨을 나타냄
- Publisher가 발행한 구독 티켓
- Cancellable Protocol을 따름
3️⃣ View(해당 Page) 에서의 핵심로직을 담당할 부분을 Bind 함수에 집어넣기!
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let framework = list[indexPath.item] // 몇번째 item인지?
// FrameworkDetailViewController 띄우기
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController // FrameworkDetailViewController로 강제 캐스팅
vc.framework = framework // FrameworkDetailView가 떴을 때, 이미 업데이트가 완료된 상태로 뜨게 됨.
// vc.modalPresentationStyle = .fullScreen // fullScreen으로 모달이 뜨게 -> 제스쳐로 모달을 닫을 수 없음
present(vc, animated: true) // present 메소드로 띄워주기
}
}
↑ 바로 이 코드를 Bind 함수에 집어 넣는 것!
private func bind() {
// input: 사용자 인풋을 받아서 처리해야할 것
// - item 선택되었을 때 처리
didSelect
.receive(on: RunLoop.main) // UI 변경이니, main thread에서 일어날 수 있게 하기
.sink { [unowned self] framework in
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController
vc.framework = framework
self.present(vc, animated: true)
}.store(in: &subscriptions)
}
→ Subscriber
데이터를 보내는 부분은?
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let framework = list[indexPath.item] // 몇번째 item인지?
// 데이터 보내기!
didSelect.send(framework)
}
}
이제 Output 을 세팅해보자
- @Published 로 property 선언
@Published var list: [AppleFramework] = AppleFramework.list
@Published?
- @Published로 선언된 Property를 Publisher로 만들어줌
- 클래스에 한해서 사용됨 (구조체에서 사용 X)
- type은 let이 아닌, var!
- $을 이용해서 Publisher에 접근 가능
private func bind() {
// input: 사용자 인풋을 받아서 처리해야할 것
// - item 선택되었을 때 처리
didSelect
.receive(on: RunLoop.main) // UI 변경이니, main thread에서 일어날 수 있게 하기
.sink { [unowned self] framework in
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController
vc.framework = framework
self.present(vc, animated: true)
}.store(in: &subscriptions)
// output: data, state 변경에 따라서, UI 업데이트 할 것
// - items 세팅이 되었을 때, 컬랙션뷰를 업데이트
$list
.receive(on: RunLoop.main )
.sink { [unowned self] list in
self.applySectionItems(list)
}.store(in: &subscriptions)
}
🚀 전체 코드
// FrameworkListViewController.swift
import UIKit
import Combine
class FrameworkListViewController: UIViewController {
// CollectionView 자체를 연결
@IBOutlet weak var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
typealias Item = AppleFramework
enum Section {
case main
}
// Combine
var subscriptions = Set<AnyCancellable>()
let didSelect = PassthroughSubject<AppleFramework, Never>()
@Published var list: [AppleFramework] = AppleFramework.list
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self // collectionView의 위임을 나에게 하겠다. 내가 담당하겠다!
navigationController?.navigationBar.topItem?.title = "🎀 Apple Frameworks"
configureCollectionView()
bind()
}
private func bind() {
// input: 사용자 인풋을 받아서 처리해야할 것
// - item 선택되었을 때 처리
didSelect
.receive(on: RunLoop.main) // UI 변경이니, main thread에서 일어날 수 있게 하기
.sink { [unowned self] framework in
let storyboard = UIStoryboard(name: "Detail", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "FrameworkDetailViewController") as! FrameworkDetailViewController
vc.framework = framework
self.present(vc, animated: true)
}.store(in: &subscriptions)
// output: data, state 변경에 따라서, UI 업데이트 할 것
// - items 세팅이 되었을 때, 컬랙션뷰를 업데이트
$list
.receive(on: RunLoop.main )
.sink { [unowned self] list in
self.applySectionItems(list)
}.store(in: &subscriptions)
}
// Collection View Presentation, Layout 설정
private func configureCollectionView() {
// presentation -> diffable datasource
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FrameworkCell", for: indexPath) as? FrameworkCell else {
return nil
}
cell.configure(item) // item이 AppleFramework와 같음
return cell
})
// layout -> compositional layout
collectionView.collectionViewLayout = layout()
}
// Collection View Data 설정
private func applySectionItems(_ items: [Item], to section: Section = .main) {
// data -> snapshot
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([section])
snapshot.appendItems(items, toSection: section)
// dataSource에 Snapshot을 적용시키기
dataSource.apply(snapshot)
}
private func layout() -> UICollectionViewCompositionalLayout {
// item, group, section, layout 만들기
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1)) // item의 width는 group너비의 1/3, height는 group높이과 같음
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.33)) // group의 width는 section너비, height는 section너비의 1/3
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3) // item을 3등분으로 균일하게 쓰겠다!
let section = NSCollectionLayoutSection(group: group)
// section 좌우에 안쪽패딩주기
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let framework = list[indexPath.item] // 몇번째 item인지?
// 데이터 보내기!
didSelect.send(framework)
}
}
👀 PassthroughSubject 말고, CurrentValueSubject로 해보자
CurrentValueSubject?
- PassthroughSubject와 같이, Publisher의 일종인 Subject의 Built-in Type
- Subscriber가 달라고 요청하면, 최근에 가지고 있던 값을 전달하고, 그때부터 받은 값을 전달 → 가장 최근에 전달한 값을 들고 있음
let items = CurrentValueSubject<[AppleFramework], Never>(AppleFramework.list)
→ 초깃값을 넣어줘야 함! AppleFramework.list
bind()에서,,
items
.receive(on: RunLoop.main )
.sink { [unowned self] list in
self.applySectionItems(list)
}.store(in: &subscriptions)
// item이 선택되었을 때 효과 넣기
extension FrameworkListViewController: UICollectionViewDelegate{
// item이 선택되었을 때 호출되는 method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// let framework = list[indexPath.item] // 몇번째 item인지?
let framework = items.value[indexPath.item]
// 데이터 보내기!
didSelect.send(framework)
}
}
→ list를 사용못하니, framework도 다음과 같이 수정
➰ FrameworkDetailViewController도 Combine으로 개선해보자
FrameworkDetailViewController 이전코드
// FrameworkDetailViewController.swift
import UIKit
import SafariServices // 사파리를 띄우기 위한 Framework.
class FrameworkDetailViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
var framework: AppleFramework = AppleFramework(name: "Unknown", imageName: "", urlString: "", description: "")
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI() {
imageView.image = UIImage(named: framework.imageName)
titleLabel.text = framework.name
descriptionLabel.text = framework.description
}
// Learn More 버튼을 클릭했을 때, Action target
@IBAction func learnMoreTapped(_ sender: Any) {
// url 객체 만들기
guard let url = URL(string: framework.urlString) else {
return
}
// Safari View Controller
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
}
Combine 으로 개선시킨 코드
// FrameworkDetailViewController.swift
import UIKit
import SafariServices // 사파리를 띄우기 위한 Framework.
import Combine
class FrameworkDetailViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@Published var framework: AppleFramework = AppleFramework(name: "Unknown", imageName: "", urlString: "", description: "")
var subscriptions = Set<AnyCancellable>()
let buttonTapped = PassthroughSubject<AppleFramework, Never>()
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
private func bind() {
// input : Button 클릭
// 구현할 logic : framework -> url -> safari -> present
buttonTapped
.receive(on: RunLoop.main)
.compactMap { URL(string: $0.urlString) }
.sink { [unowned self] url in
let safari = SFSafariViewController(url: url)
self.present(safari, animated: true)
}.store(in: &subscriptions)
// output : Data setting될 떄, UI 업데이트
$framework
.receive(on: RunLoop.main)
.sink { [unowned self] framework in
self.imageView.image = UIImage(named: framework.imageName)
self.titleLabel.text = framework.name
self.descriptionLabel.text = framework.description
}.store(in: &subscriptions)
}
// Learn More 버튼을 클릭했을 때, Action target
@IBAction func learnMoreTapped(_ sender: Any) {
buttonTapped.send(framework)
}
}
📌 정리,
- Combine을 적용하여 코드 개선
- Publisher, Subscriber - Presentation, Layout, Data 설정은 따로 함수로 빼둠
- Presentation, Layout : configureCollectionView()
- Data → applySectionItems() - 해당 Page의 핵심이 되는 Logic은 bind() 함수에 집어 넣기
(1) input 설정 : 사용자 인풋을 받아서, 처리해야할 것
(2) output 설정 : data, state 변경에 따라서, UI 업데이트 해야할 것
Reference
패스트캠퍼스 온라인 강의
'iOS > Toy project' 카테고리의 다른 글
[iOS : Toy Project] Github Profile (2) : Refactoring (0) | 2022.07.17 |
---|---|
[iOS : Toy Project] Github Profile (1) (0) | 2022.07.16 |
[iOS : Toy Project] Head Space Focus (2) : Navigation (0) | 2022.06.30 |
[iOS : Toy Project] Apple Framework List (3) : Modal (0) | 2022.06.29 |
[iOS : Toy Project] Spotify Paywall : CollectionView, Paging Control (0) | 2022.06.28 |