Protocol vs. Class에서 이어지는 내용입니다.
iOS 앱 개발을 할 때에 UIKit으로 제공되는 컴포넌트도 많이 사용하지만, 직접 만들어서 사용하는 컴포넌트도 꽤 많습니다.
예를 들어 터치가 가능한 이미지 같은 것들이 있겠죠? 그런 경우에는 직접 제스쳐 이벤트를 추가해주어야 합니다.
class ViewController: UIViewController {
private let myTouchView = UIView() // 탭이 필요한 UIView 객체
override func viewDidLoad() {
...
// 탭 동작을 인식할 Recognizer 객체 선언
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTapGesture(sender:)))
// 선언한 Recognizer 객체를 추가
myTouchView.addGestureRecognizer(tapGesture)
}
// 탭 이벤트 발생 시 수행할 메소드
@objc func onTapGesture(sender: UITapGestureRecognizer) {
print("Hello, World!")
}
}
탭 이벤트는 위와 같이 추가할 수 있습니다. UIView 객체를 선언하고, 필요한 곳에서 GestureRecognizer를 통해 원하는 제스쳐를 생성한 후 addGestureRecognizer() 메소드를 통해 추가하는 것이죠. 하지만 위 방식에는 아래와 같은 단점들이 있습니다.
저는 비동기 라이브러리인 RxGesture를 사용해서 프로토콜을 작성했습니다.
protocol Tappable {
var disposeBag: DisposeBag { get }
func setTapAction(action: @escaping () -> void)
}
위는 탭이 가능한 모든 커스텀 UI를 위한 Tappable 프로토콜입니다. 이 프로토콜을 따르는 모든 UI는 탭이 가능하다는 것을 의미하게 됩니다. 그러면 위 코드를 좀 더 자세하게 설명해보겠습니다.
var disposeBag: DisposeBag { get }
우선 메모리 관리를 위해 DisposeBag을 포함해주었습니다. 단, 프로토콜이기 때문에 실제로 할당되는 것은 아니고, 나중에 프로토콜을 따르는 구조체/클래스를 만들 때 거기에서 직접 선언해주어야 합니다.
그리고 get 옵션만 주었습니다. DisposeBag를 다시 사용할 일은 없으니까요.
func setTapAction(action: @escaping () -> void)
그리고 이 프로토콜의 핵심인, 탭 이벤트를 처리할 setTapAction() 메소드를 정의했습니다. 이 메소드는 인자로 클로저인 action을 받아, 탭 이벤트에 연결해줍니다.
이제 프로토콜 정의가 끝났네요!. 하지만 이대로 두면 Tappable 프로토콜을 따르는 모든 구조체/클래스에서 똑같은 코드를 계속 반복해서 작성해야 합니다.
extension Tappable where Self: UIView {
func setTapAction(action: @escaping () -> Void) {
self.rx.tapGesture()
.when(.recognized)
.subscribe(onNext: { _ in
action()
}).disposed(by: disposeBag)
}
Swift에서는 extension을 통해, 프로토콜의 메소드를 구현할 수 있습니다. 개인적으로 이 부분은 Java의 추상클래스와 비슷하다고 생각합니다.
위는 작성한 전체 코드이고, 아래에서 하나씩 설명드리겠습니다.
extension Tappable where Self: UIView { ... }
제가 만들 커스텀 UI는 모두 UIView를 상속 받을 것입니다. 따라서 그런 경우만을 위한 메소드 구현을 한다면 상당수의 코드를 줄일수 있겠죠?
Swift에서는 where Self:
키워드를 통해 특정 구조체/클래스가 프로토콜을 따르는 경우를 처리할 수 있습니다. 지금은 UIView를 위한 구현을 하고 있기 때문에 UIView
를 적어 주었습니다.
self.rx.tapGesture()
.when(.recognized)
저는 위에서 언급했듯이, RxGesture를 사용했습니다. 이 라이브러리에서는UIView를 위한 Rx
객체를 제공하여 비동기 이벤트를 간편하게 처리할 수 있습니다.
지금은 탭하는 기능을 추가할 것이기 때문에 tapGesture()
를 사용했고, 탭 이벤트 발생이 인식 되었을 때 원하는 기능을 수행할 것이므로 .when(.recognized)
를 사용했습니다.
.subscribe(onNext: { _ in
action()
}).disposed(by: disposeBag)
마지막으로, subscribe
로 이벤트 구독을 해주고 onNext
를 통해 이벤트 발생 시 인자로 전달받은 action()
을 실행하게 해주었습니다. 그리고 DisposeBag
객체를 이용하여 메모리 반환까지 처리해주면 끝!
이제 프로토콜은 정의와 구현까지 끝났으니 활용 예시를 볼까요?
class TouchView: UIView, Tappable {
private let message: String
init(_ message: String) {
self.message = message
super.init(frame: .zero)
setTapAction {
print("메세지 출력: \(self.message)")
}
}
}
예시로 문자열 변수 message
를 프로퍼티로 갖고, 탭할 때마다 message
를 출력하는 UIView인 TouchView
를 만들었습니다. 그러면 코드를 하나씩 살펴보겠습니다.
class TouchView: UIView, Tappable {
private let message: String
...
}
위와 같이 TouchView
가 Tappable
프로토콜을 따르게 설정해줍니다. 그러면 setTapAction()
메소드를 사용할 수 있겠죠?
init(_ message: String) {
self.message = message
super.init(frame: .zero)
setTapAction {
print("메세지 출력: \(self.message)")
}
}
생성자에서 message
프로퍼티를 인자로 받아 정의해주고, super
의 생성자를 호출해줍니다. 그리고 Tappable
을 통해 정의된 setTapAction
을 이용해 탭할 때마다 message
가 출력되게 해줍니다.
이것 외에도 Animatable
, NeedsLoading
등 다양한 프로토콜을 만들어 유용하게 사용했습니다. 확실히, 프로토콜을 사용하니 재사용성도 높아지고 구조화도 명확하게 코드를 작성할 수 있네요. 아직 POP
를 해보지 않으신 분들은, 한번 시도해보는 것을 강력히 추천드립니다!