Skip to content

About

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

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

SY키보드

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

  • 한글 키보드: 나랏글, 천지인, 두벌식
  • 영어 키보드: QWERTY
  • 한영 통합 키보드: 한글·영어 키보드를 한영 전환 버튼으로 오가는 단일 키보드

Figma

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




👥 대상 사용자

  • 나랏글/천지인 키보드를 사용 중이거나 입문하는 사람
  • 키보드 앱을 찾는 사람
  • 키보드 자체 기능과 더불어 기본적인 편의 기능이 있는 키보드를 사용해 보고 싶은 사람
    • 한 손 키보드, 키보드 높이 조절, 자동완성 문구 추천 등
  • 한글 키보드를 사용해 보고 싶은 외국인



🛠️ 기술 스택

범위 기술 이름
의존성 관리 도구 SPM
형상 관리 도구 Git, GitHub
디자인 패턴 Delegate, Singleton, Adapter
인터페이스 UIKit, SwiftUI
활용 API Firebase Analytics, Firebase Crashlytics, Google AdMob(+ Meta Audience Network 미디에이션)
내부 저장소 UserDefaults(App Group), 바이너리 plist 파일(n-gram 학습 데이터 · 클립보드 기록), 클립보드 이미지 파일(App Group Library/Application Support)
자동완성 텍스트 UILexicon, UITextChecker, 자체 n-gram 엔진, 수식 계산 후보(MathExpressionCompletionEvaluator)
로컬라이징 String Catalog
테스트 Swift Testing



🔨 개발 환경

Static Badge|67 Static Badge|55

빌드는 Xcode 26 이상을 기준으로 하며, 로컬 SPM 패키지는 swift-tools-version: 6.0 manifest를 기준으로 한다.



👨‍💻 트러블 슈팅

이 트러블 슈팅 절의 코드는 각 문제를 해결하던 당시의 코드이다. 이후 기능 추가로 현재 구현과는 다르며, 현재 구조는 docs/architecture를 참조한다.

복잡했던 버튼 코드

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에서는 Button의 action이 touchUpInside 기준으로 고정되어 있어서, Gesture를 사용하여 우회적으로 다른 이벤트들을 구현해야 했다.
하지만 UIKit에서는 addTarget 혹은 addAction의 UIControlEvents를 통해 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에 키보드 관련 인스턴스 존재
  • 키보드를 표시할때마다 새로운 인스턴스가 쌓이다가 임계점을 넘어가면 크래시가 발생

원인 분석

  • KeyboardView가 deinit되지 않음
  1. iOS 버그로 인해 코드 베이스 레이아웃 작성 시 BaseKeyboardViewController(UIInputViewController)의 view(UIInputView)가 메모리에서 해제되지 않는다.
  2. view의 subview인 KeyboardView 또한 해제되지 않게 된다.
  • 버튼이 순환 참조로 인해 deinit되지 않음
  1. BaseKeyboardViewController와 ButtonStateController에서 버튼에 액션을 할당할 때, 클로저 내부에서 인자로 들어온 버튼을 강하게 참조
  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())와 translatesAutoresizingMaskIntoConstraints를 false로 설정하는 코드가 없었음
  • 이로 인해 Autoresizing Mask로 view의 크기와 위치를 정하려 하는 과정에서 Auto Layout의 높이 제약조건이 충돌을 일으켜 애니메이션에 글리칭이 발생한 것으로 추측
  • translatesAutoresizingMaskIntoConstraints만 false로 설정하는 경우 아래 사진처럼 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...'




📚 문서

구현 구조와 동작 원리를 설명하는 아키텍처 문서는 docs/architecture에 있다.

  • 전체 아키텍처 · 한글 입력 로직 · 자동완성 로직 · 삭제와 실행취소 로직 · 한영 통합 키보드 · 성능 고려 사항



📊 다이어그램

전체 구조

아래 상세 다이어그램들이 어떻게 연결되는지 나타낸 최상위 구조이다. 자동완성을 포함한 각 영역의 동작 원리는 docs/architecture 문서에 정리되어 있다.

