-Today's Learning Content-

  • UIButton/addTarget
  • Delegate

1. UIButton.addTarget

개념 정리

UIButton의 메소드 중 .addTarget(_:action:for:) 메소드가 있는데, 이 메소드는 버튼에 이벤트가 발생했을 때 실행시킬 동작을 정의할 수 있는 메소드이다.

1) addTarget과 매개변수

iOS 앱개발 입문의 과제인 계산기 앱 만들기를 구현 중에 버튼 액션을 구현하는 과제가 있었다.
특정 버튼을 눌렀을 때, 버튼의 text 값이 화면에 표시되도록 해야 했는데, 나는 버튼을 복잡하게 구현한 탓에 어떻게 구현하면 좋을지 감이 잡히질 않았다...

일단은 addTarget을 사용하여 버튼의 액션을 지정해주자고 생각하여 메소드를 만들었다.

@objc private func buttonAction(_ button: UIButton) {
	print(button.titleLabel?.text ?? "")
}

이 메소드는 특정 버튼을 누르면 해당 버튼의 text를 콘솔창에 출력하는 메소드이다.
매개변수로 button을 따로 지정해준 이유는, 버튼의 값을 설정하는 것을 메소드 내부에 구현했기 때문에 메소드에서 버튼의 인스턴스를 사용할 수 없기 때문이다.

문제는 이 다음에 발생했다.

@objc 메소드로 선언했기 때문에 addTarget에서 #selector를 통해 메소드를 읽어야 하는데, 이 방식에서 매개변수를 읽을 수 있을까??

button.addTarget(self, 
				action: #selector(buttonAction(button)), 
				for: .touchDown)

이렇게 하면 될까??

될리가 없다...

#selector는 함수의 이름을 읽고 호출하는 역할이기 때문에 매개변수를 쓸 수 없다... 자세한 내용은 여기에서 확인할 수 있다.

그럼 어떻게 버튼의 인스턴스에 접근하지??

일단 #selector에 메소드 이름만 넣고 실행해서 어떻게 되나 확인해보기로 했다... 결과를 보고 어떤 방향으로 수정하면 좋을지 생각해보기 위함이었다.

button.addTarget(self, 
				action: #selector(buttonAction), 
				for: .touchDown)

메소드를 수정하고 빌드를 하여 버튼을 눌러봤는데...

왜 정상 작동하지...??

크래시나 에러가 발생하지 않고 의도한 대로 잘 작동했다... 아니 오류가 뜨는걸 상정하고 실행 했으니 반대로 의도하지 않은 대로 작동했다고 봐야하나...

어쨌든 어떤 작용으로 정상(?) 작동하는지 궁금해져서 이유를 찾아봤다.

공식 문서에서 addTarget을 찾아보았는데 솔직히 이해하기 어렵다... 그러나 단서는 찾을 수 있었다.

바로 target이다.

나는 버튼에 addTarget 메소드를 호출할 때 targetself로 지정했다. 이 뜻은 addTarget 메소드를 사용할 인스턴스는 self 즉, 이 메소드를 호출한 인스턴스로 지정하겠다는 의미이다.
그리고 #selector를 통해 함수를 불러오면 매개변수를 필요로 하는데 이것을 시스템에서 알아서 현재 인스턴스를 참조하여 값을 넣어주는게 아닐까??

이 말을 검증하기 위해 구글에 addTarget과 메소드의 매개변수의 관계에 대해 검색했고, 답변을 구할 수 있었다.

결론부터 말하자면,
addTarget(_:action:for:) 메서드가 자동으로 호출된 버튼 인스턴스를 매개변수로 전달하는 것이 맞다.

사진으로 보자면 다음과 같다.

우리가 버튼을 클릭하면 addTarget 메소드에 이벤트 발생을 전달하게 되고, #selector가 메소드를 호출하고 이 때 타겟을 self로 했기 때문에 현재 인스턴스를 자동으로 매개변수로 전달하게 된다.
덕분에 매개변수로 별도로 지정해주지 않아도 원하는 결과를 얻을 수 있는 것이다.

그렇다면 매개변수로 UIButton 외에 다른 타입이 오면 어떻게 될까?
확인을 위해 우선 메소드를 수정한다.

@objc private func buttonAction(_ button: UIButton) {
	print(button.titleLabel?.text ?? "")
}

매개변수 타입을 String으로 지정하고 #selector를 통해 메소드를 호출해보면

button.addTarget(self, 
				action: #selector(buttonAction), 
				for: .touchDown)

아직 오류가 발생하지 않는다.
정확한 실험을 위해 빌드를 하고 버튼을 눌러봤다.

앱이 크래시가 발생하며 강제종료되고 오류를 내뱉는다.
어찌보면 당연한 일이다. addTarget은 현재 인스턴스를 매개변수로 전달해주는데, 타입이 전혀 다르기 때문에 에러가 발생하는 것이다.

