
이 경우에는 키보드 높이를 파악하여 이에 맞춰 화면 위치를 조정할 필요가 있었다.
먼저 키보드 높이값을 파악하기 위한 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키를 살려야 했다.
Command + K (키보드 보여주기)
SwiftUI Keyboard Listeners
SwiftUI dismiss keyboard when tapping segmentedControl
How to add a toolbar to the keyboard