본문 바로가기

iOS/Deep dive!!!

[iOS] UITextField 입력시 자동으로 키보드 띄우기, 키보드 위에 버튼 올리기 +_+ | InputAccessoryView

 

안녕하세요.

 

지난번 포스트와 같은 키워드인 이번 포스트는 UITextField를 터치할 때, 키보드 올라오도록 하는 방법과 이때 키보드의 실시간 위치를 파악하지 않고도 키보드 위에 버튼을 쉽게 올릴 수 있는 InputAccessoryView. 지정된 텍스트 필드 이외의 영역을 터치할 때 텍스트 필드 이외의 영역이라면 키보드를 내려가게 하는 (resignFirstResponder() ) 방법에 소개하려고 합니다.

 

 

1. InputAccessoryView in UITextField

 

텍스트 필드가 first responder가 될 경우 시스템 키보드가 자동으로 올라옵니다. (예전에 새로 알게 된 개념2 주제로 정리했었는데. 까먹었다는게 사실..ㅠ) 간단 요약하자면, input view는 사용자가 UITextField 또는 UITextView 컴포넌트를 tap할 때, 그 뷰 "becoms the first responder and display its input view.". 즉 텍스트필드, 텍스트 뷰는 first responder로 지정됩니다. 만약 UITextField에서 이벤트 처리를 하지 못한다면? root view에서는 responder chain으로 rootView인 UIWindow에게 event 전달 전에 UIView와 연결된 UIViewController에게 event를 전달합니다. 

 

즉 터치, 입력 event 발생 시 여러 response chain 중 이벤트를 가장 먼저 받는자가 first responder가 되는 것입니다.  그리고 first responder로 텍스트 필드가 지정되면 (커스텀으로 구현 안한 경우)시스템 키보드가 자동으로 올라옵니다.

 

 

텍스트 필드는 inputAccessoryView를 옵셔널로 갖고 있습니다.

 

https://developer.apple.com/documentation/uikit/uitextview/1618596-inputaccessoryview/

 

그리고 text view ( UITextView)가 first responder가 된다면 input accessory view가 화면에 보여집니다. 그런데 신기한 것은 시스템 키보드 바로 위에 함께 올라옵니다. 

 

2. init InputAccessoryView in UITextField

텍스트 필드를 지정했다면, inputAccessoryView를 초기화 할 컨테이너 뷰가 필요합니다. 

 

기본적인 텍스트 필드 placeholder등 지정

텍스트 필드 초기화를 했다면 이제 Input accessory view에 컨테이너 뷰 인스턴스를 할당해야 합니다.

 

func initInputAccessoryView() {
    // constant
    let height: CGFloat = 75.0
    let origin: CGPoint = .init(x: 0, y: 0)
    let size: CGSize = .init(
        width: UIScreen.main.bounds.width,
        height: height)
    
    let containerView = UIView(
        frame: CGRect(origin: origin, size: size))
    containerView.backgroundColor = .red
        
    textField.inputAccessoryView = containerView
}

 

크기와 높이를 지정한 containerView를 만들고 textField의 인스턴스 할당을 했다면,

 

 

이렇게 텍스트필드를 탭했을 때 시스템 키보드 위에 input accessory view도 딸려옵니다. 이때 신기한 것은 originr값은 뭘 넣든 고정되어있습니다. size 만 적용되서 시스템 키보드 위에 정확히 붙어서 올라옵니다. 이제 이 컨테이너 뷰 내부에 subview를 추가할 때는 spacing등을 넣어 input accessoryView안에 원하는 위치에 Auto layout하면 됩니다.

 

저는 그래서 버튼 객체를 커스텀해서 버튼 클래스 내부에 layout을 조정하는 함수를 짰습니다. leading, spacing과 centerY, heightAnchor를 지정해줬습니다.

 

// 버튼 안 함수
func setInputAccessoryViewLayout(
    from accessoryView: UIView?,
    spacing: UISpacing = .init()
) {
    guard let accessoryView = accessoryView else { return }
    accessoryView.addSubview(self)
    self.accessoryView = accessoryView
    self.spacing = spacing
    NSLayoutConstraint.activate([
        leadingAnchor.constraint(
            equalTo: accessoryView.leadingAnchor,
            constant: spacing.leading),
        trailingAnchor.constraint(
            equalTo: accessoryView.trailingAnchor,
            constant: -spacing.trailing),
        heightAnchor.constraint(equalToConstant: height),
        centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor)])
}

 

다시 텍스트 필드가 있는 뷰로 돌아와서

 

var accessoryView: UIView? {
    textField.inputAccessoryView
}
  
func setInputAccessoryViewWithButton() {
    searchButton.setInputAccessoryViewLayout(
        from: accessoryView,
        spacing: .init(leading: 24, trailing: 24))
}

 

이렇게 함수를 써서 컨테이너 뷰 내부에 버튼의 오토 레이아웃을 잡고 view did load시점에 두 함수들을 호출해주면 됩니다. 그런데 키보드에 return을 클릭해도 텍스트 필드가 안 내려갈 수 있습니다. 그럴 땐 UITextFieldDelegate에서 추가 설정을 해야 합니다.

 

// MARK: - UITextFieldDelegate
extension GroupChatSearch: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
  }
  
  func textFieldDidEndEditing(_ textField: UITextField) {
    textField.resignFirstResponder()
  }
}

 

만약 textField 이외의 화면을 터치할 때 내려가지 않는다면? 역시 textField의 firstResponder를 resign하지 않았기 때문입니다.

 

이 경우 viewController에서 touchesBegan(_:with:) 메서드를 오버라이딩해서 터치했을 당시 텍스트 필드 이외인 경우 textfield의 firstResponder를 resign하면 됩니다.

 

class ViewController {
 	...
 	override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    	super.touchesBegan(touches, with: event)
    	guard let touch = touches.first else { return }
    	let touchLocation = touch.location(in: self.view)
    	if !naviBottomView.searchBarView.frame.contains(touchLocation) {
        	// textfield.resignResponder() 호출 메서드
      		naviBottomView.hideKeyboard()
        }
    }
}

 

 

관련 코드는 아래 에서 볼 수 있습니다 :)

https://github.com/SHcommit/NetworkSocketProgrammingWithSwiftBsd/issues/1

 

 

References:

https://ios-development.tistory.com/689

https://developer.apple.com/documentation/uikit/uitextview/1618596-inputaccessoryview/