회사서비스 특성상, 서드파티 라이브러리를 사용할수 없기 때문에 아키텍쳐가 조금 난잡한 경향이 있어서, 리액터킷을 도입하기 위해서
combine으로 리액터킷 프레임워크 만들어봤습니다.
pulse 구현
state로 구현을 하고 싶었으나, 구현에 시간이 조금 부족해서 그냥 바로 publisher를 방출하는 pulse로 구현하였습니다.
@propertyWrapper
final class Pulse<Value> {
private let subject: CurrentValueSubject<Value, Never>
var wrappedValue: Value {
get { subject.value }
set { subject.send(newValue) }
}
/// 외부에서 Combine 구독 할수 있도록
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
init(wrappedValue: Value) {
self.subject = CurrentValueSubject(wrappedValue)
}
}
Reactor Protocol 구현
//
// Reactor.swift
// ShopLiveStreamerSDK
//
// Created by yong C on 7/21/25.
// Copyright © 2025 com.app. All rights reserved.
//
import Foundation
import Combine
protocol ReactorProtocol {
associatedtype Action
associatedtype Mutation
func mutate(action: Action) -> Effect<Mutation>
func reduce(mutation: Mutation)
}
class Reactor<A, M>: ReactorProtocol {
typealias Action = A
typealias Mutation = M
let action = PassthroughSubject<Action, Never>()
var cancellables = Set<AnyCancellable>()
init() {
action.flatMap { [weak self] action -> AnyPublisher<Mutation, Never> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
return self.mutate(action: action).publisher
}
.sink { [weak self] mutation in
self?.reduce(mutation: mutation)
}
.store(in: &cancellables)
}
func mutate(action: Action) -> Effect<Mutation> { return .empty() }
func reduce(mutation: Mutation) {}
}
Effect 구현
struct Effect<M> {
let publisher: AnyPublisher<M, Never>
}
extension Effect {
// ✅ 단일 Mutation을 바로 Effect로
static func just(_ mutation: M) -> Effect {
Effect(publisher: Just(mutation).eraseToAnyPublisher())
}
// ✅ 외부 Publisher를 Effect로 감싸기
static func from(_ pub: some Publisher<M, Never>) -> Effect {
Effect(publisher: pub.eraseToAnyPublisher())
}
// ✅ 아무 값도 방출하지 않는 Effect
static func empty() -> Effect {
Effect(publisher: Empty().eraseToAnyPublisher())
}
// ✅ 여러 Effect를 순차적으로 이어붙이기
static func concat(_ effects: [Effect]) -> Effect {
let combined = effects.map(\\.publisher)
.reduce(Empty().eraseToAnyPublisher()) { acc, next in
acc.append(next).eraseToAnyPublisher()
}
return Effect(publisher: combined)
}
// ✅ 여러 Effect를 병렬로 실행해서 머지
static func merge(_ effects: [Effect]) -> Effect {
Effect(publisher: Publishers.MergeMany(effects.map(\\.publisher)).eraseToAnyPublisher())
}
}
reactorkit에서 action -> mutation -> State로 가는 동작을 구현하기 위해서 Effect라는 형태를 추가하여
Action -> Mutation -> Effect -> State변화 를 통해서 구현
Effect는 TCA에서 차용하여 고안하였습니다.
Reactor 실제 사용부
enum Action {
case viewDidLoad
}
enum Mutation {
case updateSomething(value)
}
@Pulse var value: String?
override func mutate(action: Action) -> Effect<Mutation> {
switch action {
case .viewDidLoad:
let onBoardingState = UserDefaults.standard.bool(forKey: "didShowStreamOnboarding") == false
let onBoardingStateMutation: Effect<Mutation> = onBoardingState ? .just(.updateOnboardingState(true)) : .empty()
let updateCurrentDeviceID = dependency.updateCurrentDeviceID()
.map { _ in Mutation.empty }
.replaceError(with: .empty)
return .concat([
.just(.updateNeedToLockOrientation(.portrait, .portrait)),
.just(.updateAudioLevelMeterState(.arrangeLayers)),
.just(.updateSetUpInitialStream),
setCamera(),
getWebViewPlayerURL(),
onBoardingStateMutation,
.callAPI(updateCurrentDeviceID)
])
이때
var updateCurrentDeviceID: (
) -> AnyPublisher<ShopLiveBaseResponse, ShopLiveError>
이렇게 퍼블리셔로 들어오는 값은, .callAPI로 감싸주면 된다.
실제로 여러개의 publisher가 연속적으로 호출되거나, 성공, 혹은 실패 케이스를 처리하려면 해당부분에서 처리해주면됨,
에러 케이스 구현
.catch({ [weak self] error in
guard let self else { return Just([Mutation.empty]) }
let result: [Mutation] = [
[handleAPIError(error: error)],
handleCampaginAvailabilityError(error)
].flatMap { $0 }
return Just(result)
})
캐치에서 에러를 받아, Mutation으로 변경 하여 Publisher로 방출
ViewController 사용부
init(
reactor: SRTStreamReactor
) {
super.init(nibName: nil, bundle: nil)
defer {
self.reactor = reactor
bind(reactor: reactor)
}
}
바인딩
func bind(reactor: SRTStreamReactor) {
bindAction(to: reactor)
bindState(from: reactor)
}
func bindState(from reactor: SRTStreamReactor) {
reactor.$setUpInitialStream
.receive(on: DispatchQueue.main)
.compactMap { $0 }
.sink { [weak self] _ in
guard let self else { return }
onSetUpInitialStream()
}
.store(in: &cancellables)
reactor.$streamingState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
guard let self else { return }
self.updateStreamingState(state)
}
.store(in: &cancellables)
reactor.$webViewURL
.receive(on: DispatchQueue.main)
.sink { [weak self] url in
guard let self else { return }
self.loadWebView(url)
}
.store(in: &cancellables)
네트워크
네트워크 주입부는, https://matdongsane.tistory.com/208 참고
반응형
'앱 > Swift' 카테고리의 다른 글
| [Swift] GCD와 swift concurrency가 나오게된 배경과, concurrency 기본개념 (1) | 2026.01.19 |
|---|---|
| [iOS] moya를 combine을 이용해서 구현해보자 (0) | 2026.01.18 |
| Reactorkit의 Pulse 구현부에 대해서 (0) | 2025.02.18 |
| Moya Mock Data 사용하기(feat. test Double) (0) | 2025.01.31 |
| reactorkit에서 testcode 작성 하는법(feat. nimble) (0) | 2025.01.31 |