관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - SwiftUI] 키보드 높이 구하는 방법 (#combine) 본문

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)
    }
}

결과)

Comments