[iOS Programming Big Nerd Ranch] 2. 텍스트, 글 입력과 Delegation(위임)


텍스트를 입력하는 것과 Delegation에 대해 알아보자


UITextField를 추가해서 텍스트를 입력할 수 있게 해보자.

라이브러리에서 Text Field를 추가하고 constraint를 추가했다.

image

이제 실행을 해서 텍스트 필드를 클릭하고 텍스트를 입력한다. 텍스트 필드를 입력하는데 키보드가 뜨지 않는다면 Command-K를 누르거나 I/O 메뉴에서 키보드 옵션을 관리한다.

image

나 같은 경우는 시뮬레이션 하기에는 컴퓨터 노트북으로 타자를 치는게 편하므로 시뮬레이터 키보드가 뜨지 않도록 설정해두었다. 하지만 일단 이 키보드 옵션을 다뤄야 해서 다시 켰다.

Keyboard attributes

텍스트 필드가 클릭되면, 키보드가 스크린 아래에서 위로 슬라이드하며 올라온다. 이 키보드가 화면에 뜨는 것은 UITextFieldUITextInputTraits 프로퍼티에 의해 결정된다. 이 프로퍼티 중 하나는 키보드의 타입에 대한 것이다. 만약에 숫자 패드를 열고 싶다면, inspector의 Keyboard Types라는 부분에서 “Decimal Pad”를 선택해준다.

image

이제 숫자 키보드가 잘 나온다.

image

텍스트 필드가 변하는 것에 반응하기

이제 이 텍스트 라벨의 텍스트가 바뀔때마다 label의 텍스트를 업데이트 할 것이다.

이걸 하려면 인터페이스와 연관된 뷰 컨트롤러 subclass에 코드를 작성해야 하는데, 이거는 ViewController.swift에 정의된 ViewController 클래스다. 하지만 설명의 기능이 있는 타입 이름을 작성하면 프로젝트를 확장하기 더 좋다. 그래서 원래 있던 ViewController.swift를 삭제하고, ConversionViewController.swift라는 파일을 생성하고, 아래와 같이 코드를 작성했다.

import UIKit

class ConversionViewController: UIViewController {
    
}

그리고 이제 만든 뷰 컨트롤러를 Main.storyboard의 인터페이스로 연결해야 한다. Main.sotryboard에서 View Controller 를 선택해서 inspector 창의 identity inspector에서 Custom Class 의 class를 ConversionViewController로 지정한다.

image

이제 ConversionViewController.swift에 outlet과 action을 생성한다.

class ConversionViewController: UIViewController {
    @IBOutlet var celsiusLabel: UILabel!
    
    @IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
        celsiusLabel.text = textField.text
    }
}

outlet은 앞에서 했던 것처럼 설정해준다. action같은 경우는 action이 텍스트의 변경이 일어날 때마다 작동해야 하기 때문에 앞에서 했던 거랑은 다르게 동작해야 한다. 텍스트 필드를 클릭하고 connections inspector를 열고 앞에서 target으로 드래그 드롭했던 것처럼 “Editing Changed” 항목을 View Controller 쪽으로 드래그 드랍한다. 그리고 앞서 작성했던 action 함수를 선택한다.

image

이제 시뮬레이터를 실행시키면 텍스트를 입력한 대로 라벨의 텍스트가 바뀌게 된다.

Dismissing the keyboard

아직도 문제가 있는데, 바로 키보드를 해제할 방법이 없다는 것이다. 이걸 하는 흔한 방법은 유저가 Return key를 눌렀을 때를 감지해서 키보드를 닫는 것이다. 이거는 나중에 보도록 하고, Decimal pad는 리턴 키가 없기 때문에 사용자가 바깥 뷰를 터치했을 때 키보드를 해제할 수 있게 하겠다.

텍스트 필더가 선택되면 ,becomeFirstResponder()라는 메서드가 호출된다. 이 메서드는 키보드가 뜨도록 한다. 키보드를 해제하려면, 텍스트 필드의 resignFirstResponder()라는 메서드를 호출한다. 이것에 대한 자세한 것도 나중에 볼 것이다.

먼저 텍스트 필드를 가리킬 outlet을 선언하고 키보드를 해제하는 action을 ConversionViewController 안에 선언한다.

@IBOutlet var textField: UITextField!

