UIKit과 POP 함께 쓰기

Junyoung Lee·2022년 10월 23일
0
post-thumbnail

Protocol vs. Class에서 이어지는 내용입니다.

터치가 가능한 커스텀 UIView 만들기

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으로 프로토콜 구현하기

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 객체를 이용하여 메모리 반환까지 처리해주면 끝!

이제 프로토콜은 정의와 구현까지 끝났으니 활용 예시를 볼까요?


커스텀 UI 컴포넌트 정의

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
	...
}

위와 같이 TouchViewTappable 프로토콜을 따르게 설정해줍니다. 그러면 setTapAction() 메소드를 사용할 수 있겠죠?

init(_ message: String) {
    self.message = message
    super.init(frame: .zero)

    setTapAction {
        print("메세지 출력: \(self.message)")
    }
}

생성자에서 message 프로퍼티를 인자로 받아 정의해주고, super의 생성자를 호출해줍니다. 그리고 Tappable을 통해 정의된 setTapAction을 이용해 탭할 때마다 message가 출력되게 해줍니다.


사용 후기

이것 외에도 Animatable, NeedsLoading 등 다양한 프로토콜을 만들어 유용하게 사용했습니다. 확실히, 프로토콜을 사용하니 재사용성도 높아지고 구조화도 명확하게 코드를 작성할 수 있네요. 아직 POP를 해보지 않으신 분들은, 한번 시도해보는 것을 강력히 추천드립니다!

profile
여행과 피자를 좋아하는 iOS 개발자입니다. 피자에는 파인애플이 들어가지 않습니다.

0개의 댓글