Skip to content

제스처로 기능 활용이 가능한 한글, 영어 키보드

Notifications You must be signed in to change notification settings

SNMac/SYKeyboard

Repository files navigation

SY키보드

SY키보드는 가볍고, 사용하기 간편한 한글, 영어 키보드입니다.

  • 한글 키보드: 나랏글, 천지인, 두벌식
  • 영어 키보드: QWERTY

Figma

개발 기간: 2024.07.30 ~ 2025.01.15
리팩토링 기간: 2025.07.09 ~ 2025.12.07




👥 대상 사용자

  • 나랏글 키보드/천지인 키보드를 계속 사용해 왔던 사람
  • 나랏글 키보드/천지인 키보드에 입문하는 사람
  • 편의 기능이 있는 두벌식 키보드를 사용해 보고 싶은 사람
  • 필수적인 기능들을 포함하되, 가벼운 키보드 앱을 찾는 사람



🛠️ 기술 스택

범위 기술 이름
의존성 관리 도구 SPM
형상 관리 도구 Git, GitHub
디자인 패턴 Delegate, Singleton
인터페이스 UIKit, SwiftUI
활용 API Firebase Analytics, Firebase Crashlytics, Google AdMob
내부 저장소 UserDefaults
테스트 Swift Testing



🔨 개발 환경

Static Badge Static Badge Static Badge



👨‍💻 트러블 슈팅

복잡했던 버튼 코드

SwiftUI로 최초 개발

첫 iOS 프로젝트인 SY키보드를 SwiftUI로 개발하여 1월에 출시하였다.
하지만 Swift 언어를 다루는 데에 미숙했던 것과 UIKit에 비해 부족한 터치 이벤트 및 상태 관리로 인해 키보드 버튼 코드가 매우 길어졌다.
이후 4개월간의 UIKit 부트캠프를 수강하며 어느 정도 iOS 앱 개발에 익숙해지면서, 복잡한 상태 관리에는 SwiftUI보다 UIKit이 적합함을 알게 되었다.
미숙했던 개발 실력과 SwiftUI의 특징이 맞물려 유지보수하기 어려웠던 SY키보드의 UIKit 리팩토링을 생각하게 되었고, 부트캠프 수료 이후 진행하였다.


SwiftUI ➡️ UIKit 리팩토링

메인 앱

SY키보드의 메인 앱은 키보드 설정 위주의 단순한 구조이므로 기존 SwiftUI를 유지하면서 개선에 목적을 두었다.

Keyboard Extension

Keyboard Extension 부분은 SwiftUI에서 UIKit으로 리팩토링하는 작업을 진행했다.
리팩토링을 거치며 영어 키보드를 추가하였고, 천지인 키보드도 다음 업데이트를 위해 기본적인 UI를 만들어 두었다.
또한, 다른 프로젝트에서 가져와 수정해서 사용했던 한글 오토마타 코드도 처음부터 다시 만들기로 결정했다.


UIKit 리팩토링 작업

SwiftUI에서는 ButtonactiontouchUpInside 기준으로 고정되어 있어서, Gesture를 사용하여 우회적으로 다른 이벤트들을 구현해야 했다.
하지만 UIKit에서는 addTarget 혹은 addActionUIControlEvents를 통해 touchDown, touchUpInside, touchDownRepeat로 세밀하게 제어할 수 있었다.
또한 버튼이 눌렸을 때(highlighted, selected)에 대한 상태 변경도 더 직관적이었다.