%%{
  init: {
    "theme": "default",
    "fontFamily": "monospace"
  }
}%%
flowchart TB
    subgraph VCA["ViewController · InputAdapter 구조"]
        direction LR
        FinalVC["언어별 ViewController<br/>(한글 · 영어 · 한영 통합)"] -->|상속| BaseVC["BaseKeyboardViewController"]
        FinalVC -->|입력 위임| Adapter["InputAdapter<br/>(한글 · 영어)"]
    end

    subgraph HD["한글 입력 도메인 구조"]
        Domain["HangeulCompositionState<br/>Processor · Automata"]
    end

    subgraph LO["키보드 레이아웃 구조"]
        Layout["KeyboardView<br/>*KeyboardLayoutProvider 구현 View"]
        Toolbar["SuggestionBarView<br/>(후보 · undo/redo · 클립보드 버튼)"]
        ClipboardPanel["ClipboardHistoryPanelView"]
    end

    subgraph GS["제스처 구조"]
        Gesture["TextInteractionGestureController<br/>SwitchGestureController"]
    end

    subgraph BT["키보드 버튼 구조"]
        Button["BaseKeyboardButton 계열<br/>(TextInteractable)"]
    end

    SuggestionCoord["SuggestionSelectionCoordinator"]
    ClipboardCoord["ClipboardHistoryCoordinator"]
    DeletionCoord["TextDeletionCoordinator"]
    UndoCoord["UndoRedoCoordinator"]
    Suggestion["자동완성<br/>SuggestionController · 예측 엔진"]
    ClipboardStore["클립보드 저장소<br/>ClipboardHistoryStore · ClipboardImageStore"]
    Host["호스트 앱 텍스트 필드<br/>(textDocumentProxy)"]

    Adapter -->|조합 위임| Domain
    Adapter -->|현재 키보드 View 제공| Layout
    Layout -->|버튼 배치| Button
    Layout -->|상단 툴바| Toolbar
    Layout -->|클립보드 패널| ClipboardPanel
    Button -->|UIAction| BaseVC
    Toolbar -->|후보 탭 · undo/redo · 패널 열기| SuggestionCoord
    ClipboardPanel -->|항목 탭 · 편집| ClipboardCoord
    SuggestionCoord -->|Host: 삽입 · 후보 갱신 · undo/redo| BaseVC
    ClipboardCoord -->|Host: 삽입 · 자판 갱신| BaseVC
    SuggestionCoord -->|후보 선택 · 학습| Suggestion
    ClipboardCoord -->|기록 조회 · 저장| ClipboardStore
    Gesture -->|삭제 드래그 · 길게 누르기| BaseVC
    BaseVC -->|삭제 · 반복 · 드래그 위임| DeletionCoord
    DeletionCoord -->|Host: open 삭제 메서드 · 훅| BaseVC
    BaseVC -->|기록 · 확정 · 적용 위임| UndoCoord
    UndoCoord -->|Host: 쓰기 · 갱신| BaseVC
    BaseVC -->|터치/드래그| Gesture
    BaseVC -->|후보 조회 · 학습| Suggestion
    BaseVC -->|insert/delete| Host
Loading

ViewController · InputAdapter 구조

%%{
  init: {
    "theme": "default",
    "fontFamily": "monospace",
    "elk": {
        "mergeEdges": false,
        "nodePlacementStrategy": "BRANDES_KOEPF",
        "forceNodeModelOrder": false,
        "considerModelOrder": "NODES_AND_EDGES"
    },
    "class": {
        "hideEmptyMembersBox": true
    }
  }
}%%
classDiagram
direction LR
    %% ViewController & InputAdapter
    namespace ParentKeyboardViewController {
      class BaseKeyboardViewController
      class HangeulKeyboardCoreViewController
      class EnglishKeyboardCoreViewController
      class HangeulEnglishKeyboardCoreViewController
    }

    namespace FinalKeyboardViewController {
      class HangeulKeyboardViewController
      class EnglishKeyboardViewController
      class HangeulEnglishKeyboardViewController
    }

    namespace KeyboardInputAdapter {
      class HangeulKeyboardInputAdapter
      class EnglishKeyboardInputAdapter
    }

    class PrimaryKeyboardRepresentable:::SYKeyboard_primary { <<protocol>> }
    class SymbolKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class NumericKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }
    class TenkeyKeyboardLayoutProvider:::SYKeyboard_primary { <<protocol>> }

    BaseKeyboardViewController --> PrimaryKeyboardRepresentable: Association
    BaseKeyboardViewController *-- SymbolKeyboardLayoutProvider: Composition
    BaseKeyboardViewController *-- NumericKeyboardLayoutProvider: Composition
    BaseKeyboardViewController *-- TenkeyKeyboardLayoutProvider: Composition
    BaseKeyboardViewController *-- SuggestionSelectionCoordinator: Composition
    BaseKeyboardViewController *-- ClipboardHistoryCoordinator: Composition
    BaseKeyboardViewController *-- TextDeletionCoordinator: Composition
    BaseKeyboardViewController *-- UndoRedoCoordinator: Composition

    BaseKeyboardViewController <|-- HangeulKeyboardCoreViewController: Inheritance
    BaseKeyboardViewController <|-- EnglishKeyboardCoreViewController: Inheritance
    BaseKeyboardViewController <|-- HangeulEnglishKeyboardCoreViewController: Inheritance
    HangeulKeyboardCoreViewController <|-- HangeulKeyboardViewController: Inheritance
    EnglishKeyboardCoreViewController <|-- EnglishKeyboardViewController: Inheritance
    HangeulEnglishKeyboardCoreViewController <|-- HangeulEnglishKeyboardViewController: Inheritance

    HangeulKeyboardCoreViewController *-- HangeulKeyboardInputAdapter: Composition
    EnglishKeyboardCoreViewController *-- EnglishKeyboardInputAdapter: Composition
    HangeulEnglishKeyboardCoreViewController *-- HangeulKeyboardInputAdapter: Composition
    HangeulEnglishKeyboardCoreViewController *-- EnglishKeyboardInputAdapter: Composition
    HangeulEnglishKeyboardCoreViewController *-- HangeulEnglishKeyboardModeCoordinator: Composition

    HangeulKeyboardInputAdapter --> PrimaryKeyboardRepresentable: Association
    EnglishKeyboardInputAdapter --> PrimaryKeyboardRepresentable: Association

    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
    %% Hangeul Input Domain
    namespace HangeulInputDomain {
      class HangeulCompositionState
      class NaratgeulProcessor
      class CheonjiinProcessor
      class DubeolsikProcessor
      class HangeulAutomata
    }

    class HangeulProcessable:::SYKeyboard_primary { <<protocol>> }
    class HangeulAutomataProtocol:::SYKeyboard_primary { <<protocol>> }

    HangeulKeyboardInputAdapter *-- HangeulCompositionState: Composition
    HangeulKeyboardInputAdapter *-- HangeulProcessable: Composition
    HangeulCompositionState --> HangeulProcessable: Association

    HangeulProcessable <|.. NaratgeulProcessor: Implementation
    HangeulProcessable <|.. CheonjiinProcessor: Implementation
    HangeulProcessable <|.. DubeolsikProcessor: Implementation

    HangeulProcessable --> HangeulAutomataProtocol: Association
    HangeulAutomataProtocol <|.. HangeulAutomata: 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 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
    NormalKeyboardLayoutProvider <|.. StandardKeyboardView: 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 Gesture
    namespace KeyboardGestureController {
      class TextInteractionGestureController
      class SwitchGestureController
    }

    namespace KeyboardGestureProtocol {
      class SwitchGestureHandling
    }

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

    BaseKeyboardViewController *-- TextInteractionGestureController: Composition
    BaseKeyboardViewController *-- SwitchGestureController: Composition
    TextInteractionGestureController --> TextInteractable: Association
    SwitchGestureController --> SwitchGestureHandling: Association

    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 LanguageSwitchButton
      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 <|-- LanguageSwitchButton: 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

