뱅크샐러드박보영
RxSwift to Combine
feat. SwiftUI
Introduce
Introduce
Introduce
Introduce
Introduce
Introduce
Customize handling
of asynchronous events
by combining event-processing operators.
https://developer.apple.com/documentation/combine
Introduce
Combine declares publishers to
expose values that can change over
time, and subscribers to receive
those values from the publishers.
https://developer.apple.com/documentation/combine
Introduce
By adopting Combine, you’ll make your
code easier to read and maintain, by
centralizing your event-processing code
and eliminating troublesome techniques
like nested closures and convention-based
callbacks.
https://developer.apple.com/documentation/combine
Introduce
Index
개념
비교하기
예제로
확인하기
정리
개념 비교하기
Asynchronous Interfaces
•Target/Action
•Notification Center
•URLSession
•KVO
•Ad-hoc callbacks
왜? Combine
https://developer.apple.com/videos/play/wwdc2019/722/
A unified declarative API for
processing values over time
Combine은,
https://developer.apple.com/videos/play/wwdc2019/722/
Combine 핵심 요소
Publishers Subscribers Operators
Combine vs. RxSwift
Publishers Subscribers Operators
Observable Observer Operators
Publishers
/Subscribers
Observable
/Observer
Operator Operator
Combine vs. RxSwift
Publishers
/Subscribers
Observable
/Observer
Subject
Operator Operator
Cancellable
Subscribe(on:)
Combine vs. RxSwift
Publishers
/Subscribers
Observable
/Observer
Subject
Operator
Subject
Operator
Cancellable
Subscribe(on:)
Combine vs. RxSwift
Publishers
/Subscribers
Observable
/Observer
Subject
Operator
Subject
Operator
Cancellable Disposable
Subscribe(on:)
Combine vs. RxSwift
Publishers
/Subscribers
Observable
/Observer
Subject
Operator
Subject
Operator
Cancellable Disposable
subscribe(on:) subscribeOn(_:)
Combine vs. RxSwift
Publisher vs. Observable
• AnyPublisher •Observable
class Observable: ObservableType { }
struct AnyPublisher: Publisher { }
Publisher vs. Observable
• AnyPublisher •Observable
protocol Publisher { }⋯
⋯
⋯
Publisher vs. Observable
• AnyPublisher •Observable
protocol Publisher { }
struct AnyPublisher: Publisher { }
class Observable: ObservableType { }
⋯
⋯
⋯
Publisher vs. Observable
• AnyPublisher
• Value Type
•Observable
•Reference Type
struct AnyPublisher: Publisher { }
class Observable: ObservableType { }
⋯
⋯
Publisher vs. Observable
• AnyPublisher
• Value Type
• Output(Data type)
• Failure(Error type)
•Observable
•Reference Type
•Element(Data type)
•❌
Publisher vs. Observable
• AnyPublisher
• Value Type
• Output(Data type)
• Failure(Error type)
•Observable
•Reference Type
•Element(Data type)
•❌
AnyPublisher<String, Error> Observable<Result<String, Error>>
Publisher vs. Observable
• AnyPublisher
• Value Type
• Output(Data type)
• Failure(Error type)
•Observable
•Reference Type
•Element(Data type)
•❌
AnyPublisher<String, Error>
AnyPublisher<String, Never>
Observable<Result<String, Error>>
Observable<String>
Operators, RxSwift Only
Combine에는 없는 Operators
• amb()
• asObserver()
•concatMap
• create
• delaySubscription
• dematerialize
• enumerated
•flatMapFirst
• from
• groupBy
• ifEmpty(switchTo:)
• interval
• materialize
• range
• repeatElement
• retryWhen
• sample
•withLatestFrom
https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet
👋
Operators, Combine Only
Combine에만 있는 Operators
• tryMap
• tryScan
• tryFilter
• tryCompactMap
• tryRemoveDuplicates(by:)
• tryReduce
• tryMax(by:)
• tryMin(by:)
• tryContains(where:)
• tryAllSatisfy
• tryDrop(while:)
• tryPrefix(while:)
• tryFirst(where:)
• tryLast(where:)
• tryCatch
👀
Map vs. tryMap
func map<T>(_ transform: (Output) -> T)
-> Just<T>
func tryMap<T>(_ transform: (Output) throws -> T)
-> Result<T, Error>.Publisher
Combine Operators
• Merge, Merge3,
Merge4, Merge5,
Merge6, Merge7,
Merge8, MergeMany
• merge
• combineLatest• CombineLatest,
CombineLatest3,
CombineLatest4
• zip• Zip, Zip3, Zip4
• ReplaySubject• ❌
• BehaviorSubject• CurrentValueSubject
Subjects
• PassthroughSubject • PublishSubject
• ReplaySubject• ❌
• BehaviorSubject• CurrentValueSubject
Subjects
• PassthroughSubject • PublishSubject
class PassthroughSubject {
public init()
class PublishSubject {
public override init()
⋯
⋯
• ReplaySubject• ❌
• BehaviorSubject• CurrentValueSubject
Subjects
class CurrentValueSubject {
public init(_ value: Output)
class BehaviorSubject {
public init(value: Element)
⋯
⋯
🧟
Cancellable vs. Disposable
🧟
🧟
Cancellable vs. Disposable
🧟🗑 • Disposable
• DisposeBag
🧟
Cancellable vs. Disposable
🧟🗑 • Disposable
• DisposeBag
• Cancellable
• AnyCancellable
Cancellable vs. Disposable
NO DISPOSE BAG!
🗑 🙅
Cancellable vs. Disposable
var disposeBag = DisposeBag()
Observable.just(1)
.subscribe(onNext: { number in
print(number)
})
.disposed(by: disposeBag)
var cancellables = Set<AnyCancellable>()
Just(1)
.sink { number in
print(number)
}
.store(in: &cancellables)
Cancellable vs. Disposable
var disposeBag = DisposeBag()
Observable.just(1)
.subscribe(onNext: { number in
print(number)
})
.disposed(by: disposeBag)
var cancellables = Set<AnyCancellable>()
Just(1)
.sink { number in
print(number)
}
.store(in: &cancellables)
Thread Handling
http://reactivex.io/documentation/operators/subscribeon.html
Thread Handling
http://reactivex.io/documentation/operators/subscribeon.html
Just(1)
.subscribe(on: DispatchQueue.main)
.map { _ in
implements()
}
.sink { … }
Combine vs. RxSwift
Publishers
/Subscribers
Observable
/Observer
Subject
Operator
Subject
Operator
Cancellable Disposable
subscribe(on:) subscribeOn(_:)
WWDC 2019
•Introducing Combine
https://developer.apple.com/videos/play/wwdc2019/722/
•Combine in Practice
https://developer.apple.com/videos/play/wwdc2019/721/
•Modern Swift API Design
https://developer.apple.com/videos/play/wwdc2019/415/
Reference
예제로 확인하기
BringMyOwnBeer🍺
https://github.com/fimuxd/BringMyOwnBeer-
PunkAPI by Brewdog
List / Search / Random
https://github.com/fimuxd/BringMyOwnBeer-Combine
BringMyOwnBeer🍺
📡
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
import RxSwift
import Combine
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
Observable<Result<[Beer], PunkNetworkError>>
AnyPublisher<[Beer], PunkNetworkError>
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
session.rx.data
session.dataTaskPublisher
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
.decode(type:decoder:)
do {
try JSONDecoder().decode(_:from:)
} catch { }
import RxSwift
import Combine
func getBeers(page: Int?) -> Observable<Result<[Beer], PunkNetworkError>> {
return session.rx.data(request: URLRequest(url: url))
.map { data in
do {
let beers = try JSONDecoder().decode([Beer].self, from: data)
return .success(beers)
} catch {
return .failure(.error("JSON parsing 에러”))
}
}
}
func getBeers(page: Int?) -> AnyPublisher<[Beer], PunkNetworkError> {
return session.dataTaskPublisher(for: URLRequest(url: url))
.flatMap { data in
return Just(data.data)
.decode(type: [Beer].self, decoder: JSONDecoder())
.mapError { _ in
.error("JSON parsing 에러”)
}
}
.eraseToAnyPublisher()
}
🍻
•리스트 형태
•GET beerList API 이용
•한 번에 25개의 맥주 정보
•최대 325개의 맥주 정보
Beer List
import RxSwift
import RxCocoa
Beer List with RxSwift
import RxSwift
import RxCocoa
Beer List with RxSwift
ViewModel
Bindable
View
import RxSwift
import RxCocoa
Beer List with RxSwift
View
UIViewController
import RxSwift
import RxCocoa
Beer List with RxSwift
View
UIViewController
UITableView
import RxSwift
import RxCocoa
Beer List with RxSwift
View
UIViewController
UITableView
UITableViewDelegate
import RxSwift
import RxCocoa
Beer List with RxSwift
View
func items<S, O>(_ source: O)
-> (@escaping (UITableView, Int, S.Iterator.Element)
-> UITableViewCell)
-> Disposable
where S : Sequence, S == O.E, O : ObservableType
public var willDisplayCell: ControlEvent<WillDisplayCellEvent> { }
UIViewController
UITableView
UITableViewDelegate
import RxSwift
import RxCocoa
Beer List with RxSwift
View
ViewModel
Bindable
UIViewController
UITableView
UITableViewDelegate
UIViewController
UITableView
UITableViewDelegate
import RxSwift
import RxCocoa
Beer List with RxSwift
View
ViewModel
Bindable
var viewWillAppear: PublishSubject<Void> { get }
var willDisplayCell: PublishRelay<IndexPath> { get }
UIViewController
UITableView
UITableViewDelegate
import RxSwift
import RxCocoa
Beer List with RxSwift
View
ViewModel
Bindable
var cellData: Driver<[BeerListCell.Data]> { get }
var errorMessage: Signal<String> { get }
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
let tableView = UITableView()
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
class BeerListViewController: UIViewController {
let tableView = UITableView()
func bind(_ viewModel: BeerListViewBindable) {
self.disposeBag = DisposeBag()
self.rx.viewWillAppear
.map { _ in Void() }
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
viewModel.cellData
.drive(tableView.rx.items) { tv, row, data in
let index = IndexPath(row: row, section: 0)
let cell = tv.dequeueReusableCell(
withIdentifier: String(describing: BeerListCell.self),
for: index
) as! BeerListCell
cell.setData(data: data)
return cell
}
.disposed(by: disposeBag)
}
func attribute() { }
func layout() { }
viewModel.cellData
.drive(tableView.rx.items) { }
.disposed(by: disposeBag)
protocol BeerListViewBindable {
//View -> ViewModel
var viewWillAppear: PublishSubject<Void> { get }
var willDisplayCell: PublishRelay<IndexPath> { get }
//ViewModel -> View
var cellData: Driver<[BeerListCell.Data]> { get }
var errorMessage: Signal<String> { get }
}
protocol BeerListViewBindable {
//View -> ViewModel
var viewWillAppear: PublishSubject<Void> { get }
var willDisplayCell: PublishRelay<IndexPath> { get }
//ViewModel -> View
var cellData: Driver<[BeerListCell.Data]> { get }
var errorMessage: Signal<String> { get }
}
protocol BeerListViewBindable {
//View -> ViewModel
var viewWillAppear: PublishSubject<Void> { get }
var willDisplayCell: PublishRelay<IndexPath> { get }
//ViewModel -> View
var cellData: Driver<[BeerListCell.Data]> { get }
var errorMessage: Signal<String> { get }
}
struct BeerListViewModel: BeerListViewBindable {
let disposeBag = DisposeBag()
let viewWillAppear = PublishSubject<Void>()
let cellData: Driver<[BeerListCell.Data]>
let willDisplayCell = PublishRelay<IndexPath>()
let errorMessage: Signal<String>
private var cells = BehaviorRelay<[Beer]>(value: [])
init(model: BeerListModel = BeerListModel()) {
struct BeerListViewModel: BeerListViewBindable {
init(model: BeerListModel = BeerListModel()) {
let beerListResult = viewWillAppear
.flatMapLatest(model.getBeerList)
.asObservable()
.share()
let beerListValue = beerListResult
.map { result -> [Beer]? in
guard case .success(let value) = result else {
return nil
}
return value
}
.filterNil()
let beerListError = beerListResult
.map { result -> String? in
guard case .failure(let error) = result else {
return nil
}
return error.message
}
.filterNil()
struct BeerListViewModel: BeerListViewBindable {
init(model: BeerListModel = BeerListModel()) {
let beerListResult = viewWillAppear
.flatMapLatest(model.getBeerList)
.asObservable()
.share()
let beerListValue = beerListResult
.map { result -> [Beer]? in
guard case .success(let value) = result else {
return nil
}
return value
}
.filterNil()
let beerListError = beerListResult
.map { result -> String? in
guard case .failure(let error) = result else {
return nil
}
return error.message
}
.filterNil()
struct BeerListViewModel: BeerListViewBindable {
init(model: BeerListModel = BeerListModel()) {
let beerListResult = viewWillAppear
.flatMapLatest(model.getBeerList)
.asObservable()
.share()
let beerListValue = beerListResult
.map { result -> [Beer]? in
guard case .success(let value) = result else {
return nil
}
return value
}
.filterNil()
let beerListError = beerListResult
.map { result -> String? in
guard case .failure(let error) = result else {
return nil
}
return error.message
}
.filterNil()
struct BeerListViewModel: BeerListViewBindable {
Observable
.merge(
beerListValue,
fetchedList
)
.scan([]){ prev, newList in
return newList.isEmpty ? [] : prev + newList
}
.bind(to: cells)
.disposed(by: disposeBag)
self.cellData = cells
.map(model.parseData)
.asDriver(onErrorDriveWith: .empty())
self.errorMessage = Observable
.merge(
beerListError,
fetchedError
)
.asSignal(onErrorJustReturn: .defaultError.message ?? “")
}
import Combine
import SwiftUI
Beer List with Combine
UI Binding
Combine
SwiftUI
RxSwift
RxCocoa
View 는,
https://developer.apple.com/videos/play/wwdc2019/226
Views are a function of state,
not of a sequence of events.
Property Wrapper
@State @ObservableObject
https://developer.apple.com/videos/play/wwdc2019/226
Property Wrapper
@State @ObservableObject
https://developer.apple.com/videos/play/wwdc2019/226
View-local External
Property Wrapper
@State @ObservableObject
https://developer.apple.com/videos/play/wwdc2019/226
View-local External
prev) BindableObject
Property Wrapper
@State @ObservableObject
https://developer.apple.com/videos/play/wwdc2019/226
View-local
Value type
External
Reference type
prev) BindableObject
Property Wrapper
@State @ObservableObject
https://developer.apple.com/videos/play/wwdc2019/226
View-local
Value type
Framework Managed
External
Reference type
Developer Managed
prev) BindableObject
🧑💻
SwiftUI
Action
User Interaction
https://developer.apple.com/videos/play/wwdc2019/226
Data Flow through SwiftUI
🧑💻
SwiftUI
Action
@StateMutation
User Interaction
https://developer.apple.com/videos/play/wwdc2019/226
Data Flow through SwiftUI
🧑💻
SwiftUI
Action View
@State UpdatesMutation
User Interaction
https://developer.apple.com/videos/play/wwdc2019/226
Data Flow through SwiftUI
🧑💻
SwiftUI
Action View
@State
Render
UpdatesMutation
User Interaction
https://developer.apple.com/videos/play/wwdc2019/226
Data Flow through SwiftUI
🧑💻
SwiftUI
Action
@State
Render
UpdatesMutation
User Interaction
https://developer.apple.com/videos/play/wwdc2019/226
🧨 🍺
@Publisher
View
Data Flow through SwiftUI
import SwiftUI
struct BeerList: View {
@ObservedObject var viewModel: BeerListViewModel
init(viewModel: BeerListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List(viewModel.beers, id: .id) { beer in
BeerRow(beer: beer).onAppear {
self.viewModel.appearedID.send(beer.id)
}
}
.navigationBarTitle(Text("맥주리스트"))
}
.alert(isPresented: $viewModel.showingAlert) {
Alert(title: Text(viewModel.errorMessage))
}
}
}
struct BeerList: View {
@ObservedObject var viewModel: BeerListViewModel
init(viewModel: BeerListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List(viewModel.beers, id: .id) { beer in
BeerRow(beer: beer).onAppear {
self.viewModel.appearedID.send(beer.id)
}
}
.navigationBarTitle(Text("맥주리스트"))
}
.alert(isPresented: $viewModel.showingAlert) {
Alert(title: Text(viewModel.errorMessage))
}
}
}
class BeerListViewModel: ObservableObject {
@Published var beers: [Beer] = []
@Published var showingAlert: Bool = false
@Published var errorMessage: String = ""
let appearedID = PassthroughSubject<Int?, PunkNetworkError>()
private var cancellables = Set<AnyCancellable>()
init(model: BeerListModel = BeerListModel()) {
class BeerListViewModel: ObservableObject {
//ViewModel -> View
@Published var beers: [Beer] = []
@Published var showingAlert: Bool = false
@Published var errorMessage: String = ""
//View -> ViewModel
let appearedID = PassthroughSubject<Int?, PunkNetworkError>()
private var cancellables = Set<AnyCancellable>()
init(model: BeerListModel = BeerListModel()) {
class BeerListViewModel: ObservableObject {
//ViewModel -> View
@Published var beers: [Beer] = []
@Published var showingAlert: Bool = false
@Published var errorMessage: String = ""
//View -> ViewModel
let appearedID = PassthroughSubject<Int?, PunkNetworkError>()
private var cancellables = Set<AnyCancellable>()
init(model: BeerListModel = BeerListModel()) {
class BeerListViewModel: ObservableObject {
init(model: BeerListModel = BeerListModel()) {
let loadBeerList = appearedID
.map { model.getPageToPatch(beers: self.beers, id: $0) }
.filter { $0 != nil }
.eraseToAnyPublisher()
class BeerListViewModel: ObservableObject {
init(model: BeerListModel = BeerListModel()) {
let loadBeerList = appearedID
.map { model.getPageToPatch(beers: self.beers, id: $0) }
.filter { $0 != nil }
.eraseToAnyPublisher()
.eraseToAnyPublisher() .asObservable()
class BeerListViewModel: ObservableObject {
.eraseToAnyPublisher()
loadBeerList
.prepend(nil)
.flatMap(model.getBeerList)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
guard case .failure(let error) = $0 else { return }
self.beers = []
self.showingAlert = true
self.errorMessage = error.message ?? "에러 발생🚨”
}, receiveValue: { beers in
self.beers += beers
})
.store(in: &cancellables)
}
}
class BeerListViewModel: ObservableObject {
.eraseToAnyPublisher()
loadBeerList
.prepend(nil)
.flatMap(model.getBeerList)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
guard case .failure(let error) = $0 else { return }
self.beers = []
self.showingAlert = true
self.errorMessage = error.message ?? "에러 발생🚨”
}, receiveValue: { beers in
self.beers += beers
})
.store(in: &cancellables)
}
}
class BeerListViewModel: ObservableObject {
.eraseToAnyPublisher()
loadBeerList
.prepend(nil)
.flatMap(model.getBeerList)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
guard case .failure(let error) = $0 else { return }
self.beers = []
self.showingAlert = true
self.errorMessage = error.message ?? "에러 발생🚨”
}, receiveValue: { beers in
self.beers += beers
})
.store(in: &cancellables)
}
}
class BeerListViewModel: ObservableObject {
.eraseToAnyPublisher()
loadBeerList
.prepend(nil)
.flatMap(model.getBeerList)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
guard case .failure(let error) = $0 else { return }
self.beers = []
self.showingAlert = true
self.errorMessage = error.message ?? "에러 발생🚨”
}, receiveValue: { beers in
self.beers += beers
})
.store(in: &cancellables)
}
}
.sink(
receiveCompletion:
receiveValue:
)
.subscribe(
onNext:
onError:
onCompleted:
onDisposed:
)
class BeerListViewModel: ObservableObject {
.eraseToAnyPublisher()
loadBeerList
.prepend(nil)
.flatMap(model.getBeerList)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
guard case .failure(let error) = $0 else { return }
self.beers = []
self.showingAlert = true
self.errorMessage = error.message ?? "에러 발생🚨”
}, receiveValue: { beers in
self.beers += beers
})
.store(in: &cancellables)
}
}
@Published var beers: [Beer] = []
import SwiftUI
struct BeerList: View {
@ObservedObject var viewModel: BeerListViewModel
init(viewModel: BeerListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List(viewModel.beers, id: .id) { beer in
BeerRow(beer: beer).onAppear {
self.viewModel.appearedID.send(beer.id)
}
}
.navigationBarTitle(Text("맥주리스트"))
}
.alert(isPresented: $viewModel.showingAlert) {
Alert(title: Text(viewModel.errorMessage))
}
}
}
import SwiftUI
struct BeerList: View {
@ObservedObject var viewModel: BeerListViewModel
init(viewModel: BeerListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List(viewModel.beers, id: .id) { beer in
BeerRow(beer: beer).onAppear {
self.viewModel.appearedID.send(beer.id)
}
}
.navigationBarTitle(Text("맥주리스트"))
}
.alert(isPresented: $viewModel.showingAlert) {
Alert(title: Text(viewModel.errorMessage))
}
}
}
struct List: View {
public init(
_ data: Data,
id: KeyPath<Data.Element, ID>,
⋯
BringMyOwnBeer🍺
정리
Combine/SwiftUI RxSwift/RxCocoa
Summary
App Size
0MB
1MB
2MB
3MB
4MB
Combine RxSwift
Summary
Summary
https://medium.com/flawless-app-stories/will-combine-kill-rxswift-64780a150d89
Summary
The memory models of RxSwift and Combine are very different.
Combine is really made for performance.
https://engineering.q42.nl/swift-combine-framework/
Summary
👨💻 ❤ RxSwift
Summary
👨💻 ❤ Combine
Summary
👨💻 ❤ Combine
👩💻 ❓ RxSwift
Summary
👨💻 ❤ Combine
👩💻 ❤ Combine
Summary
👨💻 ❤ Combine
👩💻 ❤ Combine
🎁 SwiftUI
✨
✨ ✨
✨ ✨
Summary
👨💻
❤
Combine👩💻
❤
Combine
🎁 SwiftUI
🎁 SwiftUI
👨💻
❤
Combine👩💻
❤
Combine
Summary
iOS 13.0+
⚡⚡⚡
⚡⚡⚡
⚡⚡⚡
완벽함이란 더 이상 더할 것이 없는 상태가 아니라,
더 이상 뺄 것이 없는 상태를 말한다.
Antoine de Saint-Exupéry
RxSwift to Combine

RxSwift to Combine