기존 SwiftUI
// KeyboardButton 구조체의 일부
Button(action: {}) {
            // Image 버튼들
            if systemName != nil {
                if systemName == "return.left" {  // 리턴 버튼
                    if state.returnButtonType == .default {
                        Image(systemName: "return.left")
                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                            .font(.system(size: imageSize))
                            .foregroundStyle(Color(uiColor: UIColor.label))
                            .background(checkPressed() ? Color("PrimaryKeyboardButton") : Color("SecondaryKeyboardButton"))
            // ...(중략)...
        .highPriorityGesture(
            LongPressGesture(minimumDuration: 0)
                .onEnded({ _ in
                    // 버튼 눌렀을 때
                    os_log("LongPressGesture() onEnded: pressed", log: log, type: .debug)
                    gesturePressed()
                })
        )
        .simultaneousGesture(
            LongPressGesture(minimumDuration: state.longPressDuration, maximumDistance: cursorActiveDistance)
            // 버튼 길게 눌렀을 때
                .onEnded({ _ in
                    os_log("simultaneous_LongPressGesture() onEnded: longPressed", log: log, type: .debug)
                    gestureLongPressed()
                })
                .sequenced(before: DragGesture(minimumDistance: 10, coordinateSpace: .global))
            // 버튼 길게 누르고 드래그시 호출
                .onChanged({ value in
                    switch value {
                    case .first(_):
                        break
                    case .second(_, let dragValue):
                        if let value = dragValue {
                            os_log("LongPressGesture()->DragGesture() onChanged: longPressedDrag", log: log, type: .debug)
                            gestureLongPressedDrag(dragGestureValue: value)
                        }
                    }
                })
                .exclusively(before: DragGesture(minimumDistance: cursorActiveDistance, coordinateSpace: .global)
                             // 버튼 드래그 할 때
                    .onChanged({ value in
                        os_log("exclusively_DragGesture() onChanged: drag", log: log, type: .debug)
                        gestureDrag(dragGestureValue: value)
                    })
                            )
        )
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
            // 버튼 뗐을 때
                .onEnded({ _ in
                    os_log("DragGesture() onEnded: released", log: log, type: .debug)
                    gestureReleased()
                })
        )
UIKit 리팩토링 이후
// BaseKeyboardViewController 클래스의 일부
func addInputActionToTextInterableButton(_ button: TextInteractable) {
    let inputAction = UIAction { [weak self] action in
        guard let self, let currentButton = action.sender as? TextInteractable else { return }
        
        if currentButton.isProgrammaticCall {
            // 코드(sendActions)로 호출된 경우 -> 무조건 입력 수행
            performTextInteraction(for: currentButton)
            
        } else {
            // 사용자가 touchUpInside한 경우 -> currentPressedButton 확인
            if let currentPressedButton = buttonStateController.currentPressedButton,
               currentPressedButton == currentButton {
                performTextInteraction(for: currentButton)
            }
        }
    }
    if button is DeleteButton {
        button.addAction(inputAction, for: .touchDown)
    } else if let spaceButton = button as? SpaceButton {
        button.addAction(inputAction, for: .touchUpInside)
        addPeriodShortcutActionToSpaceButton(spaceButton)
    } else {
        button.addAction(inputAction, for: .touchUpInside)
    }
}

// ButtonStateController 클래스의 일부
func setFeedbackActionToButtons(_ buttonList: [BaseKeyboardButton]) {
    buttonList.forEach { button in
        let playFeedbackAndSetPressed: UIAction
        if button is ShiftButton {
            playFeedbackAndSetPressed = UIAction { [weak self] action in
                guard let senderButton = action.sender as? BaseKeyboardButton else { return }
                
                if let previousButton = self?.currentPressedButton, previousButton != senderButton {
                    previousButton.sendActions(for: .touchUpInside)
                }
                
                self?.isShiftButtonPressed = true
                senderButton.playFeedback()
            }
        } else {
            playFeedbackAndSetPressed = UIAction { [weak self] action in
                guard let senderButton = action.sender as? BaseKeyboardButton else { return }
                
                if let previousButton = self?.currentPressedButton, previousButton != senderButton {
                    previousButton.sendActions(for: .touchUpInside)
                }
                
                self?.currentPressedButton = senderButton
                senderButton.playFeedback()
            }
        }
        button.addAction(playFeedbackAndSetPressed, for: .touchDown)
    }
}

// PrimaryButton 클래스의 일부
func setStyles() {
    self.configurationUpdateHandler = { [weak self] button in
        guard let self else { return }
        switch button.state {
        case .normal:
            backgroundView.backgroundColor = isGesturing ? .primaryButtonPressed : .primaryButton
        case .highlighted:
            backgroundView.backgroundColor = isPressed || isGesturing ? .primaryButtonPressed : .primaryButton
        default:
            break
        }
    }
}

결론 및 회고

설명 스크린샷
리팩토링 이전 SY키보드 구버전 입력 처리 시간
리팩토링 이후 SY키보드 신버전 입력 처리 시간

실기기에 리팩토링 이전과 이후 버전을 Release 빌드로 설치 후 "동해물과 백두산이 마르고 닳도록"을 입력, 키보드의 입력 처리 시간을 측정한 결과이다.


명령형 프레임워크인 UIKit으로 리팩토링하면서 복잡한 버튼, 제스처 로직을 효율적으로 처리할 수 있었지만, 선언형 프레임워크인 SwiftUI보다 UI 구현 코드는 더 길어지게 되었다.
하지만 리팩토링 이후 키보드 입력 처리 시간이 이전보다 1/3 정도로 단축되어 성능이 높아지는 이점을 얻을 수 있었다.
또한, 현재로선 UIKit이 SwiftUI보다 세밀한 커스텀이 가능한 장점이 있어 SY키보드에는 UIKit이 좀더 적합하다고 생각된다.
최근 WWDC에서 SwiftUI 위주의 업데이트가 계속 발표되고 있으니, 나중에 커스텀하기 더 편하게 SwiftUI가 업데이트된다면 그때 다시 SwiftUI로 리팩토링을 해봐야겠다.




메모리 누수로 인한 크래시

문제 상황

설명 스크린샷
Crashlytics
Instruments
Allocations
Instruments
Generations

Crashlytics에 didReceiveMemoryWarning 로그와 크래시가 발생하는 것을 보고, Profile을 실행하여 다음 사항을 확인했다.

  • Allocations 그래프를 통해 키보드 dismiss 이후에도 인스턴스가 메모리에서 해제되지 않음
  • 키보드 dismiss 이후 Generation에 키보드 관련 인스턴스 존재
  • 키보드를 표시할때마다 새로운 인스턴스가 쌓이다가 임계점을 넘어가면 크래시가 발생

원인 분석

  • KeyboardViewdeinit되지 않음
  1. iOS 버그로 인해 코드 베이스 레이아웃 작성 시 BaseKeyboardViewController(UIInputViewController)의 view(UIInputView)가 메모리에서 해제되지 않는다.
  2. viewsubviewKeyboardView 또한 해제되지 않게 된다.
  • 버튼이 순환 참조로 인해 deinit되지 않음
  1. BaseKeyboardViewControllerButtonStateController에서 버튼에 액션을 할당할 때, 클로저 내부에서 인자로 들어온 버튼을 강하게 참조
  2. 버튼 -> UIAction -> 클로저 -> 버튼 순환 참조
  3. UIAction 클로저 내부 [weak self]는 해당 인스턴스와의 연결만 약한 참조로 변경
// BaseKeyboardViewController (UIInputViewController)

func addInputActionToTextInterableButton(_ button: TextInteractable) {
    let inputAction = UIAction { [weak self] action in
        guard let self, let currentButton = action.sender as? TextInteractable else { return }
        
        if currentButton.isProgrammaticCall {
            // 메서드 인자인 'button'을 클로저가 강하게 참조(캡처)
            performTextInteraction(for: button)
    // ...

해결 과정

  • KeyboardView를 Storyboard(xib)로 초기화하고, loadView 시점에 할당하도록 수정
  • UIAction 클로저의 매개변수를 활용하여 버튼 객체에 대한 강한 참조 제거
// BaseKeyboardViewController (UIInputViewController)

deinit {
    logger.debug("\(String(describing: type(of: self))) deinit")
    keyboardView.removeFromSuperview()
}

func addInputActionToTextInterableButton(_ button: TextInteractable) {
    let inputAction = UIAction { [weak self] action in
        guard let self, let currentButton = action.sender as? TextInteractable else { return }
        
        if currentButton.isProgrammaticCall {
            // UIAction 클로저의 매개변수를 활용, 버튼에 대한 강한 참조(캡처) 제거
            performTextInteraction(for: currentButton)
    // ...
설명 스크린샷
Instruments
Allocations
Instruments
Generations

Instruments의 Allocations 그래프와 Generations 표를 통해 키보드 dismiss 이후에 인스턴스가 메모리에서 해제되는 것을 확인하였다.

출처: Apple Developer Forums - UIInputView is not deallocated from memory




키보드 높이 제약조건 지정 시 키보드 표시 애니메이션 글리칭 현상

문제 상황

설명 스크린샷
애니메이션
글리칭

애플 공식 문서(레거시, 최신) 기반으로 키보드 높이 조절 코드를 구현했을 때, 위 GIF처럼 키보드가 잠깐동안 높이 튀어오르는 현상이 발생하였다.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    setKeyboardHeight()
    FeedbackManager.shared.prepareHaptic()
}
  
func setKeyboardHeight() {
    let heightConstraint = self.view.heightAnchor.constraint(equalToConstant: UserDefaultsManager.shared.keyboardHeight)
    heightConstraint.priority = .init(999)
    heightConstraint.isActive = true
}
  • view를 위한 애니메이션이 구성되기 직전인 viewWillAppear 메서드에 높이 제약조건 코드 구현

원인 분석

문제 해결을 위해 찾아보던 중 Stack Overflow의 한 질문글의 답변에서 힌트를 얻을 수 있었다.

private var constraintsHaveBeenAdded = false

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    initKeyboardConstraints()
}

private func initKeyboardConstraints() {
    if constraintsHaveBeenAdded { return }
    guard let superview = view.superview else { return }
    view.translatesAutoresizingMaskIntoConstraints = false
    view.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
    view.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
    view.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
    view.heightAnchor.constraint(equalToConstant: 250.0).isActive = true
    constraintsHaveBeenAdded = true
}
  1. 제약조건이 설정되었는지를 판단하는 플래그 변수 constraintsHaveBeenAdded 설정
  2. 제약조건이 이미 설정되었거나, 상위 view가 설정되지 않은 경우 실행 X (방어 코드)
  3. view의 모든 edge에 대해 상위 view와 같도록 제약조건 설정
  4. constraintsHaveBeenAdded를 true로 설정
  • 이전 코드에서는 view의 모든 edge에 대해 상위 view와 같도록 제약조건을 설정하는 코드($0.edges.equalToSuperview())와 translatesAutoresizingMaskIntoConstraintsfalse로 설정하는 코드가 없었음
  • 이로 인해 Autoresizing Mask로 view의 크기와 위치를 정하려 하는 과정에서 Auto Layout의 높이 제약조건이 충돌을 일으켜 애니메이션에 글리칭이 발생한 것으로 추측
  • translatesAutoresizingMaskIntoConstraintsfalse로 설정하는 경우 아래 사진처럼 UI가 치우치는 현상이 발생함
설명 스크린샷
UI 치우침

해결 과정

위 답변을 토대로 높이 제약조건 코드를 수정하고 방어코드를 추가하였다.

  • 키보드 가로모드 대응 코드도 추가된 상태
func setKeyboardHeight() {
    let keyboardHeight: CGFloat
    if let orientation = self.view.window?.windowScene?.effectiveGeometry.interfaceOrientation {
        keyboardHeight = orientation == .portrait ? UserDefaultsManager.shared.keyboardHeight : KeyboardLayoutFigure.landscapeKeyboardHeight
    } else {
        if !isPreview {
            assertionFailure("View가 window 계층에 없습니다.")
        }
        keyboardHeight = UserDefaultsManager.shared.keyboardHeight
    }
    
    if let keyboardHeightConstraint {
        keyboardHeightConstraint.constant = keyboardHeight
    } else {
        let constraint = self.view.heightAnchor.constraint(equalToConstant: keyboardHeight)
        constraint.priority = .init(999)
        constraint.isActive = true
        
        keyboardHeightConstraint = constraint
    }
}
설명 스크린샷
해결 이후

출처: Stack Overflow - iOS 8 Custom Keyboard: Changing the height without warning 'Unable to simultaneously satisfy constraints...'




📊 다이어그램

키보드 종류 구조

%%{
  init: {
    "theme": "default",
    "fontFamily": "monospace",
    "elk": {
        "mergeEdges": false,
        "nodePlacementStrategy": "BRANDES_KOEPF",
        "forceNodeModelOrder": false,
        "considerModelOrder": "NODES_AND_EDGES"
    },
    "class": {
        "hideEmptyMembersBox": true
    }
  }
}%%
classDiagram
direction LR
    %% Keyboard Type
    namespace KeyboardGestureController {
      class TextInteractionGestureController
      class SwitchGestureController
    }

    namespace KeyboardGestureProtocol {
      class SwitchGestureHandling
    }

    namespace KeyboardTypeLayoutProtocol {
      class HangeulKeyboardLayoutProvider
      class EnglishKeyboardLayoutProvider
      class SymbolKeyboardLayoutProvider
      class NumericKeyboardLayoutProvider
      class TenkeyKeyboardLayoutProvider
    }

    namespace ParentKeyboardViewController {
      class BaseKeyboardViewController
    }

    namespace FinalKeyboardViewController {
      class HangeulKeyboardViewController
      class EnglishKeyboardViewController
    }

    class NormalKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class PrimaryKeyboardRepresentable:::SYKeyboard_primary { <<protocol>> }
    class HangeulKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class EnglishKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class SymbolKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class NumericKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class TenkeyKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class SwitchGestureHandling:::SYKeyboard_primary { <<protocol>> }

    BaseKeyboardViewController --> PrimaryKeyboardRepresentable: Association
    BaseKeyboardViewController *-- SymbolKeyboardLayoutProvider: Composition
    BaseKeyboardViewController *-- NumericKeyboardLayoutProvider: Composition
    BaseKeyboardViewController *-- TenkeyKeyboardLayoutProvider: Composition

    NormalKeyboardLayoutProvider <|-- PrimaryKeyboardRepresentable: Inheritance
    NormalKeyboardLayoutProvider <|-- SymbolKeyboardLayoutProvider: Inheritance
    NormalKeyboardLayoutProvider <|-- NumericKeyboardLayoutProvider: Inheritance

    BaseKeyboardViewController *-- TextInteractionGestureController: Composition
    BaseKeyboardViewController *-- SwitchGestureController: Composition

    SwitchGestureHandling <|-- NormalKeyboardLayoutProvider: Inheritance
    SwitchGestureController --> SwitchGestureHandling: Association

    BaseKeyboardViewController <|-- HangeulKeyboardViewController: Inheritance
    BaseKeyboardViewController <|-- EnglishKeyboardViewController: Inheritance

    HangeulKeyboardViewController *-- HangeulKeyboardLayoutProvider: Composition
    PrimaryKeyboardRepresentable <|-- HangeulKeyboardLayoutProvider: Inheritance

    EnglishKeyboardViewController *-- EnglishKeyboardLayoutProvider: Composition
    PrimaryKeyboardRepresentable <|-- EnglishKeyboardLayoutProvider: Inheritance

    classDef SYKeyboard_primary fill:#ffa6ed
Loading

키보드 레이아웃 구조

%%{
  init: {
    "theme": "default",
    "fontFamily": "monospace",
    "elk": {
        "mergeEdges": false,
        "nodePlacementStrategy": "BRANDES_KOEPF",
        "forceNodeModelOrder": false,
        "considerModelOrder": "NODES_AND_EDGES"
    },
    "class": {
        "hideEmptyMembersBox": true
    }
  }
}%%
classDiagram
direction LR
    %% Keyboard Layout
    namespace KeyboardGestureProtocol {
      class SwitchGestureHandling
    }

    namespace KeyboardLayoutProtocol {
      class HangeulKeyboardLayoutProvider
      class EnglishKeyboardLayoutProvider
      class SymbolKeyboardLayoutProvider
      class NumericKeyboardLayoutProvider
      class TenkeyKeyboardLayoutProvider
    }

    namespace ParentKeyboardView {
      class FourByFourKeyboardView
      class FourByFourPlusKeyboardView
      class StandardKeyboardView
    }

    namespace FinalKeyboardView {
      class NaratgeulKeyboardView
      class CheonjiinKeyboardView
      class DubeolsikKeyboardView
      class EnglishKeyboardView
      class SymbolKeyboardView
      class NumericKeyboardView
      class TenkeyKeyboardView
    }

    class BaseKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class NormalKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class SwitchGestureHandling:::SYKeyboard_primary { <<protocol>> }
    class PrimaryKeyboardRepresentable:::SYKeyboard_primary { <<protocol>> }
    class HangeulKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class EnglishKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class SymbolKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class NumericKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class TenkeyKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }

    BaseKeyboardLayoutProvider <|-- NormalKeyboardLayoutProvider: Inheritance
    SwitchGestureHandling <|-- NormalKeyboardLayoutProvider: Inheritance
    BaseKeyboardLayoutProvider <|-- TenkeyKeyboardLayoutProvider: Inheritance

    NormalKeyboardLayoutProvider <|-- PrimaryKeyboardRepresentable: Inheritance

    TenkeyKeyboardLayoutProvider ..|> TenkeyKeyboardView: Implementation

    PrimaryKeyboardRepresentable <|-- HangeulKeyboardLayoutProvider: Inheritance

    FourByFourKeyboardView <|-- NaratgeulKeyboardView: Inheritance
    HangeulKeyboardLayoutProvider <|.. NaratgeulKeyboardView: Implementation
    FourByFourPlusKeyboardView <|-- CheonjiinKeyboardView: Inheritance
    HangeulKeyboardLayoutProvider <|.. CheonjiinKeyboardView: Implementation
    StandardKeyboardView <|-- DubeolsikKeyboardView: Inheritance
    HangeulKeyboardLayoutProvider <|.. DubeolsikKeyboardView: Implementation

    PrimaryKeyboardRepresentable <|-- EnglishKeyboardLayoutProvider: Inheritance

    EnglishKeyboardLayoutProvider ..|> EnglishKeyboardView: Implementation
    StandardKeyboardView <|-- EnglishKeyboardView: Inheritance

    NormalKeyboardLayoutProvider <|-- SymbolKeyboardLayoutProvider: Inheritance
    SymbolKeyboardLayoutProvider ..|> SymbolKeyboardView: Implementation

    NormalKeyboardLayoutProvider <|-- NumericKeyboardLayoutProvider: Inheritance
    NumericKeyboardLayoutProvider ..|> NumericKeyboardView: Implementation
    
    classDef SYKeyboard_primary fill:#ffa6ed
Loading

키보드 버튼 구조

%%{
  init: {
    "theme": "default",
    "fontFamily": "monospace",
    "elk": {
        "mergeEdges": false,
        "nodePlacementStrategy": "BRANDES_KOEPF",
        "forceNodeModelOrder": false,
        "considerModelOrder": "NODES_AND_EDGES"
    },
    "class": {
        "hideEmptyMembersBox": true
    }
  }
}%%
classDiagram
direction LR
    %% Keyboard Button
    namespace TextInteractionProtocol {
      class TextInteractable
    }

    namespace ParentKeyboardButton {
      class BaseKeyboardButton
      class PrimaryButton
      class SecondaryButton
    }

    namespace FinalKeyboardButton {
      class PrimaryKeyButton
      class SpaceButton
      class SecondaryKeyButton
      class ShiftButton
      class DeleteButton
      class SwitchButton
      class NextKeyboardButton
      class ReturnButton
    }

    class TextInteractable:::SYKeyboard_primary { <<protocol>> }

    BaseKeyboardButton <|-- PrimaryButton: Inheritance
    BaseKeyboardButton <|-- SecondaryButton: Inheritance
    BaseKeyboardButton <|-- TextInteractable: Constraint

    PrimaryButton <|-- PrimaryKeyButton: Inheritance
    PrimaryButton <|-- SpaceButton: Inheritance

    PrimaryKeyButton ..|> TextInteractable: Implementation

    SecondaryButton <|-- DeleteButton: Inheritance
    SecondaryButton <|-- NextKeyboardButton: Inheritance
    SecondaryButton <|-- ReturnButton: Inheritance
    SecondaryButton <|-- SecondaryKeyButton: Inheritance
    SecondaryButton <|-- ShiftButton: Inheritance
    SecondaryButton <|-- SwitchButton: Inheritance

    SecondaryKeyButton ..|> TextInteractable: Implementation

    DeleteButton ..|> TextInteractable: Implementation
    ReturnButton ..|> TextInteractable: Implementation
    SpaceButton ..|> TextInteractable: Implementation

    classDef SYKeyboard_primary fill:#ffa6ed
Loading


📱 주요 기능

  1. 나랏글 키보드
    기본에 충실한 나랏글(EZ한글) 키보드입니다.



  1. 천지인 키보드
    입력이 편리한 천지인 키보드입니다.



  1. 두벌식 키보드
    대중적인 두벌식(한글 쿼티) 키보드입니다.



  1. 영어 키보드
    대중적인 영어(QWERTY) 키보드입니다.



  1. 숫자 키패드 탑재
    숫자를 입력할 때 큰 버튼으로 편하게 입력할 수 있는 숫자 입력 전용 키패드를 탑재했습니다.



  1. 한 손 키보드 모드
    한 손으로 폰을 들고 있는 상태에서도 입력하기 수월하도록 한 손 키보드 모드를 제공합니다.



  1. 다양하고 디테일한 키보드 설정
    길게 누르기 동작, 커서 이동, 키보드 높이 및 한 손 키보드 너비 조절 등 사용자의 편의에 맞게 키보드 설정이 가능합니다.

    



About

제스처로 기능 활용이 가능한 한글, 영어 키보드

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages