swiftUI 키보드 커스텀

나우리·2024년 7월 13일

Swift

목록 보기
6/13

SwiftUI에서 텍스트에디터를 사용하면서 두 가지 면에서 키보드를 커스텀할 필요가 있었다.

  • 하나, 텍스트에디터가 화면 하단에 위치할 때 키보드가 이를 덮어버려 실시간으로 텍스트를 확인할 수 없었다.
  • 둘, 텍스트에디터의 경우 Done 버튼이 별도로 존재하지 않아 키보드를 내릴려면 키보드 위쪽 화면을 잡고 드레그해야했다. 이 방식이 직관적이지 못해 내리는 법을 알지 못할 수도 있겠다는 생각이 들었다.

키보드가 텍스트에디터 영역을 가리는 경우

이 경우에는 키보드 높이를 파악하여 이에 맞춰 화면 위치를 조정할 필요가 있었다.
먼저 키보드 높이값을 파악하기 위한 struct를 만들었다.

import Foundation
import SwiftUI

struct KeyboardProvider : ViewModifier {
    
    //키보드 높이값
    var keyboardHeight: Binding<CGFloat>
    
    func body(content: Content) -> some View {
        content
        //키보드 올라가기 직전 노티를 받으면 나오는 객체
            .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
                       perform: { notification in
                guard let userInfo = notification.userInfo,
                      let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
                
                //키보드 높이값 . 바인딩 원본 객체 연결 -> 전달
                self.keyboardHeight.wrappedValue = keyboardRect.height
                
            })
        //키보드 닫기 전 보내는 노티 받으면 실행
            .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
                         perform: { _ in
                //키보드 높이값 0으로 변경
                self.keyboardHeight.wrappedValue = 0
            })
    }
}

public extension View {
    func keyboardHeight(_ state: Binding<CGFloat>) -> some View {
        self.modifier(KeyboardProvider(keyboardHeight: state))
    }
}

여기서 ViewModifier란 View를 변형해 적용할 수 있는 modifier 형태의 프로토콜을 만드는 형식인데 KeyboardProvider라는 struct는 바디값으로 content를 받고, content에서 키보드가 올라간다는 알람을 받으면 Rect, 사각형 형태로 가져온 정보에서 높이값을 가져와 그 키보드 높이값을 받아온다.

struct의 바인딩 객체에 저장된 값을 뷰로 전달하기 위해 View에 키보드 높이 값을 가져오는 함수를 추가한다.

이를 뷰에 적용하기 위해서는 다음과 같이 작성해야 한다.

struct ContentView : View {
@State var keyboardHeight : CGFloat = 0
@State var memo = ""
	var body : some View {
    Vstack {
    	TextEditor(text: $memo)
        }
        .offset(y: -keyboardHeight / 2)
		.animation(.easeOut(duration: 0.3))
		.keyboardHeight($keyboardHeight)
    }
}

먼저 키보드 높이를 받아올 객체를 선언해주고 미리 만들어준 뷰의 모디파이어인 .keyboardHeight($keyboardHeight)에 값을 넘겨서 키보드 높이를 바인딩으로 받아온다.
그리고 이 높이를 활용하여 뷰의 기준값에 키보드의 높이의 절반 정도를 빼준다.
offset(y: -keyboardHeight / 2) 은 뷰의 기준점에서 특정값만큼을 더해줘서 위치를 조정하는 모디파이어이다. 따라서 음수를 더해줘서 결과적으로 그 높이만큼 뷰를 위쪽으로 당겨준다.

이때 offset적용값은 textEditor의 위치에 따라 조정할 필요가 있다.
단순히 키보드 높이만큼 올려야겠다는 생각으로 전체를 빼버리면 생각보다 키보드와 텍스트 필드의 위치가 큰 차이가 난다. 아마도 상단부터 있는 전체 여백까지 같이 위로 당겨지기 때문인 듯하다. 필요해 따라 값을 잘 조정해서 사용하면 될 듯하다.

그리고 키보드가 올라올 때 화면이 같이 당겨지는 것을 자연스럽게 보여주기 위해 animation을 뒤로 갈수록 느려지게 당겨지는 easeOut효과를 주고 그 시간을 설정했다. 개인적으로 0.3정도가 안정적이었다.

참고로, animation은 deprecated되었다는 알람이 뜬다. 가능하면 애니메이션을 다른 방식으로 적용하는 게 향후에는 더 좋을 듯하다.