@IBAction func dismissKeyboard(_ sender: UITapGestureRecognizer) {
        textField.resignFirstResponder()
    }

그리고 스토리 보드에서 textField outlet을 텍스트 필드에 연결한다. 이제 메서드를 연결할 건데, gesture recognizer를 이용할 것이다. Gesture Recognizer는 터치를 감지해서 그 시퀀스가 감지된 때 타겟의 action을 호출하는 UIGestureRecognizer의 subclass다. 탭, 스와이핑, 길게 누르기 등등을 감지하는 gesture recognizer들이 있는데 여기서는 터치를 감지해야 하므로 UITapGestrueRecognizer를 볼 것이다.

Main.storyboard에서, 객체 라이브러리에서 Tap Gestrue Recognizer를 찾아 view controller의 바탕 view로 드래그한다.

image

그리고 gesture recognizer를 view controller로 컨트롤을 누른 상태에서 드래그-드롭 해서 아까 만든 action을 선택한다.

image

이제 다시 시뮬레이터로 실행시켰을 때, 텍스트 필드를 누르면 키보드가 뜨고, 바깥 화면을 터치하면 키보드가 해제돼서 화면에서 사라진다.

Implementing the Temperatrue Conversion

이제 화씨 온도를 섭씨 온도로 바꾸는 작업을 구현해보자.

var fahrenheitValue: Measurement<UnitTemperature>?
    
var celsiusValue: Measurement<UnitTemperature>? {
    if let fahrenheitValue = fahrenheitValue {
        return fahrenheitValue.converted(to: .celsius)
    } else {
        return nil
    }
}

ConversionViewControllerfahrenheitValue와 연산 프로퍼티인 celsiusValue를 추가했다. 여기서 텍스트 필드의 값(화씨 온도)가 변한하면, 섭씨 온도의 라벨이 바뀌어야 한다. 섭씨 온도의 값을 섭씨 온도 텍스트로 넣어주는 함수를 작성한다.

func updateCelsiusLabel() {
    if let celsiusValue = celsiusValue {
      celsiusLabel.text = "\(celsiusValue.value)"
    } else {
      celsiusLabel.text = "???"
    }
}

그리고 이제 이 함수가 화씨 온도의 값이(텍스트가) 변할때마다 바뀌게 하고 싶다. 이를 구현하기 위해 바로 사용하는 것이 프로퍼티 감시자다. 프로퍼티 감시자는 프로퍼티의 값이 변할 때마다 호출되기 때문에 유용하다.

var fahrenheitValue: Measurement<UnitTemperature>? {
    didSet {
        updateCelsiusLabel()
    }
}

그리고 이를 action 함수에서 텍스트의 값이 수정될 때마다 fahrenheitValue의 값을 설정하게 해서 didSet이 매번 호출될 수 있게 작성한다.

@IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
    if let text = textField.text, let value = Double(text) {
        fahrenheitValue = Measurement(value: value, unit: .fahrenheit)
    } else {
        fahrenheitValue = nil
    }
}

Number formatters

이제 숫자를 잘 출력하는데 숫자의 소숫점 자리때문에 너무 길게 출력된다. 이를 수정하기 위해 number formatter를 사용한다. 숫자 말고도 날짜, 에너지, 질량, 길이 등등을 커스터마이징 할 수 있는 다른 formatter도 많다.

아래와 같이 number formatter를 생성한다.

let numberFormatter: NumberFormatter = {
    let nf = NumberFormatter()
    nf.numberStyle = .decimal
    nf.minimumFractionDigits = 0
    nf.maximumFractionDigits = 1
    return nf
}()

이제 최대 소숫점 자리를 한자리까지 표시하는 formatter를 만들었다. 그리고 이 formatter의 값을 라벨의 텍스트에 반영한다.

func updateCelsiusLabel() {
    if let celsiusValue = celsiusValue {
        celsiusLabel.text = numberFormatter.string(from: NSNumber(value: celsiusValue.value)) //여길 바꿈
    } else {
        celsiusLabel.text = "???"
    }
}

image

이제 소숫점 한자리까지 출력할 수 있는 것을 확인할 수 있다.

Delegation

