iOS 응용 (SwiftUI)
[iOS - SwiftUI] 키보드 높이 구하는 방법 (#combine)
jake-kim
2025. 2. 12. 01:04
SwiftUI에서 키보드 높이 구하는 방법
- Combine을 사용하지 않으면 KeyboardInfo라는 클래스에서 기존 UIKit방식대로 NotificationCenter로 등록하는 방법이 있음
public class KeyboardInfo: ObservableObject {
public static var shared = KeyboardInfo()
@Published public var height: CGFloat = 0
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIApplication.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
@objc func keyboardChanged(notification: Notification) {
if notification.name == UIApplication.keyboardWillHideNotification {
self.height = 0
} else {
self.height = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
}
- 하지만 NotificationCenter를 사용할 때 따로 removeObserver를 사용하지 않으면 메모리에서 해제가 안되는 위험이 있으므로 아래처럼 작성이 필요 했었음
deinit {
NotificationCenter.default.removeObserver(self)
}
- SwiftUI에서는 위 방법 말고도 Combine을 사용하면 더욱 편리하게 키보드 높이 구하기가 가능
Combine을 사용하여 키보드 높이 구하기
- 키보드 높이를 구하는 Keyboard 클래스를 정의
- 내부에서 Combine으로 키보드를 관찰하고 있다가 currentHeight에 값을 갱신시키는 방향으로 구현
class Keyboard: ObservableObject {
@Published var currentHeight: CGFloat = 0
}
- init시점에서 바인딩하기
class Keyboard: ObservableObject {
@Published var currentHeight: CGFloat = 0
private var cancellableSet: Set<AnyCancellable> = []
init() {
bind()
}
private func bind() {
}
}
- bind() 구현
- keyboard가 나타날때의 이벤트 (UIResponder.keyboardWillShowNotification)와 사라질때의 이벤트(UIResponder.keyboardWillHideNotification)를 merge로 관찰하는 방식
- 우선 willShow 이벤트 먼저 옵저빙
let keyboardWillShowPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
- 위 코드에서 NotificationCenter.default 까지는 UIKit에도 있지만 애플에서 Combine을 위한 코드를 아래처럼 extension으로 제공한 것을 사용
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension NotificationCenter {
/// Returns a publisher that emits events when broadcasting notifications.
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - object: The object posting the named notfication. If `nil`, the publisher emits elements for any object producing a notification with the given name.
/// - Returns: A publisher that emits events when broadcasting notifications.
public func publisher(for name: Notification.Name, object: AnyObject? = nil) -> NotificationCenter.Publisher
}
- 여기서 관심사는 keyboardHeight이므로 인자로 넘어오는 userInfo값을 사용하여 keyboardFrame을 가져오기
let keyboardWillShowPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.map { notification -> CGFloat in
let userInfo = notification.userInfo
let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
return keyboardFrame.height
}
- 키보드가 사라지는 willHide도 옵저빙
- 키보드가 사라질땐 userInfo값 없어도 0이라고 알 수 있으므로 바로 0 리턴
let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
- 이 두 값을 merge하여 currentHeight에 입력하여 바인딩 완료
private var cancellableSet: Set<AnyCancellable> = []
Publishers.Merge(keyboardWillShowPublisher, keyboardWillHidePublisher)
.subscribe(on: RunLoop.main)
.assign(to: \.currentHeight, on: self)
.store(in: &cancellableSet)
- Keyboard 전체 코드
class Keyboard: ObservableObject {
@Published var currentHeight: CGFloat = 0
private var cancellableSet: Set<AnyCancellable> = []
init() {
bind()
}
private func bind() {
let keyboardWillShowPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.map { notification -> CGFloat in
let userInfo = notification.userInfo
let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
return keyboardFrame.height
}
let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
Publishers.Merge(keyboardWillShowPublisher, keyboardWillHidePublisher)
.subscribe(on: RunLoop.main)
.assign(to: \.currentHeight, on: self)
.store(in: &cancellableSet)
}
}
사용하는 쪽
- keyboard를 @ObservedObject로 선언하고 keyboard.height하여 키보드 높이값 사용
struct ContentView: View {
@ObservedObject private var keyboard = Keyboard()
@State private var input = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("input")
}
Text("Keyboard height: \(keyboard.currentHeight)")
}
.onTapGesture {
UIResponder.resignFirstResponder()
}
}
}
extension UIResponder {
@objc static func resignFirstResponder() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
결과)