RxSwift를 21일간 공부하는 루틴
"Rx를 기깔나게 쓰는 신입개발자 도전" 시작 🚀
휴학을 하고, 스타트업의 앱 개발을 담당하면서 커스텀 뷰를 적극적으로 활용했다.
커스텀 뷰를 만든 이유는 한번 만들어 놓으면, 비슷한 디자인 내에서 재사용이 가능했기때문이다.
textfield의 text속성이 update될때 next 이벤트를 방출하게 하고 싶으면, myTF.rx.text
처럼 rx 네임스페이스를 통해 접근, 활용할 수 있다.
하지만 만약 구현해야하는 기능이 제공되지 않는다면 어떻게 해야할까?
답은 정해져 있겠지만.. extension을 하여 직접 구현하면된다!! 🤪😜
UIButton+Rx.swift 의 일부이다.
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
tap 속성을 보면,
Reactive extension후, UIButton을 확장할 거기때문에 base를 UIButton으로 하였다.
ControlEvent메서드를 호출하고 .touchUpInside
이벤트를 전달한다.
커스텀도 위에서 살펴본 예시 코드처럼 구현하면 될것이다.
UX 관점에서 본다면, 유저가 현재 입력하고 있는 textfield가 강조되면 좋을 것이다. 왜냐하면 현재 입력을 하고 있는 입력창에 대한 시각적인 강조를 통해 "현재 여기를 입력하고 있구나" 라는 느낌을 줄 수 있기 때문이다.
따라서 Driver 를 공부하면서 만든 코드에 추가를 하였다.
1. textfield에 focus가 되면, 테두리의 color, width를 다르게 하여 강조한다
2. edit이 끝나면, 기본 형식으로 돌아온다.
1. focus 되었을때
2. edit 끝났을때
만약 UITextFieldDelegate 로 구현한다면 어떻게 진행을 했을까.
textFieldDidBeginEditing
와 textFieldDidEndEditing
함수를 써서 구현했을 것이다.
아쉽게도 rxcocoa에서 제공해주는 textfield의 reactive extension 에는 textFieldDidBeginEditing
와 textFieldDidEndEditing
기능이 없다.
따라서 직접 구현을 해줘야한다.
우선, textfield의 edit이 시작되었는지, 끝났는지에 대한 event 전달이 필요했다.
extension Reactive where Base : UITextField {
var beginEditing : ControlEvent<Void> {
return controlEvent(.editingDidBegin)
}
var endEditing : ControlEvent<Void> {
return controlEvent(.editingDidEnd)
}
}
.editingDidBegin
과 .editingDidEnd
이벤트를 전달하였다.이벤트를 받았으면, textfield의 border의 color속성과, width 속성을 변경해줘야했다.
이는 UIBinding에 쓰이는 Observer인 Binder를 사용하였다.
public init<Target: AnyObject>(_ target: Target, scheduler: ImmediateSchedulerType = MainScheduler(), binding: @escaping (Target, Value) -> Void) {
weak var weakTarget = target
self.binding = { event in
switch event {
case .next(let element):
_ = scheduler.schedule(element) { element in
if let target = weakTarget {
binding(target, element)
}
return Disposables.create()
}
case .error(let error):
rxFatalErrorInDebug("Binding error: \(error)")
case .completed:
break
}
}
}
Binder 생성자를 보면 Binder가 확장할 target(UI component)와, scheduler, 클로저를 전달하는 binding이 파라미터이다.
이때, UI는 mainThread에서 업데이트가 되어야하기때문에 scheduler는 MainScheduler()를 default값으로 갖는다.
따라서, binding 실행은 곧 mainThread에서 실행됨을 보장받을수 있다.
파라미터 binding의 클로저는 Binder가 확장할 UI Component 와 Binder로 전달된 값을 파라미터로 받는다.
전달된 값을 통해 UI Component의 속성을 변경 시킬수 있다.
우선, 테두리의 색상과 width를 바꿀것이기 때문에, UIColor와 CGFloat을 가진 struct 로 따로 선언을 해주었다.
struct ColorWidth {
let color : UIColor
let width : CGFloat
}
그다음 extension Reactive where Base : UITextField
에서 borderColorWidth 속성을 추가하였다.
var borderColorWidth : Binder<ColorWidth?> {
return Binder(self.base) { textfield, colorWidth in
textfield.layer.borderColor = colorWidth?.color.cgColor
textfield.layer.borderWidth = colorWidth?.width ?? 0.5
}
//단축인자
// return Binder(self.base) {
// $0.layer.borderColor = $1?.color.cgColor
// $0.layer.borderWidth = $1?.width ?? 0.5
// }
binding 클로저를 구현해보면, Binder가 확장할 UITextField 와 Binder로 전달된 값인 ColorWidth를 파라미터로 받는다.
그래서 받은 ColorWidth의 프로퍼티값들을 통해 textfield의 속성을 변경 시킬수 있다.
클로저를 단축인자를 활용해 줄여보았다.
내가 생각한 장점은 코드가 있어보이고 단순해보인다.
하지만 개인적인 생각으로 만약 팀작업이라면, borderColorWidth 에서는 매개변수를 표기한 것이 팀원들이 코드를 이해하는데 효과적일것같다.
왜냐하면 파라미터의 네이밍을 통해 구현하고자 하는 것이 확실히 드러난다고 생각하기 때문이다.
이제 만든 extension들을 활용해 바인딩을 해 줄 수 있다.
map 연산자를 통해 만들어놓은 ColorWidth 로 변환을 시키고, 이를 myTF.rx.borderColorWidth 에 바인딩 해주면 끝이다🎉👏
myTF.rx.beginEditing
.map { ColorWidth(color: .systemCyan, width: 2.0) }
.bind(to: myTF.rx.borderColorWidth)
.disposed(by: disposeBag)
myTF.rx.endEditing
.map { ColorWidth(color: .systemGray, width: 0.5) }
.bind(to: myTF.rx.borderColorWidth)
.disposed(by: disposeBag)
rx를 사용하면서, 확실히 느끼는점들이 있다.
1. 데이터 흐름을 직관적으로 볼 수 있다.
코드를 보면 어떤 이벤트가 발생하고 연산자를 통해 데이터를 변경하는 로직이라서 그런지 훨씬 데이터 흐름이 직관적으로 보이는 것 같다.
2. rx를 잘 활용한다면 mvvm 패턴과 호환이 잘 될것 같다.
뷰와 모델, 뷰모델의 분리가 훨씬 용이해질것 같다.
왜나하면 mvvm패턴에서 viewModel에 있는 모델들의 데이터들을 view에 표시하게 되는데, 값을 관찰하고 바인딩이 용이한 rxswift와 잘 어울린다고 생각이 들었기 때문이다.