Combine Framework FREE course: write you first iOS app - use Subscriptions & Publishers like Subject
Combine
프레임워크 사용법 익히기ObservableObject
내 @Published
및 일반 뷰 모델 클래스에서의 AnyPublished
간의 사용 방법 차이CurrentValueSubject
, @Published
등 퍼블리셔 종류에 따른 구독 방법PassthroughSubject
를 통해 데이터 동적 추가class TaskListViewModel {
// replace CurrentValueSubject with @Published
let tasks = CurrentValueSubject<[String], Never>(["Buy Milk"])
// @Published var tasks = ["Buy Milk"]
var addNewTask = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
init() {
// data stream to create new task
addNewTask
.filter {$0.count > 3}
.sink { [weak self] newTask in
guard let self = self else { return }
// self.tasks.append(newTask)
self.tasks.send(self.tasks.value + [newTask])
}
.store(in: &cancellables)
// get initial values at launch like from file system
// save changes to tasks in file system
}
}
tasks
는 다른 뷰에서 UI를 그리기 위해 구독하고 있음addNewTask
의 send
를 통해 이루어짐 @objc private func doneButtonDidTap() {
taskListModel?.addNewTask.send(text)
dismiss(animated: true, completion: nil)
}
addNewTask
의 데이터는 sink
를 통해 내려와 이후 UI를 그리는 데 사용할 tasks
의 데이터에 추가됨 private func addSubscription() {
// add data stream that calls tableView.reloadData() when data changes
taskViewModel.tasks
.sink { [weak self] values in
guard let self = self else { return }
print("Received values: \(values)")
self.tableView.reloadData()
}
.store(in: &cancellables)
}
import UIKit
import Combine
class TodoListViewController: UIViewController {
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
return tableView
}()
private let button: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.background.backgroundColor = .systemBlue
button.configuration = config
button.setTitle("Add New", for: .normal)
button.setTitleColor(.white, for: .normal)
return button
}()
private var taskViewModel = TaskListViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setTodoListViewUI()
addSubscription()
}
private func setTodoListViewUI() {
title = "Tasks"
navigationController?.navigationBar.prefersLargeTitles = true
tableView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200).isActive = true
tableView.dataSource = self
view.addSubview(button)
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
button.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 40).isActive = true
button.addTarget(self, action: #selector(didButtonTap), for: .touchUpInside)
}
private func addSubscription() {
// add data stream that calls tableView.reloadData() when data changes
taskViewModel.tasks
.sink { [weak self] values in
guard let self = self else { return }
print("Received values: \(values)")
self.tableView.reloadData()
}
.store(in: &cancellables)
}
@objc private func didButtonTap() {
let addNewVC = AddNewViewController()
addNewVC.taskListModel = taskViewModel
navigationController?.present(addNewVC, animated: true)
}
}
extension TodoListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return taskViewModel.tasks.value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
cell.textLabel?.text = taskViewModel.tasks.value[indexPath.row]
return cell
}
}
tasks
를 구독할 때 sink
파트에서 tableView.reloadData()
를 호출Combine
을 사용하고 있지만 일반적인 UIKit 프레임워크의 target-action
패턴을 그대로 사용하고 있기 때문에 완전한 반응형 프로그래밍이라고 보기에 부족(RxCocoa
에서의 바인딩과 비교)import UIKit
import Combine
class AddNewViewController: UIViewController {
private let label: UILabel = {
let label = UILabel()
label.text = "Add New Task"
label.textColor = .black
label.font = .preferredFont(forTextStyle: .title1)
return label
}()
private let textField: UITextField = {
let textField = UITextField()
textField.font = .preferredFont(forTextStyle: .headline)
textField.textColor = .black
textField.borderStyle = .roundedRect
return textField
}()
private let cancelButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.background.backgroundColor = .clear
button.configuration = config
button.setTitle("Cancel", for: .normal)
button.setTitleColor(.systemRed, for: .normal)
return button
}()
private let doneButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.background.backgroundColor = .clear
button.configuration = config
button.setTitle("Done", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()
var taskListModel: TaskListViewModel?
private var text = ""
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setAddNewViewUI()
}
private func setAddNewViewUI() {
view.backgroundColor = .systemBackground
label.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
cancelButton.translatesAutoresizingMaskIntoConstraints = false
doneButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addSubview(textField)
view.addSubview(cancelButton)
view.addSubview(doneButton)
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 200).isActive = true
textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
textField.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 50).isActive = true
textField.backgroundColor = UIColor.systemGray6
cancelButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 50).isActive = true
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true
doneButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 50).isActive = true
doneButton.leadingAnchor.constraint(equalTo: cancelButton.trailingAnchor, constant: 50).isActive = true
textField.addTarget(self, action: #selector(updateText), for: .editingChanged)
cancelButton.addTarget(self, action: #selector(cancelButtonDidTap), for: .touchUpInside)
doneButton.addTarget(self, action: #selector(doneButtonDidTap), for: .touchUpInside)
}
@objc private func doneButtonDidTap() {
taskListModel?.addNewTask.send(text)
dismiss(animated: true, completion: nil)
}
@objc private func cancelButtonDidTap() {
dismiss(animated: true, completion: nil)
}
@objc private func updateText() {
text = textField.text ?? ""
}
}
present
시에 그대로 입력받음addNewTask
에게 텍스트를 입력하는 send
가 해당 데이터 추가 이벤트 → tasks
는 해당 addNewTask
의 sink
에서 값이 변화import Foundation
import Combine
class TaskListViewModel {
// replace CurrentValueSubject with @Published
let tasks = CurrentValueSubject<[String], Never>(["Buy Milk"])
// @Published var tasks = ["Buy Milk"]
var addNewTask = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
init() {
// data stream to create new task
addNewTask
.filter {$0.count > 3}
.sink { [weak self] newTask in
guard let self = self else { return }
// self.tasks.append(newTask)
self.tasks.send(self.tasks.value + [newTask])
}
.store(in: &cancellables)
// get initial values at launch like from file system
// save changes to tasks in file system
}
}
tasks
를 데이터 퍼블리셔로 addNewTask
를 해당 tasks
에게 값을 건네주는 연결 통로로 사용하는 뷰 모델@Published
를 사용하지 않는 까닭은 UIKit의 데이터 인풋/아웃풋 타이밍에서 한 스텝 느리기 때문import SwiftUI
struct TodoListView: View {
@StateObject private var viewModel = TaskListViewModel()
@State private var isPresented: Bool = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.tasks, id:\.self) { task in
Text(task)
.font(.title)
}
}
Button {
isPresented.toggle()
} label: {
Text("Add New")
.font(.headline)
.fontWeight(.bold)
}
}
.sheet(isPresented: $isPresented, content: {
AddNewTaskView(viewModel: viewModel)
})
.navigationTitle("Tasks")
}
}
}
TaskListViewModel
을 StateObject
로 받아 값의 변화를 감지하고 있기 때문에 viewModel.tasks
를 통해 그려지는 리스트가 데이터 소스의 값 변화가 일어날 때 뷰를 다시 그릴 수 있음import SwiftUI
struct AddNewTaskView: View {
@Environment(\.dismiss) private var dismiss
var viewModel: TaskListViewModel
@State private var text: String = ""
var body: some View {
VStack(alignment: .center, spacing: 30) {
Text("Add New Task")
.font(.largeTitle)
.foregroundColor(.black)
TextField("", text: $text)
.frame(height: 60)
.background(Color.gray.opacity(0.4).cornerRadius(15))
.padding(.leading, 40)
.padding(.trailing, 40)
HStack(alignment: .center, spacing: 50) {
Button {
dismiss()
} label: {
Text("Cancel")
.font(.headline)
.foregroundColor(.red)
}
Button {
viewModel.addNewTask.send(text)
dismiss()
} label: {
Text("Done")
.font(.headline)
.foregroundColor(.blue)
}
}
}
}
}
TaskListViewModel
의 addNewTask
에 값을 send
하는 이벤트를 통해 리스트가 구독하는 tasks
의 값을 변화시킴import Foundation
import Combine
class TaskListViewModel: ObservableObject {
@Published var tasks = ["Buy Milk"]
// let tasks = CurrentValueSubject<[String], Never>(["Buy Milk"])
var addNewTask = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscriber()
}
private func addSubscriber() {
addNewTask
.filter{$0.count > 3}
.sink { [weak self] task in
guard let self = self else { return }
self.tasks.append(task)
// self.tasks.send(self.tasks.value + [task])
}
.store(in: &cancellables)
// tasks
// .sink { [weak self] values in
// guard let self = self else { return }
// print("tasks were update to \(values)")
// self.objectWillChange.send()
// }
// .store(in: &cancellables)
}
}
@Published
를 사용, 값 변화가 일어날 때 곧바로 이를 관찰하는 StateObject
를 받고 있는 파트에서 감지 가능