BaseKeyboardButton 계열이 아닌 ChevronButton(UIButton), SuggestionButtonView, SuggestionActionButtonView, 버튼 내부 구성 뷰 ButtonBackgroundView·ButtonShadowView(UIView)는 위 다이어그램에 포함하지 않았다.



📱 주요 기능

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



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



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



  1. 영어 키보드
    대중적인 영어(QWERTY) 키보드입니다.
한국어 영어
쿼티 쿼티 - 영어



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



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



  1. 키보드 툴바
    키보드 상단 툴바에서 자동완성 문구, 클립보드 기록, Undo/Redo를 사용할 수 있습니다. 클립보드 기록과 Undo/Redo는 메인 앱의 '키보드 툴바 설정'에서 켜고 끌 수 있습니다.
    • 자동완성 문구: 입력한 단어에 맞는 자동완성 문구를 추천합니다. 커서를 옮기면 커서 앞 단어를 기준으로 추천하고, 후보를 누르면 커서 앞 부분만 바꿉니다. 후보가 많으면 좌우로 스크롤해 최대 10개까지 볼 수 있고, 학습된 후보는 길게 눌러 자동완성에서 삭제할 수 있습니다. 삭제할 수 있는 후보는 조금 더 굵게 표시됩니다.
    • 클립보드 기록: 복사한 텍스트를 클립보드 버튼으로 붙여넣을 수 있습니다. '이미지도 기록'을 켜면 복사한 이미지도 저장하고, 탭하면 클립보드로 복원합니다. 기록은 메인 앱의 클립보드 기록 화면에서 고정·편집·삭제할 수 있습니다.
    • Undo/Redo: 입력을 되돌리거나 다시 실행할 수 있습니다.
한국어 영어
자동완성 자동완성 - 영어
클립보드 기록 클립보드 기록 - 영어



  1. 한영 통합 키보드
    한글 키보드와 영어 키보드를 한영 전환 버튼으로 오가는 단일 키보드입니다.
한국어 영어
한영 키보드(한글) 한영 키보드(한글) - 영어
한영 키보드(영어) 한영 키보드(영어) - 영어



  1. 수식 결과 표시
    입력 중인 수식이나 선택한 수식의 계산 결과를 자동완성 후보로 보여줍니다.
한국어 영어
수식 결과 표시 수식 결과 표시 - 영어



  1. 다양하고 디테일한 키보드 설정
    길게 누르기 동작, 커서 이동, 키보드 높이 및 한 손 키보드 너비 조절, 숫자 행 표시, 키보드 툴바(Undo/Redo · 클립보드 기록 · 이미지도 기록) 등 사용자의 편의에 맞게 키보드 설정이 가능합니다.
한국어 영어
메인 앱 1 메인 앱 1 - 영어
메인 앱 2 메인 앱 2 - 영어
메인 앱 3 메인 앱 3 - 영어



  1. 오픈소스 라이선스 고지
    메인 앱 설정 화면 하단의 '오픈소스 라이선스' 버튼에서 사용 중인 SPM 의존성의 라이선스 전문을 확인할 수 있습니다.
한국어 영어
오픈소스 라이선스 1 오픈소스 라이선스 1 - 영어
오픈소스 라이선스 2 오픈소스 라이선스 2 - 영어



About

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

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages