[SwiftUI] Combine: Publishers and Subscribers

Junyoung Park·2022년 8월 20일
0

SwiftUI

목록 보기
25/136
post-thumbnail
post-custom-banner

Publishers and Subscribers in Combine with a SwiftUI project | Continued Learning #25

Combine: Publishers and Subscribers

구현 목표

  • 타이머, 텍스트 필드 카운트 등 특정 변수를 Subcribe한다.
  • 타이머가 10초 이상, 세 글자 이상 텍스트 필드에 입력하다면 버튼이 나타난다.

구현 태스크

  1. 타이머 시간을 subscribe하기
  2. 텍스트 필드 값을 subscribe한 불리언 변수 값 구현
  3. 다른 두 변수 값을 subsribe하기

핵심 코드

    private var cancellables = Set<AnyCancellable>()

    func addTextFieldSubscriber() {
        $textFieldText
            .debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
            .map { (text) -> Bool in
                if text.count > 3 {
                    return true
                }
                return false
            }
            .sink(receiveValue: { [weak self] isValid in
                guard let self = self else { return }
                self.textIsValid = isValid
            })
            .store(in: &cancellables)
    }
  • 텍스트 필드 텍스트를 subscribe, 즉 값의 변화를 듣고 그 값을 확인한다.
  • debounce를 통해 연속적인 입력이 일어난다면 map 이후의 메소드를 처리하지 않는다. 연속적인 타이핑이 일어나지 않을 때 map 실행하기 때문에 연산 비용을 아낄 수 있다.
  • sink: 확인 연산 뒤 그 결과에 따라 주어진 데이터를 넣어줄 수 있다. 약한 참조로 선언했기 때문에 nil로 아웃되면서 이후 유저에 의한 강제 중단 등 이벤트에 대응할 수 있다.
  • store: Combine 프레임워크가 제공하는 AnyCancellable 오브젝트 집합에 구독을 취소한 데이터를 담아둘 수 있다. 즉 취소 가능함을 알려준다.

소스 코드

import SwiftUI
import Combine

class SubscriberViewModel: ObservableObject {
    @Published var textFieldText = ""
    @Published var count: Int = 0
    @Published var textIsValid: Bool = false
    @Published var showButton: Bool = false
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        setUpTimer()
        addTextFieldSubscriber()
        addButtonSubsriber()
    }
    
    func addTextFieldSubscriber() {
        $textFieldText
            .debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
            .map { (text) -> Bool in
                if text.count > 3 {
                    return true
                }
                return false
            }
            .sink(receiveValue: { [weak self] isValid in
                guard let self = self else { return }
                self.textIsValid = isValid
            })
            .store(in: &cancellables)
    }
    
    func addButtonSubsriber() {
        $textIsValid
            .combineLatest($count)
            .sink { [weak self] (isValid, count) in
                guard let self = self else { return }
                if isValid && count >= 10 {
                    self.showButton = true
                } else {
                    self.showButton = false
                }
            }
            .store(in: &cancellables)
    }
    
    func setUpTimer() {
        Timer
            .publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.count += 1
            }
            .store(in: &cancellables)
    }
}

struct SubscriberBootCamp: View {
    @StateObject private var viewModel = SubscriberViewModel()
    var body: some View {
        VStack {
            Text("\(viewModel.count)")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.gray)
            TextField("Type something here...", text: $viewModel.textFieldText)
                .padding(.leading)
                .frame(height: 55)
                .font(.headline)
                .background(Color.gray.opacity(0.3))
                .cornerRadius(10)
                .overlay(
                    ZStack {
                        Image(systemName: "xmark")
                            .foregroundColor(.red)
                            .opacity(
                                viewModel.textFieldText.count < 1 ? 0.0 :
                                viewModel.textIsValid ? 0.0 : 1.0)
                        Image(systemName: "checkmark")
                            .foregroundColor(.green)
                            .opacity(viewModel.textIsValid ? 1.0 : 0.0)
                    }
                    .font(.headline)
                    .padding(.trailing)
                    , alignment: .trailing
                )
            
            Button(action: {
                
            }, label: {
                Text("Submit".uppercased())
                    .font(.headline)
                    .foregroundColor(.white)
                    .frame(height: 55)
                    .frame(maxWidth: .infinity)
                    .background(Color.blue)
                    .cornerRadius(10)
                    .opacity(viewModel.showButton ? 1.0 : 0.0)
            })
            .disabled(!viewModel.showButton)
        }
        .padding()
    }
}

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글