apple/SwiftUI & Combine

[SwiftUI] keyboard 이벤트 핸들링

lgvv 2024. 3. 6. 02:22

[SwiftUI] keyboard 이벤트 핸들링


앱 개발에 있어서 키보드 상태에 따라서 뷰의 다른 컴포넌트들의 높이가 조정되는 등 키보드와 관련해서는 꽤나 까다롭다.

 

UIKit을 사용한다면 

 - iOS 15 이상: keyboardLayoutGuide를 활용하여 레이아웃을 잡기

 - iOS 14 이하: 키보드의 상태에 따라 키보드의 높이를 계산해서 뷰의 위치를 조정

view.keyboardLayoutGuide

 

SwiftUI 사용

 - iOS 15 이상: @FocusState 활용

 - iOS 14 이하: NotificationCenter와 Combine을 활용

 

 

샘플코드

 

키보드 상태를 읽을 수 있도록 아래 코드를 작성

protocol KeyboardReadable {
    var keyboardPublisher: AnyPublisher<Bool, Never> { get }
}

extension KeyboardReadable {
    var keyboardPublisher: AnyPublisher<Bool, Never> {
        Publishers.Merge(
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillShowNotification)
                .map { _ in true },
            
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillHideNotification)
                .map { _ in false }
        )
        .eraseToAnyPublisher()
    }
}

 

위처럼 작성하고 아래처럼 뷰를 구현해봅시다.

아래 코드를 그대로 복사 붙여넣기를 통해 활용할 수 있다.

 

import SwiftUI
import Combine

final class ContentViewModel: ObservableObject,
                              KeyboardReadable {
    
    enum Action {
        case shouldResignKeyboard
    }
    
    /// 검색어
    @Published var searchText: String = ""
    /// 키보드가 열려있는지 여부
    @Published var isShowingKeyboard: Bool = false
    
    init() {
        bind()
    }
    
    private var cancellables = Set<AnyCancellable>()
    
    private func bind() {
        keyboardPublisher
            .removeDuplicates()
            .sink { [weak self] isShowing in
                print("\(isShowing)")
                guard let self else { return }
                self.isShowingKeyboard = isShowing
            }.store(in: &cancellables)
    }
    
    func send(action: Action) {
        switch action {
        case .shouldResignKeyboard:
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                            to: nil, from: nil, for: nil)
        }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack {
            Text(
                viewModel.isShowingKeyboard
                ? "키보드가 올라와 있어요."
                : "키보드가 내려간 상태에요."
            )
            
            TextField("검색어를 입력하세요.", text: $viewModel.searchText)
                .autocorrectionDisabled()
                .textFieldStyle(.roundedBorder)
            
            Button {
                viewModel.send(action: .shouldResignKeyboard)
            } label: {
                Text("키보드 강제로 내리는 버튼")
            }
            .buttonStyle(.bordered)
            
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

 

해당 샘플 코드는 내가 요즘 주로 사용하는 SwiftUI에서 사용하는 MVVM 형태다.

키보드 뿐만 아니라 SwiftUI는 뷰가 많이 쪼개지기 때문에 해당 형태를 살짝 변형하여 Delegate Pattern 처럼 변형해 해당 뷰에서 발생하는 이벤트를 다른 뷰로 전달할 수 있다.

 

해당 코드 동작 영상

해당 코드 동작 영상