ã¿ãªããããã«ã¡ã¯ãã©ã¯ãã§iOSã¨ã³ã¸ãã¢ããã¦ããdarquroã§ãã ã©ã¯ãã¯å»å¹´12æã®ãªãªã¼ã¹ã§ãµãã¼ãOSãiOS13以ä¸ã«ãã¾ããã ããã«ä¼´ããCombine Frameworkã®å©ç¨ããããã¯ã·ã§ã³ã³ã¼ãã«æ¬æ ¼å°å ¥ãã¾ããã ã©ã¯ãiOSã¢ããªã®ã¢ã¼ããã¯ãã£ã¯MVP+Clean Architectureãæ¡ç¨ãã¦ãããRxSwiftã®ç½®ãæãã§ã¯ãªããç¾å¨ã¯UIã®ããªãã¼ã·ã§ã³ã®ä¸é¨ãAPIãªã¯ã¨ã¹ãã®ä¸é¨ã§ä½¿ç¨ãã¦ããå½¢ã«ãªãã¾ãã
ä»åã®æç¨¿ã§ã¯ãCombine Frameworkã®åãããã³ã«ãã¤ãã¬ã¼ã¿ãªã©ã®ããããããã¦ãAPIãªã¯ã¨ã¹ãã®é¨åã®å®è£ ãUnitTestã§ããã£ããã¤ã³ããªã©ããç´¹ä»ãããã¨æãã¾ãã
ç°å¢
- Xcode: 12.3
- Swift: 5
ããããããã§ï¼ï¼
ã¾ãã¯Combineã«ã¤ãã¦ãããã
Publisher㯠å¤ã渡ã ã å®äºãå¼ã¶ ã ã¨ã©ã¼ãè¿ã ã®3ã¤ãï¼
struct MyPublisher: Publisher { typealias Output = String typealias Failure = MyError struct MyError: Error {} func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { // ãªããããããã¦çµæãåºå // recieveã§OutPutãä½åã§ã渡ããã¨ãã§ãã subscriber.receive("A") // finishã¨failureã¯ã©ã¡ãã1åå¼ã¹ã¾ã subscriber.receive(completion: .finished) // subscriber.receive(completion: .failure(.init())) } }
sinkãããã¨çµæãåãåããã§ï¼
MyPublisher() .sink(receiveCompletion: { completion in switch completion { case .finished: print("finish") case .failure(let error): print(error) } }, receiveValue: { value in print(value) }) // -> A // -> finish
èªåã§Publisherãç¬èªå®è£ ããªãã¦ãã便å©ã¯ã©ã¹ãç¨æããã¦ãã§ï¼
Future
1ã¤ã®å¤ã¨ãå®äºã失æãè¿ããJust
1åã ãå¤ãè¿ãã¦ãå®äºDeferred
Futureã¯ã¤ã³ã¹ã¿ã³ã¹ä½æããæç¹ã§ãã¯ãã¼ã¸ã£ã¼å ã®å®è¡ãè¡ãããã®ã«å¯¾ããDeferredã¯sinkãããã¿ã¤ãã³ã°ã§å®è¡Empty
ä½ãå¤ã¯è¿ããªããå®äºã ãå¼ã°ããFail
failureã ãå¼ã°ããRecord
è¤æ°å¤ãè¿ãã¦ãå®äºã失æãè¿ãã
let publisher = Future<Int, MyError> { promise in // successã§å¤ã渡ãï¼å®äº promise(.success(1)) // failure // promise(.failure(.init())) }
let publisher = Record<Int, MyError> { promise in promise.receive(1) promise.receive(2) promise.receive(completion: .finished) // promise.receive(completion: .failure(.init())) }
.eraseToAnyPublisher() ã§AnyPublisher<Outpub, Failure> ã«å¤æããããï¼
Publisherãè³¼èªããå´ã¯sinkã§ãããã¨ã ãåããã°ããã®ã§ãç¹å®ã®åã«ä¾åãããªãã
let publisher = Future<Int, MyError> { promise in // successã§å¤ã渡ãï¼å®äº promise(.success(1)) // failure // promise(.failure(.init())) }.eraseToAnyPublisher() // publisherã¯AnyPublisher<Int, MyError> åã«ãªã
sinkãããstoreããã§ï¼
var cancellables: Set<AnyCancellable> = [] MyPublisher() .sink(receiveCompletion: { completion in switch completion { case .finished: print("finish") case .failure(let error): print(error) } }, receiveValue: { value in print(value) }) .store(in: &cancellables)
- sinkã®è¿ãå¤ã¯AnyCancellable
- AnyCancellableã¯cancel()ããã
storeã®ä»ã«ãassignã§å¤ãçªã£è¾¼ããã
class MyClass { var value = "" } let myclass = MyClass() Just("a") .assign(to: \.value, on: myclass) print(myclass.value)
sibsribe(on:)ã¨receive(on:)ã§ã¹ã¬ããæå®ã§ããã§ï¼
MyPublisher() // subscribeããã¨ãããããåã®å¦çã®ã¹ã¬ãããæå® .subscribe(on: DispatchQueue.global()) // receiveããããã¨ããã以éã®ã¹ã¬ãããæå® .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in switch completion { case .finished: print("finish") case .failure(let error): print(error) } }, receiveValue: { value in print(value) }) .store(in: &cancellables)
map使ã£ã¦éãå¤ã«å¤æã§ããã§ï¼
Just(1) .map({ "10åã«ããã:\($0 * 10)" }) .sink(receiveValue: { print($0) }) // -> 10åã«ããã:10
Subjectã¯CurrentValueSubjectã¨PassthroughSubjectã®2種é¡ï¼
https://developer.apple.com/documentation/combine/subject
CurrentValueSubject
ã¯åæå¤ãæã£ã¦ãã®ã§sinkããã¨ãæåã®å¤ãåããã
PassthroughSubject
ã¯åæå¤ãªãã
let subject = PassthroughSubject<String, Error>() subject .sink(receiveCompletion: { completion in switch completion { case .finished: print("finishd") case .failure(let error): print(error) } }, receiveValue: { value in print(value) }) .store(in: &cancellables) subject.send("a") subject.send("b") subject.send(completion: .finished)
subjectãã¯ã©ã¹ã®å¤ã«å ¬éããã¨ãã«ãå¤ããå¤ãæ´æ°ããå¿ è¦ãªããã°ãpublisherã«ããã¨ããã§ï¼
class MyClass { var publisher: AnyPublisher<String, Error> { subject.eraseToAnyPublisher() } private let subject = PassthroughSubject<String, Error>() }
APIãªã¯ã¨ã¹ãã®Publisherãä½ãã§ï¼
ããã§ãå°ãã©ã¯ãã®äºæ ã«ãªãã¾ãããAPIãªã¯ã¨ã¹ãã¯ãHTTPãªã¯ã¨ã¹ããããã¼ã®è¿½å ãªã©å ¨ã¦ã®ãªã¯ã¨ã¹ãã«å ±éããå¦çãã¾ã¨ãã¦ãã¾ãã 以ä¸ã®ã³ã¼ãã®APICoreClientã¯"ãªã¯ã¨ã¹ãã®åãã©ã¡ã¼ã¿ã渡ãã¨URLSessionTaskãè¿ãã¦ãããã¯ã©ã¹"ã¨æ³å®ãã¦é ããã°ã¨æãã¾ãã
struct FetchItemPublisher: Publisher { typealias Output = FetchItemResponse typealias Failure = ErrorResponse private let coreClient: APICoreClientable private let itemID: Int init(coreClient: APICoreClientable = APICoreClient.shared, itemID: Int) { self.coreClient = coreClient self.itemID = itemID } func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { let task = coreClient.dataTask( httpMethod: "GET", path: "/api/items", parameters: [ "item_id": itemID ], success: { _, responseObject in do { let jsonData = try JSONSerialization.data(withJSONObject: responseObject) let response = try JSONDecoder().decode(FetchItemResponse.self, from: jsonData) _ = subscriber.receive(response) } catch { subscriber.receive(completion: .failure(.parseError)) } subscriber.receive(completion: .finished) }, failure: { _, error in let response = coreClient.parse(error) subscriber.receive(completion: .failure(response)) }) // Subscriptionãä½ã£ã¦ç»é²ãã let subscription = FetchItemSubscription(combineIdentifier: .init(), task: task) subscriber.receive(subscription: subscription) task.resume() } } struct FetchItemSubscription: Subscription { let combineIdentifier: CombineIdentifier let task: URLSessionTask func request(_ demand: Subscribers.Demand) {} func cancel() { task.cancel() } } struct FetchItemResponse: Decodable { ... } struct ErrorResponse: Error { ... static var parseError: Self { ... } } protocol APICoreClientable { func dataTask(httpMethod: String, path: String, parameters: [AnyHashable: Any]?, success: (URLSessionTask, Any) -> Void, failure: (URLSessionTask, Error) -> Void) -> URLSessionTask func parse(_ error: Error) -> ErrorResponse } struct APICoreClient: APICoreClientable { static let shared = APICoreClient() func dataTask(httpMethod: String, path: String, parameters: [AnyHashable : Any]?, success: (URLSessionTask, Any) -> Void, failure: (URLSessionTask, Error) -> Void) -> URLSessionTask { return ... } func parse(_ error: Error) -> ErrorResponse { return ... } }
FetchItemPublisher(itemID: 100) // APIã®ãªã¯ã¨ã¹ãã¯å¥ã¹ã¬ããã§ .subscribe(on: DispatchQueue.global()) // çµæã®åãåãã¯ã¡ã¤ã³ã¹ã¬ããã§ .receive(on: DispatchQueue.main) // ã¿ã¤ã ã¢ã¦ããæå®ãã .timeout(.seconds(20), scheduler: DispatchQueue.main, customError: { .timeout }) .sink(receiveCompletion: { completion in switch completion { case .finished: break; case .failure(let error): //ã¨ã©ã¼å¦ç print(error) } }, receiveValue: { value in // å¤ãåãåã£ãå¾ã®å¦ç }) .store(in: &cancellables)
ãã¤ã³ãã¨ãã¦ã¯ã以ä¸ã®3ã¤ã«ãªãã¨æãã¾ãã
Subscription
ãä½ã£ã¦Cancelåºæ¥ãããã«ããsubscribe(on:)
ã¨receive(on:)
ã§ã¹ã¬ããæå®ãããtimeout(_:scheduler:options:customError:)
ã使ã£ã¦ã¿ã¤ã ã¢ã¦ããè¨å®ãã
URLSession dataTaskPublisher(for:)ã使ãã¨ãã£ã¨ã·ã³ãã«ã«æ¸ããã¨æãã®ã§ãããã©ã¯ãã®æ¢åã³ã¼ãããé¨åçã«Combineãå°å ¥ããã«ã¯Publisherããä½ã£ã¦ãããå¿ è¦ããããå°ãã³ã¼ãéãå¢ãã¦ãã¾ãã¾ããã å°æ¥çã«ãªãã¡ã¯ã¿ãªã³ã°ããã¦ããããã¨æãã¾ãã
UnitTestã¯ç½ ã ãããï¼
æ¨æºã®APIã使ç¨ããã¨ãexceptionã使ããfulfillã¨waitã使ã£ã¦ãéåæå¦çãå¾ ã¡åããããã«ãªãã¾ãã
var cancellables = Set<AnyCancellable>() let stub = APICoreClientStub() let exp = expectation(description: "get API response") FetchItemPublisher(core: stub, itemID: 100) .sink(receiveCompletion: { _ in exp.fulfill() }, receiveValue: { value in XCTAssertNotNil(response) }) .store(in: &cancellables) wait(for: [exp], timeout: 3)
ä¸è¨ã®ããã«Publisherã ãããã¹ãããå ´åã¯ãåé¡ã¯ãªãã®ã§ãããPublisherã使ãã.subscribe(on: DispatchQueue.global())
ã.receive(on: DispatchQueue.main)
ãæã¿ãsinkããã¦ããã¡ã½ããããã¹ããããã¨ããã¨ãsink以éãå¼ã°ãããå¼ã°ããªãã£ããã¨ããã¹ããä¸å®å®ã«ãªãã¾ããã
ã¾ããStubãMockã®å´ã¯ã©ã¹ã§ããã¼ã®ãã¼ã¿ãä½ã£ã¦ããã¹ãã³ã¼ãã§ãåãå¤ãåç §ãããã¨ããã¨ãsinkãå¼ã°ããªããªãã±ã¼ã¹ãããããªãã¹ãã¹ã³ã¼ãå ã§ã¤ã³ã¹ã¿ã³ã¹ã®åç §ãéãã¦ããæ¹ãå®å®ããããã§ããã ãã®ãããã¯ãããããXcodeã®ã¦ããããã¹ãã®å®è¡ããã»ã¹ã«ä¾åãã¦ããã並åå¦çãããã¦ãããã¨ãå½±é¿ãã¦ããã®ã§ã¯ãªããã¨æ³å®ãã¦ããã®ã§ããã詳ããã¨ããã¯ããããã£ã¦ããã¾ããã»ã»ã»ã
ãªã®ã§ãã¦ããããã¹ãã§ã¯Publisherã®ãã¹ãã¾ã§ã¯ãªãã¹ãæ¸ããã¹ã¬ãããè·¨ãã§sinkãæ¸ãã¦ããã¨ããã¯åä½ç¢ºèªãQAã§ã«ãã¼ãããã¨ã«ãã¾ããã
ã¾ããã©ã¯ãã§ã¯å°å ¥ãã¦ãã¾ããããRxSwiftã®TestSchedulerã®ããã«æ¸ããOSSã©ã¤ãã©ãªãè¯ãããã§ãã
ãããã«
ä»åã¯Combine Frameworkã«ã¤ãã¦ãç´¹ä»ãã¾ããã ã©ã¯ãã§ã¯æ°ããSwiftã®APIãç©æ¥µçã«ä½¿ããªããããµã¼ãã¹æ¹åãè¡ã£ã¦ãã¾ãã Swiftã®æ°ããæ©è½ã«é¢å¿ãé«ããä¸ç·ã«ã¢ããªéçºããã¦ãããã¡ã³ãã¼ãåéãã¦ãã¾ãã www.wantedly.com ã«ã¸ã¥ã¢ã«é¢è«ã§ãç§éã®ãã¼ã ã«ã¤ãã¦ç´¹ä»ããã¦é ãã¾ãã