Post

SwiftUI: 하드웨어 키보드 입력 받기 (Representable 사용)

SwiftUI: 하드웨어 키보드 입력 받기 (Representable 사용)

[rcblock id=”5440”]

소개

SwiftUI 앱에서 하드웨어 키보드의 입력을 받는 방법입니다.

여기서 하드웨어 키보드란 iOS/iPadOS 환경에서는 USB 키보드, 블루투스 키보드, 액세서리 키보드 등을 뜻하며, Mac Catalyst/M시리즈 CPU 등 맥 환경에서 실행되는 경우에는 노트북의 키보드, 유선 키보드, 블루투스 키보드 등을 뜻합니다. 하드웨어 키보드의 입력에 대한 처리란 이런 키보드에서 키를 눌렀을 때 해야할 작업을 뜻합니다.

SwiftUI에서는 직접적으로 기능을 제공하지 않는 것처럼 보이기 때문에 UIViewController의 키보드 처리 과정을 호스팅해서 사용하는 UIViewControllerRepresentable을 사용하도록 하겠습니다.

 

방법

1) 기본 ContentView 작성

먼저 기본 ContentView 레이아웃을 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import SwiftUI

struct ContentView: View {
    @State var pressedKeyStr = ""
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Button {
                buttonAction()
            } label: {
                Text("Press ZXCVASDF")
            }
            Text(pressedKeyStr)
                .font(.largeTitle)
            // ... 이곳에 키보드 입력 관련 내용이 들어갑니다 ... //
        }
        .padding()
    }
    
    func buttonAction(_ text: String = "From SwiftUI Button") {
        pressedKeyStr = text
    }
}

키보드를 누르면 위의 From SwiftUI Button 대신 입력된 키보드의 문자가 출력될 것입니다.

 

2) 키보드 이벤트에 대한 대리자(delegate) 생성
1
2
3
protocol KeyEventVCDelegate: AnyObject {
    func didKeyPressBegan(key: UIKey)
}

UIViewControllerRepresentable과 더불어 위의 대리자의 didKeyPressBegan 함수+ Coordinator를 통해 SwiftUI 상에서 키보드 입력에 대한 처리가 가능하게 됩니다.

  • 이 예제에서는 키보드를 누르기 시작했을 때(Presses Began)에 대한 이벤트만 필요하므로 함수를 하나만 작성했지만, 키보드를 뗐을 때(Presses Ended)에 대한 이벤트도 필요하다면 해당 함수를 추가적으로 작성하면 됩니다.

 

3) 키보드 이벤트를 처리하는 뷰 컨트롤러(UIViewController) 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class KeyEventViewController: UIViewController {
    weak var delegate: KeyEventVCDelegate?
    
    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        for press in presses {
            guard let key = press.key else {
                continue
            }
            delegate?.didKeyPressBegan(key: key)
        }
    }
    
    override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        // for press in presses {
        //     guard let key = press.key else {
        //         continue
        //     }
        //     print(key)
        // }
    }
}
  • UIViewController에는 pressesBegan, pressesEnded라는 메서드가 있습니다.
    • pressesBegan은 키보드를 누르기 시작했을 때의 이벤트를 처리합니다.
    • pressesEnded는 키보드를 뗐을 때의 이벤트를 처리합니다.
  • pressesBegan이 되었을 때 대리자의 didKeyPressBegan(key:)를 실행합니다.
    • 대리자 함수는 Coordinator에서 실행되도록 할 것입니다.

 

4) KeyEventViewController에 대한 래핑 구조체(struct) 작성

UIViewControllerRepresentable을 준수(conform)하는 함수를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct KeyboardView: UIViewControllerRepresentable {
    typealias UIViewControllerType = KeyEventViewController
    typealias KeyboardHandler = (String) -> ()
    
    let viewController = KeyEventViewController()
    var keyboardHandler: KeyboardHandler
    
    func makeUIViewController(context: Context) -> KeyEventViewController {
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: KeyEventViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        // ... 후술 ... //
    }
    
    class Coordinator: KeyEventVCDelegate {
        // ... 후술 ... //
    }
}

 

5) 코디네이터 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func makeCoordinator() -> Coordinator {
    Coordinator(viewController, keyboardHandler: keyboardHandler)
}

class Coordinator: KeyEventVCDelegate {
    var keyboardHandler: KeyboardHandler
    
    init(_ viewController: KeyEventViewController, keyboardHandler: @escaping KeyboardHandler) {
        self.keyboardHandler = keyboardHandler
        viewController.delegate = self // ViewController의 delegate를 KeyEventVCDelegate를 conform하는 Coordinator와 연결 
    }
    
    // Delegate - 사용자 정의 KeyEventVCDelegate
    func didKeyPressBegan(key: UIKey) {
        keyboardHandler(key.characters)
    }
}

앞에서 언급했던 대리자 함수 didKeyPressBegan(key:)를 실행하려면 코디네이터(Coordinator)가 필요합니다.

  • Coordinator에 대한 자세한 내용은 SwiftUI: Representable을 이용해서 UIViewController 띄우기 포스트를 참고해주세요.
  • keyboardHandler
    • 키보드를 입력했을 때 처리해야 할 작업을 외부에서 지정할 수 있도록 (String) -> () 타입의 클로저(콜백)를 값으로 받는 변수입니다.
  • viewController.delegate = self
    • 뷰 컨트롤러(=>KeyEventViewController)의 delegateCoordinator를 지정합니다.
    • CoordinatorKeyEventVCDelegate를 준수(conform)하기 때문에 이러한 지정이 가능합니다.
    • KeyEventViewController에서 키보드 입력을 받으면 pressesBegan(...)함수가 실행되고, 여기에서 delegate?.didKeyPressBegan(key:)가 실행됩니다.
    • 여기서 대리자가 코디네이터로 지정되어 있기 때문에 코디네이터 인스턴스 내에서 구현된 didKeyPressBegan(key:)가 실행되는 것입니다.
  • func didKeyPressBegan …
    • keyboardHandler 클로저 함수가 실행되도록 구현합니다.

 

6) ContentView에 키보드 입력 부분 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct ContentView: View {
    @State var pressedKeyStr = ""
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Button {
                buttonAction()
            } label: {
                Text("Press ZXCVASDF")
            }
            Text(pressedKeyStr)
                .font(.largeTitle)

            KeyboardView(keyboardHandler: { keyStr in
                let charSet = CharacterSet(charactersIn: "zxcvasdfZXCVASDF")
                if keyStr.rangeOfCharacter(from: charSet) != nil {
                    buttonAction(keyStr)
                }
            })
            .frame(width: 0, height: 0)
        }
        .padding()
    }
    
    func buttonAction(_ text: String = "From SwiftUI Button") {
        pressedKeyStr = text
    }
}
  • 래핑 구조체인 KeyboardView를 추가합니다.
  • z, x, c, v, a, s, d, f 중 하나의 키가 눌리면 buttonAction(_:)을 실행해 pressedKeyStr가 해당 키값으로 바뀌게 합니다.
  • frame(width: 0, height: 0)
    • UIViewController의 기본 크기만큼 뷰를 차지하기 때문에 프레임의 크기를 0으로 만들어 다른 뷰들에게 방해되지 않도록 합니다.
  • buttonAction
    • 버튼 또는 키보드를 눌렀을 때 해야 할 작업을 지정합니다.
    • 여기서는 @State var pressedKeyStr의 값을 변경하도록 했습니다.

 

http://www.giphy.com/gifs/lOwSPlC2J9Yz5oewYl

This post is licensed under CC BY 4.0 by the author.