키보드 내리는 버튼 추가

키보드 자체를 어떻게 바꾸기보다는 단순히 키보드를 내리는 버튼을 추가하여 이 문제를 해결하고자 했다.

public extension View {
	func hideKeyboard() {
			//셀렉터 선언
        let resign = #selector(UIResponder.resignFirstResponder)
        UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
      }
}

이전과 마찬가지로 뷰의 익스텐션으로 키보드를 내리는 func을 추가했다.
selector 는 "메서드를 식별할 수 있는 고유한 이름"이다.
UIKit 내부의 Object-C 런타임으로 실행되는 메서드가 셀렉터를 파라미터로 전달받을 때, 전달에 필요한 셀렉터 인스턴스를 생성하려고 사용한다고 한다.
쉽게 생각하면, @objc처럼 오브젝트C 기반 개체와 swift연결을 위해 사용하는 것이다.

여기서는 UIResponder의 요청값을 인스턴스로 가져오는데 사용되었다.
UIResponder는 UIKit app의 이벤트 핸들링, 응답자 객체로 이벤트 핸들링 중
resignFirstResponder 즉, 첫번째 응답자 상태를 포기하라는 요청을 객체에 전달하는 것을 가져와서 앱 액션으로 전달한다.

뷰는 첫번째 응답자가 되었을 때 사용자의 요청, 행위를 응답하기 위해 키보드를 띄운다. 이를 포기하는 것은 결국 키보드를 내리는 것이다.
결과적으로, 응답자 객체 상태를 포기하라는 메시지를 전달하여 키보드를 내린다.

이 func을 뷰에 적용하는 방법은 크게 세 가지 정도가 있었다.

  • 키보드 위에 내리는 버튼 추가하기
  • 뷰 내에 키보드 버튼을 내리는 버튼 추가하기
  • 키보드 외 화면을 터치했을 때 화면을 내리게 하기

고민하다가 키보드 위와 뷰 내에 버튼을 동시에 넣어두기로 했다.
키보드에 버튼을 추가하기 위해서 키보드 toolbar를 활용했다.

TextEditor(text: $memo)
                .toolbar {
                    ToolbarItemGroup(placement: .keyboard) {
                        Button {
                            hideKeyboard()
                        } label: {
                            Image(systemName: "keyboard.chevron.compact.down")
                            Text("키보드 내리기")
                        }
                    }
                }
        }

이미지는 SF심볼 중 키보드 down으로 설정했다.
뷰에도 Toolbar를 설정하면 상단 화면에 똑같이 버튼을 설정할 수 있다.

키보드 부분 전체 소스코드


import Foundation
import SwiftUI

struct KeyboardProvider : ViewModifier {
    
    //키보드 높이값
    var keyboardHeight: Binding<CGFloat>
    
    func body(content: Content) -> some View {
        content
        //키보드 올라가기 직전 노티를 받으면 나오는 객체
            .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
                       perform: { notification in
                guard let userInfo = notification.userInfo,
                      let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
                
                //키보드 높이값 . 바인딩 원본 객체 연결 -> 전달
                self.keyboardHeight.wrappedValue = keyboardRect.height
                
            })
        //키보드 닫기 전 보내는 노티 받으면 실행
            .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
                         perform: { _ in
                //키보드 높이값 0으로 변경
                self.keyboardHeight.wrappedValue = 0
            })
    }
}

public extension View {
    func keyboardHeight(_ state: Binding<CGFloat>) -> some View {
        self.modifier(KeyboardProvider(keyboardHeight: state))
    }
    
    func hideKeyboard() {
        let resign = #selector(UIResponder.resignFirstResponder)
        UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
      }
}

키보드 툴바에 버튼 추가 외에도 만약 done 버튼이 불필요한 경우 등에는 return키를 done 버튼으로 바꾸는 방법도 있으니 사용할 경우를 잘 생각해서 설정하면 될 듯하다.
나의 경우, TextEditor의 경우에는 줄바꿈이 있었기 때문에 return키를 살려야 했다.

xcode simulator에서 키보드 확인하는 법


Command + K (키보드 보여주기)

참고자료


SwiftUI Keyboard Listeners
SwiftUI dismiss keyboard when tapping segmentedControl
How to add a toolbar to the keyboard

profile
왕초보 개발일지

0개의 댓글