이번 일을 통해 addTarget으로 특정 메소드를 호출할 때 매개변수를 사용할 수는 있지만, UIButton 타입이 아닐 경우 에러가 발생한다는 것을 알 수 있었다.


2. Delegate를 사용하여 데이터 전달

개념 정리

Delegate란 어떤 객체가 해야 할 일을 다른 객체에게 맡겨서 대신 처리하도록 하는 디자인 패턴이다. 즉, 객체가 할 일을 대신 진행할 객체를 지정하는 방식이다.

1) 데이터를 전달하는 방법

위에서 버튼을 눌렀을 때 특정 액션이 작동하도록 코드를 구현하였지만, 여전히 버튼의 값을 전달하지는 못했다.
버튼을 관리하는 클래스와 계산기 앱에 표시되는 label의 값을 관리하는 클래스가 서로 다르기 때문에 버튼이 label의 값에 접근할 수 없기 때문이었다.

버튼의 메소드로 직접 레이블 값을 수정할 수 없으니 중간매체를 통해 메소드의 값을 전달하고, 전달된 값을 레이블 값으로 사용하는 방법을 사용하기로 했다.

이를 위한 중간매체가 바로 Delegate이다.

먼저 델리게이트 프로토콜을 만들어준다.

protocol ButtonDataDelegate: AnyObject {
    func didTapButton(with text: String)
}

그 뒤 버튼을 관리하는 클래스에 새로운 프로퍼티를 만들고 타입을 델리게이트로 지정하되 옵셔널 값으로 설정한다.

class ButtonData {
	// 강한 순환참조를 방지하기 위해 weak로 선언
	weak var delegate = ButtonDelegate?
    
    // 생략...
}

이제 @objc로 선언한 버튼 클릭시 발생하는 액션을 관리하는 메소드에 델리게이트로 데이터를 전달하도록 설정한다.

@objc private func buttonAction(_ button: UIButton) {
	print(button.titleLabel?.text ?? "")
        
	guard let text = button.titleLabel?.text else { return }
	delegate?.didTapButton(with: text)
}

이렇게 하면 버튼을 눌렀을 때, 버튼 관리 클래스 내에 델리게이트 값이 존재한다면 didTapButton메소드가 발생하고 매개변수로 버튼의 값을 전달하게 된다.

마지막으로 레이블을 관리하는 클래스에 ButtonDataDelegate 프로토콜을 채택하고, didTapButton(with:) 메소드를 구현한다.

class ViewController: UIViewController, ButtonDataDelegate {
	// 생략...

	// answer - 계산기에 표시되는 레이블
	func didTapButton(with text: String) {
    	if self.answer.text == "0" {
        	self.answer = text
        } else {
        	self.answer += text
        }
    }
}

메소드는 구현했지만 이제 이걸 어떻게 쓰면 될까?
답은 간단하다. 레이블을 관리하는 클래스 내에 새로운 프로퍼티를 생성하고 ButtonData 타입으로 설정한다.

class ViewController: UIViewController, ButtonDataDelegate {
	private let buttonData = ButtonData()
    
    // 생략...
}

ButtonData 클래스는 내부 프로퍼티로 ButtonDataDelegate를 타입으로 하는 delegate 프로퍼티를 가지고 있다. 이 프로퍼티는 기본적으로 옵셔널로 되어있기 때문에 초기화를 진행해준다.

// 델리게이트를 self, 현재 인스턴스로 설정
// 현재 인스턴스 -> ViewController -> ButtonDataDelegate 프로토콜을 채택 중
// 초기화에 문제 없음
buttonData.delegate = self

이렇게 하면 버튼을 눌렀을 때, buttonAction 메소드가 실행되고, 이 메소드 내부에서 didTapButton이라는 델리게이트 메소드가 실행된다.
그럼 ViewController내에서 구현한 didTapButton 메소드가 레이블 값을 변경하여 계산기 앱에서 보여지는 값을 변경할 수 있게 되는 것이다.

2) 구현 결과물


-Today's Lesson Review-

오늘은 Delegate에 대해 알아보았는데 무척이나 편한 기능이다...
UIKit을 쓰다보면 다양한 delegate 패턴을 만날텐데,
그 때마다 잘 써먹는다면 깔끔하고 더욱 좋은 코드를 작성할 수 있을 것 같다.
UIButton에 대해 찾아보며 아직도 원리를 모른채 사용하고 있는 코드가 많다고 느꼈다.
앞으로는 최대한 원리를 알아가며 코드를 짤 수 있도록 찾아보고, 공부하는 습관을 길러야겠다고 생각했다.
profile
이유있는 코드를 쓰자!!

2개의 댓글

comment-user-thumbnail
2024년 11월 13일

Delegate 의 편안함 저도 하루 빨리 만나보고 싶네요 내일 진도 쫙쫙 빼면서 만나볼 수 있길 ...

1개의 답글