Delegation은 콜백에 객체 지향적으로 접근하는 것이다. 콜백이란 이벤트에 앞서서 미리 제공되는 함수이며 이벤트가 발생할떄마다 호출된다. 어떤 객체는 하나 이상의 이벤트에 대해 콜백을 만들어야 한다. 예를 들어, 위의 텍스트 필드는 사용자가 Return key를 누를 때와 같이 사용자가 텍스트를 입력하면 “콜백” 해야 한다.

하지만, 두 개 이사의 콜백함수가 같이 함께 잘 동작하며 정보를 공유하는 방법이 구현되어 있지는 않다. 이게 delegation에 의해 생기는 문제점이다. 특정 객체에 대한 이벤트와 관련된 콜백들을 모두 받는 하나의 delegate를 제공한다. 이 delegate 객체는 그러면 이를 저장하고, 다루고, 콜백으로부터 정보를 전달할 것이다.

사용자가 텍스트 필드에 타이핑을 할때, 텍스트 필드는 사용자가 만드는 변화를 받아들일 것인지 delegate에 물어본다. 위에서 만든 앱은 사용자가 두번째 decimal separator를 입력하려고 할 때 이를 무시해야 한다. 텍스트 필드의 delegate는 위에서 만든 ConversionViewController의 인스턴스가 될 것이다.

Conforming to a protocol

첫 단계는 ConversionViewController의 인스턴스가 ConversionViewControllerUITextFieldDelegate 프로토콜을 따른다고 선언해서 UITextField delegate 의 역할을 수행하게 하는 것이다. 모든 delegate 역할은 객체가 delgate에 호출할 수 있는 메서드를 선언한 각각의 프로토콜이 있다.

UITextFieldDelegate 프로토콜은 아래와 같다.

  protocol UITextFieldDelegate: NSObjectProtocol {
  optional func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool optional func textFieldDidBeginEditing(_ textField: UITextField)
  optional func textFieldShouldEndEditing(_ textField: UITextField) -> Bool optional func textFieldDidEndEditing(_ textField: UITextField)
  optional func textField(_ textField: UITextField,
  shouldChangeCharactersIn range: NSRange,
  replacementString string: String) -> Bool optional func textFieldShouldClear(_ textField: UITextField) -> Bool
  optional func textFieldShouldReturn(_ textField: UITextField) -> Bool }

이 프로토콜은 모든 프로토콜과 같이 protocolUITextFieldDelegate라는 이름 뒤에 붙여서 정의했다. 이 NSObjectProtocolNSObject 프로토콜임을 말하는 것이고 UITextFieldDelegateNSObject 프로토콜에 있는 모든 메서드를 상속한다는 것을 알려준다. UITextFieldDelegate 에 명시된 메서드는 다음에 선언된다.

프로토콜은 단순히 메서드와 프로퍼티의 리스트이기 때문에 프로토콜의 인스턴스를 생성할 수는 없다. 대신, 구현하는 것은 이 프로토콜을 따르는 타입에게 맡겨지는 것이다.

클래스의 정의에서, 프로토콜은 superclass뒤에 컴마를 붙이고 뒤에 이어써서 클래스가 프로토콜을 따른다는 것을 명시한다. ConversionViewControllerUITextFieldDelegate 프로토콜을 따른다고 정의하자.

class ConversionViewController: UIViewController, UITextFieldDelegate {

delegation에 사용되는 프로토콜들은 delgate protocol이라고 불리고, delegating 하는 클래스에 Delegate라는 단어를 붙여서 이름을 짓는다. 모든 프로토콜이 delgate protocol은 아니다. 위에서 언급한 프로토콜은 iOS SDK의 일부이므로 자신만의 프로토콜을 가질 수도 있다.

Using a delegate

ConversionViewUITextFieldDelegate를 따른다고 선언했으니 텍스트 필드의 delegate프로퍼티를 설정할 수 있다.

Main.storyboard를 열어서 컨트롤을 누른 상태에서 드래그 해서 Converion View Controller로 드랍한다. 그리고 delegate라는 프로퍼티를 선택한다.

다음은 이제 UITextFieldDelegate의 메서드인 textField(_:shouldChnageCharactersIn:replcaementString:)을 구현한다. 텍스트 필드가 이 메서드를 delgate에서 호출하기 때문에, ConversionViewController.swift 에 구현해야 한다. 아래와 같이 구현해준다.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
    if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
        return false
    } else {
        return true
    }
}

이제 실행시키면 두 번째 decimal separator를 입력했을 때 이게 입력이 되지 않을 것이다.