Delegate Pattern

2 답변 글타래를 보이고 있습니다
  • 글쓴이
    • HyunJi
      참가자
      • 글작성 : 9
      • 답글작성 : 10

      텍스트 필드

      텍스트 필드는 델리게이트 패턴을 사용하는 대표적인 객체 중의 하나이다.

      기본적인 기능은 델리게이트 패턴 없이도 사용할 수 있지만, 입력값을 제어하는 등의 고급 기능을 구현하고 싶을 때에는 델리게이트 패턴을 적용해야 한다.

      텍스트 필드에 델리게이트 패턴을 적용하려면 다음의 두 가지 작업이 필요하다.

      1. 텍스트 필드에 대한 델리게이트 프로토콜 구현
      2. 텍스트 필드의 델리게이트 속성을 뷰 컨트롤러에 연결

      텍스트 필드 델리게이트 패턴 실습을 시작한다.

      스토리보드에 텍스트 필드를 하나 만들고, 뷰 컨트롤러에 tf라는 IBOutlet으로 연결해주었다.

      import UIKit
      
      class ViewController: UIViewController {
      
      @IBOutlet var tf: UITextField!
      
      override func viewDidLoad() {
      // 텍스트 필드 속성 설정
      self.tf.placeholder = "값을 입력하세요."
      self.tf.keyboardType = UIKeyboardType.alphabet // 키보드 타입 영문자 패드로
      self.tf.keyboardAppearance = UIKeyboardAppearance.dark // 키보드 스타일 어둡게
      self.tf.returnKeyType = UIReturnKeyType.join
      self.tf.enablesReturnKeyAutomatically = true // 리턴키 자동 활성화 On
      
      super.viewDidLoad()
      
      }
      
      }
      
      • 텍스트 필드에 값이 비어 있을 때, “값을 입력하세요”라는 안내 메시지를 표시
      • 키보드 타입을 영문자 패드 형태로 지정
      • 키보드 스타일은 어둡게 설정
      • 키보드의 리턴키 타입을 Join으로 설정
      • 텍스트 필드에 값이 비어 있을 때 키보드의 리턴 키를 비활성화
      // 텍스트 필드 스타일 설정
      self.tf.borderStyle = UITextField.BorderStyle.line
      self.tf.backgroundColor = UIColor(white: 0.87, alpha: 1.0)
      self.tf.contentVerticalAlignment = .center // 수직 방향으로 텍스트가 가운데 정렬되도록
      self.tf.contentHorizontalAlignment = .center // 수평 방향으로 텍스트가 가운데 정렬되도록
      self.tf.layer.borderColor = UIColor.darkGray.cgColor // 테두리 색상을 회색으로
      self.tf.layer.borderWidth = 2.0
      

      텍스트 필드에 스타일을 설정해주었다.

      // 텍스트 필드를 최초 응답자로 지정
      self.tf.becomeFirstResponder()
      

      최초 응답자란 UIWindow에서 이벤트가 발생했을 때 우선적으로 응답할 객체를 가리킨다.

      위의 코드에서 텍스트 필드가 최초 응답자로 지정했으므로, 시뮬레이터가 로딩이 된 후에 바로 텍스트 필드 입력 대기 상태가 된다.

      원래대로라면 이렇게 입력 대기 상태에서 키보드를 제거하려면, 값을 입력하고 리턴 키를 눌러서 입력을 완료하거나 화면 상의 다른 컨트롤을 클릭하여 최초 응답자 포인터를 다른 곳으로 옮겨야 한다.

      하지만 최초 응답자 메소드를 사용하면 손쉽게 키보드를 없앨 수 있다.

      @IBAction func confirm(_ sender: Any) {
      // 텍스트 필드를 최초 응답자 객체에서 삭제
      self.tf.resignFirstResponder()
      }
      

      이번에는 텍스트 필드를 직접 터치하지 않고도 버튼을 클릭하면 텍스트 필드가 입력 대기 상태가 되도록 한다.

      @IBAction func input(_ sender: Any) {
      // 텍스트 필드를 최초 응답자 객체로 지정
      self.tf.becomeFirstResponder()
      }
      

      텍스트 필드에 델리게이트 패턴 적용

      • 텍스트 필드 프로토콜에 대한 구현
      class ViewController: UIViewController, UITextFieldDelegate
      
      self.tf.delegate = self
      

      텍스트 필드에서 정해진 특정 이벤트가 발생하면 현재의 뷰 컨트롤러에게 알려달라는 요청이다.

      이를 뷰 컨트롤러가 텍스트 필드의 델리게이트 객체로 지정되었다고 표현한다.

      func textFieldDidBeginEditing(_ textField: UITextField) {
      print("텍스트 필드의 편집이 시작되었습니다.")
      }
      
      func textFieldShouldClear(_ textField: UITextField) -> Bool {
      print("텍스트 필드의 내용이 삭제됩니다.")
      return true // false를 리턴하면 삭제되지 않는다.
      }
      
      func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
      print("텍스트 필드의 내용이 \(string)으로 변경됩니다.")
      return true // false를 리턴하면 내용이 변경되지 않는다.
      }
      
      // 텍스트 필드의 리턴키가 눌려졌을 때 호출
      func textFieldShouldReturn(_ textField: UITextField) -> Bool {
      print("텍스트 필드의 리턴키가 눌려졌습니다.")
      return true
      }
      
      // 텍스트 필드의 편집이 종료될 때 호출
      func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
      print("텍스트 필드의 편집이 종료됩니다.")
      return true // false를 리턴하면 편집이 종료되지 않는다.
      }
      
      func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
      print("텍스트 필드의 편집이 종료되었습니다.")
      }
      

      각각의 델리게이트 메소드는 실행 시점에 맞추어 호출된다.

      func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
      print("텍스트 필드의 내용이 \(string)으로 변경됩니다.")
      if Int(string) == nil { // 입력된 값이 숫자가 아니라면 true를 리턴
      // 현재 텍스트 필드에 입력된 길이와 더해질 문자열 길이의 합이 10을 넘는다면 반영하지 않음
      if (textField.text?.count)! + string.count > 10 {
      return true
      } else {
      return false
      }
      } else { // 입력된 값이 숫자라면 false를 리턴
      return false
      }
      }
      

      추가적으로 위와 같은 코드를 델리게이트 메소드 안에 작성하면 텍스트 필드의 길이가 10까지만 입력되고, 숫자는 입력되지 않는다.

      이미지 피커 컨트롤러

      이미지 피커 컨트롤러는 카메라나 앨범 등을 통해 이미지를 선택할 때 사용하는 컨트롤이며, 델리게이트 패턴을 활용하는 또 다른 대표적인 객체이다.

      @IBOutlet var imgView: UIImageView!
      
      override func viewDidLoad() {
      super.viewDidLoad()
      // Do any additional setup after loading the view.
      }
      
      @IBAction func pick(_ sender: Any) {
      // 이미지 피커 컨트롤러 인스턴스 생성
      let picker = UIImagePickerController()
      picker.sourceType = .photoLibrary // 이미지 소스로 사진 라이브러리 선택
      picker.allowsEditing = true // 이미지 편집 기능 On
      
      // 이미지 피커 컨트롤러 실행
      self.present(picker, animated: true)
      
      }
      

      이렇게 구현한 후, info.plist에서 아래와 같은 설정을 해준다.

      위와 같이 구현한 경우, 델리게이트 메소드를 아직 구현하지 않았기 때문에 선택한 이미지가 이미지 뷰에 나타나지 않는다.

      선택한 이미지를 전달받아 화면에 표시할 수 있도록 델리게이트 메소드를 구현한다.

      class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate
      
      @IBAction func pick(_ sender: Any) {
      // 이미지 피커 컨트롤러 인스턴스 생성
      let picker = UIImagePickerController()
      picker.sourceType = .photoLibrary // 이미지 소스로 사진 라이브러리 선택
      picker.allowsEditing = true // 이미지 편집 기능 On
      
      // 델리게이트 지정 (추가)
      picker.delegate = self
      
      // 이미지 피커 컨트롤러 실행
      self.present(picker, animated: true)
      
      }
      

      델리게이트 처리 코드를 추가한다.

      // 이미지 피커에서 이미지를 선택하지 않고 취소했을 때 호출되는 메소드
      func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false)
      
      // 알림창 호출
      let alert = UIAlertController(title: "", message: "이미지 선택이 취소되었습니다.", preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "확인", style: .cancel))
      self.present(alert, animated: false)
      }
      

      이미지 피커 델리게이트 메소드에서 가장 먼저 처리해야 할 일은 현재의 이미지 피커 컨트롤러 창을 닫아주는 것이다.
      델리게이트 메소드를 구현하지 않았을 때는 이미지 피커를 취소하거나 이미지를 선택하면 자동으로 해당 컨트롤러 창이 닫히지만, 일단 델리게이트 메소드를 구현하게 되면 컨트롤러 창을 닫기 위한 dismiss(animated:) 메소드를 직접 호출해 주어야 이미지 피커 창이 닫히고 원래의 뷰 컨트롤러로 돌아올 수 있다.

      // 이미지 피커에서 이미지를 선택했을 때 호출되는 메소드
      func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false) { () in
      // 이미지를 이미지 뷰에 표시
      let img = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
      self.imgView.image = img
      }
      }
      

      이 메소드는 이미지를 올바르게 선택했을 때 호출된다.

      이미지를 올바르게 선택했을 때 이미지 피커 컨트롤러 창을 닫고 나서 원하는 로직을 차례로 구현하면 된다.

      이 메소드에서 처리할 핵심 내용은 사용자가 선택한 이미지를 화면에 뿌려주는 것이다.

      딕셔너리 타입으로 정의된 매개변수 info에는 사용자가 선택한 이미지 정보가 담겨서 전달되기 때문에 이미지 관련 키를 사용하여 원하는 이미지 정보를 추출할 수 있다.

      Extension을 이용한 델리게이트 패턴 코딩

      델리게이트 패턴은 최소한 하나 이상의 프로토콜을 구현해야 하기 때문에, 한 화면에서 다양한 객체의 델리게이트 패턴을 구현하다 보면 코드가 복잡해지기 쉽다.

      이때 익스텐션을 활용하여 훨씬 깔끔하게 코딩할 수 있는 방법이 있다.

      익스텐션은 클래스를 대신해서 프로토콜을 구현할 수 있기 때문에, 델리게이트 패턴에 사용되는 프로토콜을 익스텐션에서 구현하면 하나의 뷰 컨트롤러 클래스에 여러 프로토콜 메소드가 있는 것을 방지할 수 있다.

      extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
      }
      

      현재의 코드에서 익스텐션을 정의하면서 UIImagePickerControllerDelegate와 UINavigationControllerDelegate 프로토콜을 해당 익스텐션으로 옮긴다.

      extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
      // 이미지 피커에서 이미지를 선택하지 않고 취소했을 때 호출되는 메소드
      func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false)
      
      // 알림창 호출
      let alert = UIAlertController(title: "", message: "이미지 선택이 취소되었습니다.", preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "확인", style: .cancel))
      
      self.present(alert, animated: false)
      // self.dismiss(animated: false) { () in
      // // 알림창 호출
      // let alert = UIAlertController(title: "", message: "이미지 선택이 취소되었습니다.", preferredStyle: .alert)
      // alert.addAction(UIAlertAction(title: "확인", style: .cancel))
      //
      // self.present(alert, animated: false)
      // }
      }
      
      // 이미지 피커에서 이미지를 선택했을 때 호출되는 메소드
      func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false) { () in
      // 이미지를 이미지 뷰에 표시
      let img = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
      self.imgView.image = img
      }
      }
      
      }
      
      

      그리고 구현했던 델리게이트 메소드를 작성해주면 끝이다.

      그런데 모든 프로토콜을 반드시 하나의 익스텐션에 담을 필요는 없다.

      // MARK:- 이미지 피커 컨트롤러 델리게이트 메소드
      extension ViewController: UIImagePickerControllerDelegate {
      // 이미지 피커에서 이미지를 선택하지 않고 취소했을 때 호출되는 메소드
      func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false)
      
      // 알림창 호출
      let alert = UIAlertController(title: "", message: "이미지 선택이 취소되었습니다.", preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "확인", style: .cancel))
      
      self.present(alert, animated: false)
      // self.dismiss(animated: false) { () in
      // // 알림창 호출
      // let alert = UIAlertController(title: "", message: "이미지 선택이 취소되었습니다.", preferredStyle: .alert)
      // alert.addAction(UIAlertAction(title: "확인", style: .cancel))
      //
      // self.present(alert, animated: false)
      // }
      }
      
      // 이미지 피커에서 이미지를 선택했을 때 호출되는 메소드
      func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
      // 이미지 피커 컨트롤러 창 닫기
      picker.dismiss(animated: false) { () in
      // 이미지를 이미지 뷰에 표시
      let img = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
      self.imgView.image = img
      }
      }
      
      }
      
      // MARK:- 내비게이션 컨트롤러 델리게이트 메소드
      extension ViewController: UINavigationControllerDelegate {
      
      }
      

      프로토콜을 따로 구현해주고, 위와 같이 주석을 작성한다면 메소드 목록이 보기 좋게 정돈된다.

      • 이 게시글은 HyunJi에 의해 4 years 전에 수정됐습니다.
      • 이 게시글은 HyunJi에 의해 4 years 전에 수정됐습니다.
    • 야곰
      키 마스터
      • 글작성 : 37
      • 답글작성 : 580

      보이지 않는 이미지가 많네요 +_+

      1. 물론 예제기 때문에 그렇긴 하지만, 예제에서도 이름짓기에 신경쓰는 것이 좋습니다. 습관이 되거든요. 가령 tf 등의 이름보다는 정 이름짓기 어렵다면 textField라도 써주는 것이 좋겠습니다. 별거 아니라고 생각하지만 습관이란게 정말 무섭습니다.
      2. 아무리 예제라지만 필요없는 코드는 주석으로 남겨두는 것은 좋지 않은 습관이라고 생각합니다. 만약 예제의 설명을 위해 꼭 필요한 코드라면 조금 더 정리할 방법을 생각해보는 것은 어떨까요? 스스로에게도 나중에 큰 도움이 될겁니다.
      • HyunJi
        참가자
        • 글작성 : 9
        • 답글작성 : 10

        이미지 수정 완료했습니다!

        좋은 조언 감사합니다 🙂

    • 멍단비
      참가자
      • 글작성 : 10
      • 답글작성 : 98

      주석도 많고 글도 많고 이미지도 엄청 많네요..ㅎㅎ 공부 정말 열심하 하시는 것 같아요. 자극이 됩니다.

      아 그런데 질문이 있어요.

      제가 알고 있기로는 보통 becomeFirstResponder를 써줄때 viewwillappear에서 세팅해주고
      첫번째 응답자 삭제는 viewwilldisapper에서 resignFirstResponder을 세팅해주는 것으로 알고 있었어요.

      그런데 위에 글에서 보면, 응답자 해제를 confirm버튼에서 해주시는 것 같은데
      저는 보통 글 다쓰거나 해서 내려주는 상황이면 view.endediting이나 tableview의 shouldReturn delegate 메소드를 썼던 것 같아요.

      endEditing하고 resignFirstResponder의 차이를 알 수 있을까요 ?
      제가 그동안 잘못써왔던건지 혹은 뭐 어떤 특정 상황에 따라 다르게 사용해야 한다던지요.

      @hyunji-kim


      @IBAction func confirm(_ sender: Any) { // 텍스트 필드를 최초 응답자 객체에서 삭제 self.tf.resignFirstResponder() }
      • 야곰
        키 마스터
        • 글작성 : 37
        • 답글작성 : 580

        정말 궁금한 질문일까요 아니면 좀 더 많은걸 알려주고 싶어서 질문하셨을까요 ㅎㅎ

        endEditing(_ force: Bool)resignFirstResponder() 차이점은?
        공식문서에서 보면 둘 다 text field 의 first responder 상태의 사임 요청을 하는 것은 비슷해 보입니다.
        endEditing(_:)UIView의 메서드이고, resignFirstResponder()UIResponder의 메서드입니다.
        endEditing(_:) 메소드는 현재 뷰와 그 서브 뷰를 찾아 first responder 된 text filed 의 사임을 요청합니다. force에 전달한 값이 true면 요청이 아닌 강제로 사임 시킵니다.

        resignFirstResponder() 메소드는 어떤 text field 를 사임시킬 지 정확히 알고 있을 때 endEditing(_:) 보다 약간 더 효율적으로 동작한다고 합니다. endEditing(_:)은 뷰의 계층을 따라 내려가면서 현재 first responder 를 찾아서 그것을 사임 요청 하기 때문에 그렇게 찾아가지 않는 resignFirstResponder() 가 어떤 first responder 를 사임시킬 지 정확히 안다는 가정 하에 약간 더 효율적이라고 하는 것 같습니다. 대신 endEditing(_:) 은 그것이 어디에 있든 현재 first responder 된 text field 를 없애고 싶을 때 사용할 수 있습니다.

        • 멍단비
          참가자
          • 글작성 : 10
          • 답글작성 : 98

          저같은 초보자가 뭘 알겠습니까,, 또 하나 배웠네요. 감사합니다😄

        • HyunJi
          참가자
          • 글작성 : 9
          • 답글작성 : 10

          야곰님 좋은 답변 감사합니다! 늦게 확인했더니 이미 답글이 달려있었네요 ㅎㅎ..
          저는 텍스트필드 하나로만 실습했기 때문에 resignFirstResponder() 사용했습니다!
          보다 유동적인 방식으로 first responder 텍스트 필드를 없애고 싶을 때는 endEditing(_:)이 좋을 것 같네요 🙂

2 답변 글타래를 보이고 있습니다
  • 답변은 로그인 후 가능합니다.

logo landscape small

사업자번호 : 743-81-02195
통신판매업 신고번호 : 제 2022-충북청주-1278 호
고객센터 : 카카오톡채널 @yagom