[iOS] Combine 활용하기(1)

Han's·2023년 6월 25일
0
post-thumbnail

비밀번호 매칭 예제를 통해 Combine을 활용하기

예제 코드) passwordTextField에 비밀번호를 입력한 후 비밀번호 확인을 위해
passwordConfirmTextField에 비밀번호를 입력했을 때 확인 버튼의 활성화, 비활성화를 Combine을 사용해 비동기 처리한 예시입니다.


viewModel

final class PasswordViewModel {
    @Published var passwordInput: String = "" // 비밀번호 입력을 위한 프로퍼티
    @Published var passwordConfirmInput: String = "" // 비밀번호 확인 입력을 위한 프로퍼티
    
    lazy var isMatchPasswordInput: AnyPublisher<Bool, Never> = Publishers.CombineLatest($passwordInput, $passwordConfirmInput)
        .map { password, passwordConfirm in
            if password == "" || passwordConfirm == "" {
                return false
            }
            if password == passwordConfirm {
                return true
            } else {
                return false
            }
        }
        .eraseToAnyPublisher()
}
  • CombineLatest는 두 개 이상의 퍼블리셔(Publisher)를 조합하여 새로운 퍼블리셔를 생성하는 역할을 합니다. combineLatest는 각각의 퍼블리셔들이 새로운 이벤트를 방출할 때마다 해당 이벤트들을 조합하여 새로운 값을 방출하는 퍼블리셔를 생성합니다.
    즉, 최신의 이벤트들을 조합하여 새로운 이벤트를 방출하는 것입니다.

ViewController

class PasswordViewController: UIViewController {

    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var passwordConfirmTextField: UITextField!
    @IBOutlet weak var confirmBtn: UIButton!
    
    let viewModel = PasswordViewModel()
    var subscriptions = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configure()
        bind()
    }
}

private extension PasswordViewController {
    func configure() {
        navigationItem.title = "비밀번호 매치"
    }
    
    func bind() {
        passwordTextField.myTextPublisher
        	.print() // .print()를 통해 들어오는 값을 확인할 수 있습니다.
            .receive(on: DispatchQueue.main)
            .assign(to: \.passwordInput, on: viewModel)
            .store(in: &subscriptions)
        
        passwordConfirmTextField.myTextPublisher
            .receive(on: DispatchQueue.main)
            .assign(to: \.passwordConfirmInput, on: viewModel)
            .store(in: &subscriptions)
        
        viewModel.isMatchPasswordInput
            .print()
            .receive(on: DispatchQueue.main)
            .assign(to: \.isValid, on: confirmBtn)
            .store(in: &subscriptions)
    }
}

extension UIButton {
    var isValid: Bool {
        get {
            backgroundColor == .systemYellow
        }
        set {
            backgroundColor = newValue ? .systemYellow : .lightGray
            isEnabled = newValue
            let titleColor: UIColor = newValue ? .systemBlue : .white
            setTitleColor(titleColor, for: .normal)
        }
    }
}

extension UITextField {
    var myTextPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextField }
            .map { $0.text ?? "" }
            .eraseToAnyPublisher()
    }
}
  • sink 연산자는 데이터의 특정 처리를 위해 사용 또는 값을 받아와서 클로저에서 원하는 작업을 수행하거나, 값을 변환할 때 사용합니다.
    (필요에 따라 sink, assign 중 선택해서 사용하면 됩니다. 해당 예제에서는 assign을 사용하였습니다.)

  • assign 연산자는 View와 ViewModel 사이에서 데이터를 바인딩 할 때 유용하게 활용합니다. (단순 연결)
    \.은 Key Path 문법으로 속성이나 메서드를 가리키는 경로를 나타내는 표현 방식입니다.

  • .eraseToAnyPublisher()는 Publisher 타입을 AnyPublisher로 변환하는 역할을 합니다.
profile
 iOS Developer

0개의 댓글