Combine์ ๋ํด์ ์ค์ ์ฌ์ฉํ๋ค ๋ณด๋, ์ด๊ฒ ๋๋์ฒด ์ด๋ค ๋ฐฉ์์ผ๋ก ์ด๋ฃจ์ด์ง๋ ์ง ๊ถ๊ธํ๋ค. ์ค์ ์ฝ๋๋ฅผ ๋ณด์ง ์์ผ๋ฉด ๋ช ํํด์ง์ง ์์ ๊ฒ ๊ฐ์ ์ ๋ฆฌํ๋ค.
Objective
URLSession์์ ์์ฒญ์ ๋ฐ์ ๋ด๊ฐ ์ํ๋ ์๋ต์ ๋ณด๋ด์ฃผ๋ Publisher, ๊ทธ์ ํด๋นํ๋ Subscriber, Subscription์ ๋ชจ๋ ๋ฐ๋ก ๋ง๋ค์ด ๋ณธ๋ค.
์ด์ ๊ธ์์ ๋ฐฐ์ ๋ ์ด ํ๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก, Custom Publisher, Subscriber๋ฅผ ๋ง๋ค์ด๋ณด๋ฉด์ ๋ด๋ถ ๋์์ ์ดํดํด๋ณด๋ ๊ฒ์ ๋ชฉํ๋ก ํ๋ค. ๊ทธ ์ ์, ์์ ๊ธ๊ณผ ์กฐ๊ธ ๋ค๋ฅธ ๋ถ๋ถ์ด ์์ด ๊ทธ๋ฆผ์ ์์ ํ๋ค. WWDC์์ ๋์จ ์ด ๊ทธ๋ฆผ์ ์ผํ ๋ณด๋ฉด Publisher์๊ฒ ๊ฐ์ ์์ฒญํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค. ํ์ง๋ง ๊ทธ๊ฒ์ ์ฌ์ค์ด ์๋๋ฉฐ, Publisher๊ฐ Subscription์ ์ธ์๋ก ๋๊ฒจ์ฃผ๋ ์์ ๋ถํฐ, Subscription ์ธ์คํด์ค์์ Subscriber์ Method๋ค์ ํธ์ถํด ์ค๋ค. ๊ทธ๋์ Subscription ๊ฐ์ฒด ๊ทธ๋ฆผ์ ํ๋ ์ถ๊ฐํ๋ค. ์ด๋ป๊ฒ Customdmfh Publisher, Subscriber๋ฅผ ๋ง๋๋์ง ๋ฐ๋ผ๊ฐ๋ค๋ฉด ์ดํดํ ์ ์์ ๊ฒ์ด๋ค.
๊ทธ์ ์, ๊ตฌํ์ ์ด๋ค ๋ฐฉ์์ผ๋ก ํ ์ง์ ๋ํด ๊ฐ๋จํ๊ฒ ์์๋ณด์.
- Publisher๋ URLSession์ extension์ ์์นํด์ ๋ง๋ ๋ค.
- Subscriber๋ ๊ทธ๋ฅ ๋ง๋ค์ด์ ๊ด๋ฆฌํ๋ค.
- Publisher ์
์ฅ์์ subscriber๊ฐ ์ธ์๋ก ๋ค์ด์์ ๋, Subscriber์
receive(subscription:)
์ ํธ์ถํด์ฃผ์ด์ผ ํ๊ธฐ ๋๋ฌธ์, Subscription ๊ฐ์ฒด๋ Publisher ๋ด๋ถ์์ ์ ์ํ๋๋ก ํ๋ค.
๊ทธ๋ผ ์์ํด๋ณด์.
Custom Subscriber
์ผ๋จ Subscriber๋ฅผ ๋ง๋ค์ด๋ณด์. WWDC์ ํ๋ฆ๋๋ฅผ ๋ณธ๋ค๋ฉด, Subscriber์์๋ Publisher์ ์์ ์ ๋ณด๋ด๋ method, input์ ๋ฐ๋ method, ๋๋ฌ์ ๋ ํธ์ถ๋๋ method ์ธ๊ฐ๊ฐ ํ์ํ๋ค.
public protocol Subscriber : CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure : Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
์ค์ Subscriber protocol์ ๋ณด๋ฉด ๋ง๊ฒ ๋์ด ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
typealias Failure = Error // Error๊ฐ ๋์ง ์๋๋ค๋ฉด Never Type. completionํ์
์๋ ๋ฐ์๋จ
func receive(subscription: Subscription) { // 1. ์ฒ์ publisher์ ์ํด ํธ์ถ๋จ
print("Received subscription")
subscription.request(.unlimited) // Subscription์ผ๋ก ๋ถํฐ ๋ฐ๋ ๊ฐ์๋ฅผ ์ ํ ์๋ ์์: subscription.request(.max(3))
}
func receive(_ input: Input) -> Subscribers.Demand { // 2. subscription์ ์ํด ํธ์ถ๋จ
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) { // 3. ๋ชจ๋ ๊ฐ ๋ฐฉ์ถ์ด ๋๋๋ฉด ํธ์ถ๋จ
print("Received completion \(completion)")
}
}
// ํน์ ์ด๋ฐ ๋ฐฉ์๋ ๊ฐ๋ฅํ๋ค.
class DecodableDataTaskSubscriber<Input: Decodable, Failure: Error>: Subscriber {
func receive(subscription: Subscription) {
print("Received subscription")
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
}
}
class DecodableDataTaskSubscriber: Subscriber {
typealias Input = Decodable
typealias Failure = Error
func receive(subscription: Subscription) {
print("Received subscription")
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
}
}
์ธ๊ฐ์ง ๋ฐฉ๋ฒ์ ๊ฐ๊ฐ์ ์ฅ๋จ์ ์ ๊ฐ์ง๋ค. Input, Output Type์ด ๋ณํ ์ ์๋ค๋ฉด ๋ช
์์ ์ผ๋ก ์ ์ด์ฃผ๊ณ , ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ ๋ด๋ถ์ ์ผ๋ก typealias
๋ฅผ ์ฌ์ฉํด์ ์ฒ๋ฆฌํด์ฃผ์.
Custom Publisher
Publisher์ ๊ฒฝ์ฐ์๋ Subscriber์๊ฒ Subscription์ ๋์ ธ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค. ํ๋์ ๋ฉ์๋๋ง ํ์ํ ๊ฒ์ด๋ค.
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
์ค์ ํ๋กํ ์ฝ๋ ๊ทธ๋ ๊ฒ ๋์ด ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ์ฐ๋ฆฌ๋ URLSession์์ ์ฌ์ฉํ Publisher์ด๊ธฐ ๋๋ฌธ์ ๊ทธ ์์ ๋ง๋ค์ด์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ข๊ฒ ๋ค.
extension URLSession {
func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
}
struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
typealias Failure = Error
let urlRequest: URLRequest
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
}
์ธ๋ถ์์ ์ฌ์ฉํ ๋๋ decodedDataTaskPublisher(for urlRequest:)
๋ฅผ ์ฌ์ฉํ๋ฉด ๋๊ฒ ๋ค. ๊ทธ๋ฐ๋ฐ ์์ง Subscription ๊ฐ์ฒด์ ๋ํด ์ ์ํ์ง ์์ ์ฌ์ค ์ ๋ฉ์๋๋ ์ฌ์ฉ์ด ๋ถ๊ฐํ๋ค. ์ด ๋
์์ ๋ง๋ค๋ฌ ๊ฐ๋ณด์.
Custom Subscription
์ค์ ๋ก Subscriber์๊ฒ ์์ฒญ์ ๋ฐ์ ๊ฐ์ ๋๋ ค์ฃผ๋ ๋ ์์ด๋ค. ๊ฐ์์ ๋ํ ์์ฒญ์ ๋ฐ์ method๋ง ์์ฑํด์ฃผ๋ฉด ๋๋ค.
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {
func request(_ demand: Subscribers.Demand)
}
์ฌ๊ธฐ์ ๋ถํฐ ์ ๋ด์ผ ํ๋ค. ์ด๋
์์ ํ์
์ ๋ณด๋ฉด Cancellable
์ ์ฑํํ๊ณ ์๋ค. ๊ณง, ์ด๋
์์ด ์ฐ๋ฆฌ๊ฐ ๋ณดํต AnyCancellable
๋ก ๋ฐ๋ ๋
์์ด๋ผ๋ ๊ฑฐ๋ค. ๊ตฌ๋
์ life cycle์ ๋ด๋นํ๋ ์น๊ตฌ๊ฐ ์ด๋
์์ด๋ค. ์ผ๋จ ๋ง๋ค์ด๋ณด์.
extension URLSession.DecodedDataTaskPublisher {
class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
where S.Input == Output, S.Failure == Error {
private let urlRequest: URLRequest
private var subscriber: S?
init(urlRequest: URLRequest, subscriber: S) { // ์์ฑ ์์ ์ ์ค์ ์์ฒญ์ ํ request ๊ฐ์ฒด์ subscriber๋ฅผ ๋ฐ๋๋ค.
self.urlRequest = urlRequest
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) { // Subscriber์ชฝ์์ ์์ฒญํ๋ฉด ์ฌ๊ธฐ์ subscriber์๊ฒ ์ ๋ฌํด์ค๋ค.
if demand > 0 {
URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
defer { self?.cancel() }
if let data = data {
do {
let result = try JSONDecoder().decode(Output.self, from: data)
self?.subscriber?.receive(result)
self?.subscriber?.receive(completion: .finished)
} catch {
self?.subscriber?.receive(completion: .failure(error))
}
} else if let error = error {
self?.subscriber?.receive(completion: .failure(error))
}
}.resume()
}
}
func cancel() {
subscriber = nil
}
}
}
Subscription์์ subscriber๋ก ๋ฉ์๋๋ฅผ ํธ์ถํด์ค๋ค. ๋ด๋ถ์ ์ผ๋ก subscriber๋ฅผ subscription์ด ๋ค๊ณ ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
Attach Whole Thing
๊ทธ๋ผ ์ด์ ํ๋ฒ ์คํํด๋ณด์.
struct SomeModel: Decodable {}
func makeTheRequest() {
let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
let subscriber = DecodableDataTaskSubscriber<SomeModel>()
publisher.subscribe(subscriber)
}
์์ํ๋ ๋์์ ๋ค์๊ณผ ๊ฐ๋ค.
Received subscription
Received value ~
Received completion ~
ํ์ง๋ง ์ค์ ๋ก ์คํ์์ผ๋ณด๋ฉด, subscription๋ง ์ถ๋ ฅ๋๋ค.
Received subscription
๋ฌด์์ด ๋ฌธ์ ์ผ๊น?
Problem
์ฐฌ์ฐฌํ combine ํธ์ถ ํ๋ฆ์ ๋ฐ๋ผ๊ฐ๋ณด๋ฉด, subscriber์ ์๋ method๊ฐ ํธ์ถ๋์ง ์์๋ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ํด๋น ๊ฐ์ฒด๊ฐ ํ ๋น ํด์ ๋ ๊ฒ ์ธ์ง ํ์ธํด๋ณด์.
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
typealias Failure = Error
func receive(subscription: Subscription) {
print("Received subscription")
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
}
deinit {
Swift.print("deinit subscriber")
}
}
Received subscription
deinit subscriber
- subscription์ subscriber ์ธ์คํด์ค๋ฅผ ์๊ณ ์๋ค. (subscriber count + 1)
- publisher๋ subscriber์๊ฒ subscription ๊ฐ์ฒด๋ฅผ ๋๊ฒจ์ค๋ค.
receive(subscription: Subscription)
์์ subscriber๋ subscription์ ์๊ตฌ๊ฐ์๋งํผ์ ์์ฒญํ๋ค.- ํ์ง๋ง ํจ์ ์ธ์๋ก ๋ค์ด์จ subscription ์ธ์คํด์ค์ ์๋ช
์ฃผ๊ธฐ๋
receive(subscription: Subscription)
๋ด๋ถ์ด๋ค. - ์ค์ฝํ ์ข ๋ฃํ subscription instance๋ ํ ๋น ํด์ ๋๋ค.(๋ฉ์์ง ๋ณด๋ด๊ธฐ๋ ์ ์ ํ ๋นํด์ ๋จ) ๋ด๋ถ์ ์๋ subscriber ๊ฐ์ฒด ์ญ์ ํ ๋นํด์ ๋๋ค. (subscriber count 0)
- subscriber์ reference count๊ฐ 0์ด ๋์ด, subscriber๋ ํ ๋น ํด์ ๋๋ค.
์ด๋ฌํ ํ๋ฆ์์ ์ฐ๋ฆฌ๊ฐ ์ ์ ์๋ ์ ์, ๋๊ตฐ๊ฐ๋ subscription ์ธ์คํด์ค๋ฅผ ์์ ํด์ผ ํ๋ค๋ ๊ฒ์ด๋ค.
Subscription Instance ์์ ์ ์ฑ ์
๊ทธ๋ผ ์ด subscription ์ธ์คํด์ค๋ ๋๊ฐ ๊ฐ์ง๊ณ ์์ด์ผ ํ ๊น? ๋ด๊ฐ ์ฝ๊ณ ์๋ ์ ์์ ๊ฒฝ์ฐ, completion์ด ํธ์ถ๋๋ ์๊ธฐ์ subscription ๊ฐ์ฒด๋ฅผ ํด์ ํด์ฃผ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค.
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber, Cancellable {
typealias Failure = Error
var subscription: Subscription?
func receive(subscription: Subscription) {
print("Received subscription")
self.subscription = subscription
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
cancel()
}
func cancel() {
subscription?.cancel()
subscription = nil
}
}
์ผ๋จ ์ด๋ ๊ฒ ํ๋ฉด, instance๊ฐ ์ด์์๊ธฐ ๋๋ฌธ์, ๊ตฌ๋ ์ด ๋ชจ๋ ์ข ๋ฃ๋๋ฉด subscription์ ์ข ๋ฃํ์ฌ ๊น๋ํ๊ฒ ํด๊ฒฐ๋๊ธฐ๋ ํ๋ค. ํ์ง๋ง ์ด๋ ๊ฒ ๋๋ ๊ฒฝ์ฐ, ์ด subscriber๋ฅผ ์ค์ ๋ก ์ฌ์ฉํ๋ ๊ฐ์ฒด๊ฐ ๊ตฌ๋ ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ ์๊ฐ ์๊ฒ ๋๋ค. ์ด๋ป๊ฒ ํด์ผํ ๊น?
์ง๊ธ๊น์ง์ ์ฝ๋
//
// CustomCombineViewController.swift
// test
//
// Created by Choiwansik on 2022/05/26.
//
import UIKit
import Combine
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber, Cancellable {
typealias Failure = Error
var subscription: Subscription?
func receive(subscription: Subscription) {
print("Received subscription")
self.subscription = subscription
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
cancel()
}
func cancel() {
subscription?.cancel()
subscription = nil
}
}
extension URLSession {
func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
}
struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
typealias Failure = Error
let urlRequest: URLRequest
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
}
extension URLSession.DecodedDataTaskPublisher {
class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
where S.Input == Output, S.Failure == Error {
private let urlRequest: URLRequest
private var subscriber: S?
init(urlRequest: URLRequest, subscriber: S) {
self.urlRequest = urlRequest
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
if demand > 0 {
URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
defer { self?.cancel() }
if let data = data {
do {
let result = try JSONDecoder().decode(Output.self, from: data)
self?.subscriber?.receive(result)
self?.subscriber?.receive(completion: .finished)
} catch {
self?.subscriber?.receive(completion: .failure(error))
}
} else if let error = error {
self?.subscriber?.receive(completion: .failure(error))
}
}.resume()
}
}
func cancel() {
subscriber = nil
}
}
}
struct SomeModel: Decodable {}
class CustomCombineViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.setup()
}
func makeTheRequest() {
let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
let subscriber = DecodableDataTaskSubscriber<SomeModel>()
publisher.subscribe(subscriber)
}
func setup() {
self.view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 20, y: 40, width: 100, height: 50))
button.backgroundColor = .blue
self.view.addSubview(button)
button.titleLabel?.text = "request!!"
button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
self.makeTheRequest()
}
}
sink์ ๋ํ ์ดํด
๊ตฌ๋ ์ life cycle์ ๊ด๋ฆฌํ๋ subscription ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ ๊ฒ์ด ๋ง๋ค๋ ์๊ฐ์ ํ๋ค. ์ค์ ๋ก sink์ ๊ฐ์ด apple์์ ์ ๊ณตํ๋ ๋ฉ์๋์ ๊ฒฝ์ฐ์ ๊ทธ๋ ๊ฒ ๊ตฌํ๋์ด ์์ด, ํ๋ฒ ์๋ํด๋ณด์๋ค.
extension URLSession {
func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
}
struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
// sink ๋์์ ๋ฐ๋ผํ์ฌ ๋ง๋ค์ด๋ด
func ssink(receiveValue: (Self.Output) -> Void, receiveCompletion: (Combine.Subscribers.Completion<Self.Failure>) -> Void) -> Cancellable {
let subscriber = DecodableDataTaskSubscriber<SomeModel>()
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
return subscription
}
typealias Failure = Error
let urlRequest: URLRequest
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
}
class CustomCombineViewController: UIViewController {
var cancellable: Cancellable? // ์์
deinit {
self.cancellable?.cancel()
}
func makeTheRequest() {
let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
self.cancellable = publisher.ssink(receiveValue: { value in
print("Received value: \(value)")
}, receiveCompletion: { completion in
print("Received completion: \(completion)")
})
}
}
์ด๋ฅผ ํตํด ์ฐ๋ฆฌ๊ฐ ๋ณดํต ์ฌ์ฉํ๋ sink์ ๊ฐ์ api๊ฐ ์ด๋ค ๊ตฌ์กฐ๋ก ๋์ด ์์ ์ง ์ ์ถํด ๋ณผ ์ ์๋ค.
- receiveCompletion, receiveValue์ ์ํ๋ ๋์์ ๋ฃ๋๋ค.
- ํด๋น ์
๋ ฅ์
receive(_ input: Input) -> Subscribers.Demand
,receive(completion: Subscribers.Completion<Error>)
method ๋ด๋ถ์ ๋ฐฐ์นํ๋ค. - 2์์ ๋ง๋ค์ด์ง subscriber ์ธ์คํด์ค๋ฅผ Publisher์ ์๋
receive<S>(subscriber: S)
์์ ๋ฃ์ด ํธ์ถํ๋ค. - publisher๋ Subscriber์
receive(subscription: Subscription)
๋ฅผ ํธ์ถํ๋ค. - subscriber๋ Subscription์
request(_ demand: Subscribers.Demand)
๋ฅผ ํธ์ถํ๋ค. - Subscription์ Subscriber์
receive(_ input: Input) -> Subscribers.Demand
๋ฅผ ์๊ตฌ ๊ฐ์์ ๋ง๊ฒ ํธ์ถํด์ค๋ค. - Subscription์ด ๋๋๋ฉด subscriber์
receive(completion: Subscribers.Completion<Error>)
๋ฅผ ํธ์ถํ๋ค. - ์์ ์์ ์ ์ธํ ํ๋ค. (์์ง ์ค์ ๋ก ์คํ๋์ง ์์)
- ์ด ์์ ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด์ฅํ๋ Subscription์ instance๋ก ๋ฆฌํดํ๋ค.
- ์ฌ์ฉํ๋ ์ชฝ์์๋ ์ด Subscription์ ๋ณ์๋ก ๋ฐ๋๋ค.
๋ณ์๋ก ๋ฐ์ง ์์์ ๋์ ์ํฉ์ ์๊ฐํด๋ณด์. ๋ง์ฝ ๋ฐ์ง ์๋๋ค๋ฉด Subscription์ reference count๊ฐ 0์ด ๋๋ค. Subscription์์๋ ๋ณดํต Cancellable
์ ์ฑํํ๊ฒ ๋๋ฏ๋ก, reference count๊ฐ 0์ด ๋๋ ์์ ์ cancel()
method๊ฐ ํธ์ถ๋๊ฒ ๋๋ค. ์ด cancel()
method์๋ ๊ฐ์ง๊ณ ์๋ subscriber instance๋ฅผ ํ ๋นํด์ ํ๋ ๋ก์ง์ด ์ฒจ๋ถ๋์ด ์๋ค. ์ด๋ง์ ์ฆ์จ, ๋ณ์๋ก ๋ฐ์ง ์์์ ๊ฒฝ์ฐ subscription์ด ์ค์ง ์๋๋ค๋ ๋ง๊ณผ ๋์์, subscriber instance์ ๋ฉ๋ชจ๋ฆฌ ํด์ ๋ฅผ ์ฌ์ฉํ๋ ์ชฝ์์ ๊ด๋ฆฌํ ํ์๊ฐ ์๋ค๋ ๋ง์ด๋ค.
์ฌ๊ธฐ์ ์๋ฌธ ์ฌํญ์ด ์์ ํ ๋ฐ, Cancellable๋ก ๊ตณ์ด ๋ฆฌํดํ๋ ์ด์ ๊ฐ ๋ฌด์์ธ์ง? AnyCancellable๋ก ํ์ erasing์ ํ๋๊ฒ ์ข์ ๊ฒ ์๋์ง? ์ ๊ฐ์ ์๋ฌธ์ด ๋ค ์ ์๋ค. ์ค์ ๋ก ์ ์ฉํด๋ณด๋ AnyCancellable์ ๋จ์ํ Cancellable์ Type erasing์ ์ํ ๊ฒ์ด ์๋ ๋ฏํ๋ค. ๊ทธ๋ฐ ๋ฉ์๋๋ ์์๊ณ , Anycancellable์ ๊ฒฝ์ฐ class์๋ค. ๊ทธ๋์ ์ด๋ฌํ ์ ์ ๋ํด ๋ค์ ๊ณต๋ถ๋ฅผ ํด์ผ ํ ๊ฒ ๊ฐ์ ์ผ๋จ์ ๊ธ์ ์ฌ๊ธฐ์ ๋ฉ์ถ๋ค.
์ ๊น ์กฐ์ฌํด๋ณธ ๊ฒฐ๊ณผ๋ก๋ AnyCancellable์ธ ๊ฒฝ์ฐ์๋ ๋ฉ๋ชจ๋ฆฌ์์ ํด์ ๋๋ ์์ ์ ์๋์ผ๋ก cancel()
์ ํธ์ถํด์ค๋ค๊ณ ํ๋ค. ์ง๊ธ์ cancellable์ด๋ผ ๋ช
์์ ์ผ๋ก ์ ์ด๋์๋ค.
Whole Code
//
// CustomCombineViewController.swift
// test
//
// Created by Choiwansik on 2022/05/26.
//
import UIKit
import Combine
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
typealias Failure = Error
var subscription: Subscription?
func receive(subscription: Subscription) {
print("Received subscription")
self.subscription = subscription
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
print("Received value: \(input)")
return .none
}
func receive(completion: Subscribers.Completion<Error>) {
print("Received completion \(completion)")
}
}
extension URLSession {
func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
}
struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
// sink ๋์์ ๋ฐ๋ผํ์ฌ ๋ง๋ค์ด๋ด
func ssink(receiveValue: (Self.Output) -> Void, receiveCompletion: (Combine.Subscribers.Completion<Self.Failure>) -> Void) -> Cancellable {
let subscriber = DecodableDataTaskSubscriber<SomeModel>()
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
return subscription
}
typealias Failure = Error
let urlRequest: URLRequest
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
}
extension URLSession.DecodedDataTaskPublisher {
class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
where S.Input == Output, S.Failure == Error {
private let urlRequest: URLRequest
private var subscriber: S?
init(urlRequest: URLRequest, subscriber: S) {
self.urlRequest = urlRequest
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
if demand > 0 {
URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
defer { self?.cancel() }
if let data = data {
do {
let result = try JSONDecoder().decode(Output.self, from: data)
self?.subscriber?.receive(result)
self?.subscriber?.receive(completion: .finished)
} catch {
self?.subscriber?.receive(completion: .failure(error))
}
} else if let error = error {
self?.subscriber?.receive(completion: .failure(error))
}
}.resume()
}
}
func cancel() {
subscriber = nil
}
}
}
struct SomeModel: Decodable {}
class CustomCombineViewController: UIViewController {
var cancellable: Cancellable? // ์์
override func viewDidLoad() {
super.viewDidLoad()
self.setup()
}
deinit {
self.cancellable?.cancel()
}
func makeTheRequest() {
let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
self.cancellable = publisher.ssink(receiveValue: { value in
print("Received value: \(value)")
}, receiveCompletion: { completion in
print("Received completion: \(completion)")
})
}
func setup() {
self.view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 20, y: 40, width: 100, height: 50))
button.backgroundColor = .blue
self.view.addSubview(button)
button.titleLabel?.text = "request!!"
button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
self.makeTheRequest()
}
}
์ ๋ฆฌ
- Subscriber๋ ๊ถ๊ทน์ ์ผ๋ก Subscription์๊ฒ ๊ฐ์ ๋ฐ๋๋ค.
- Subscription์ life cycle์ ๊ด๋ฆฌํ๋ค.
- Subscription์ Subscriber๋ฅผ ๋ค๊ณ ์์ ์ ๋ฐ์ ์์ผ๋ฉฐ, ์ฑํํ Protocol Subscription์ด Cancellable์ ์ด๋ฏธ ์ฑํํ๊ณ ์๋ค.
- subscriber์์ subscription์ ์๊ณ ์๋ค๊ฐ ํน์ ์์ ์ ํด์ ํด์ฃผ๋ , ์๋๋ฉด ๋ฐ์ผ๋ก ๋นผ๋ ํ์ฌ ๊ตฌ๋ ์ life cycle์ ํด์ ํด์ฃผ์ด์ผ ํ๋ค.
- sink๋ sugar api๋ก ์ด ๊ตฌ๋ ๊ถ์ ๋ฐ์ผ๋ก ๋นผ์ฃผ๋ ๋ฏํ๋ค.
๋ค์์๋ AnyCancellableํ๊ณ Cancellable์ ์ฐจ์ด๋ฅผ ์ข ๋ด์ผํ ๋ฏ ํ๋ค. ๋!