Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

iOSDC 2024-08-21

Tatsumi0000
August 25, 2024
93

iOSDC 2024-08-21

iOSDC2024 JAPAN のルーキーズLTで発表した「InputMethodKitとTCAを使ったmacOS上で動作するIMEの開発」です。

公式サイト: https://iosdc.jp/2024/
プロポーザル: https://fortee.jp/iosdc-japan-2024/proposal/8910998f-169e-4181-a65a-c176ae475362
実際のコード: https://github.com/Tatsumi0000/Raelize

Tatsumi0000

August 25, 2024
Tweet

Transcript

  1. ղܾࡦ • IMKInputControllerͷίʔυ͕ංେԽ • The Composable ArchitectureʢTCAʣΛಋೖ͠ղܾ • ༧ଌม׵ͷϩδοΫͷΈΛݕূ͠ʹ͍͘ •

    ϚϧνϞδϡʔϧߏ੒ʹͯ͠༧ଌม׵ͷΈΛݕূ͢ΔσόοάΞϓϦΛ։ൃ • ࡞ͬͨIMEΛಛఆͷσΟϨΫτϦʹίϐʔͯ͠ಈ࡞ݕূ • fastlaneΛ࢖ͬͨࣗಈԽ 6
  2. ࣮૷ʛInputMethodKit • IMKServer • ೖྗϝιο ド ΁ͷΫϥΠΞϯτ઀ଓΛ؅ཧ͢ΔΫϥε • IMKCandidates •

    ิ׬΢Οϯυ΢Λ؅ཧ͢ΔΫϥε • IMKInputController • ςΩετೖྗΛ੍ޚ͢ΔΫϥεɻ͜ͷΫϥεΛܧঝ͠IMEͷϩδοΫ Λ࣮૷ 8
  3. ࣮૷ʛState 1@ObservableState 2public struct State: Equatable { 3 /// IME

    ͷೖྗϞʔυ 4 var raelizeState: RaelizeState 5 /// Ϣʔβʹදࣔ͢Δิ׬ީิҰཡ 6 var candinates: [String] = [] 7 /// Ϣʔβ͕ೖྗ͍ͯ͠Δจࣈ 8 var inputWord: String = "" 9 /// ୯ޠϦετͷϑΝΠϧ໊ 10 var fileName: String = "" 11 /// Ϣʔβ͕બ୒ͨ͠ิ׬ީิ 12 var insertText: String = "" 13 /// Ϣʔβ͕બ୒ͨ͠ิ׬ީิͷจࣈྻ 14 var selectedWord: String = "" 15 /// IME ʹରͯ͠Ϣʔβ͕ىͨ͜͠Πϕϯτ 16 var candidateEvent: NSEvent? = nil 17} IMEͷStateྫ IMEͷঢ়ଶΛ؅ཧ
  4. ࣮૷ʛAction 1public enum Action: Equatable { 2 /// ૢ࡞ΩʔʢEnterɺ໼ҹͳͲʣ 3

    case operationEventKey(NSEvent) 4 /// ೖྗͨ͠จࣈ 5 case inputWord(String) 6 /// ݕࡧͨ͠จࣈ 7 case candinates([String]?) 8 /// ೖྗΛ֬ఆ͢Δจࣈ 9 case insertText(String) 10 /// ิ׬΢Οϯυ΢Ͱબ୒ͨ͠จࣈ 11 case selectedWord(String) 12 /// StateΛॳظԽ 13 case resetState(RaelizeState) 14 /// RaelizeStateΛ੍ޚ 15 case handleRaelizeState(NSEvent) 16} IMEͷActionྫ IMEʹΑΔૢ࡞Λఆٛ
  5. ࣮૷ʛॳظԽ 1@objc(RaelizeIMKController) 2public class RaelizeIMKController: IMKInputController { 3 4 private

    let candidates: IMKCandidates 5 private let store: StoreOf<RaelizeIMKReducer> 6 7 public override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { 8 self.candidates = IMKCandidates( 9 server: server, panelType: kIMKSingleColumnScrollingCandidatePanel) 10 self.store = Store( 11 initialState: RaelizeIMKReducer.State(raelizeState: .inputMode), 12 reducer: { RaelizeIMKReducer() }) 13 14 super.init(server: server, delegate: delegate, client: inputClient) 15 16 observe { 17 if self.store.candinates.isEmpty || self.store.inputWord.isEmpty { 18 self.candidates.hide() 19 } else { 20 self.candidates.update() 21 self.candidates.show() IMKInputControllerΛ ܧঝͨ͠Ϋϥεͷ ΠχγϟϥΠβʢҰ෦লུʣ
  6. ࣮૷ʛActionྫ 1public override func handle(_ event: NSEvent!, client sender: Any!)

    -> Bool { 2 guard let event = event else { return false } 3 4 self.store.send(.handleRaelizeState(ev ent)) 5 6 switch self.store.raelizeState { 7 case .neutralMode: 8 return false 9 case .inputMode, .operationMode: 10 return true 11 } 12} handleϝιου಺Ͱͷॲཧ ϢʔβͷΩʔೖྗʹΑͬͯ ͜ͷϝιου͕ݺ͹ΕΔ
  7. ·ͱΊ • ໰୊ • IMKInputControllerͷίʔυ͕ංେԽ • ղܾࡦ • TCAΛಋೖ͠ϩδοΫΛҠৡ •

    ݁Ռ • ίʔυͷݟ௨͕͠ྑ͘ͳΓɺҠ২ੑͱςελϏϦςΟ͕޲্ 18
  8. ࣮૷ʛkeyCode ͷม׵ 1public struct KeyEvent { 2 3 /// NSEvent.keyCode

    4 private let keyCode: Int 5 6 public init(keyCode: Int) { 7 self.keyCode = keyCode 8 } 9 10 /// NSEvent.keyCodeʹରԠ͢Δ໊લΛఆٛ 11 public enum EventName: Int { 12 case enter = 36 13 case backspace = 51 14 case upArrow = 126 15 case downArrow = 125 16 case tab = 48 17 case undefined = -1 18 } 19 20 /// keyCodeΛΩʔͷ໊લʹม׵ 21 public var eventName: EventName { 22 return EventName(rawValue: keyCode) ?? .undefined 23 } 24} KeyEventߏ଄ମΛ࡞੒
  9. ࣮૷ʛkeyCode ͷ࢖͍ํ 1public func reduce(into state: inout State, action: Action)

    -> Effect<Action> { 2 switch action { 3 case .operationEventKey(let event): 4 let keyEvent = KeyEvent(keyCode: Int(event.keyCode)) 5 state.raelizeState = .operationMode 6 switch keyEvent.eventName { 7 case .enter: 8 let text = state.candinates.isEmpty ? state.inputWord : state.selectedWord 9 if text.isEmpty { 10 return .send(.resetState(.neutralMode)) 11 } 12 return .run(operation: { send in 13 await send(.insertText(text)) 14 }) 15 case .upArrow, .downArrow, .tab: 16 state.candidateEvent = event 17 return .none 18 case .backspace: 19 state.inputWord.removeLast() 20 let word = state.inputWord 21 return .publisher({ 22 wordListFileUseCase.searchWordList(word: word) 23 .map({ Action.candinates($0) }) 24 }) TCAͷreduceϝιου಺Ͱ Actionܦ༝Ͱड͚औͬͨ NSEvent.keyCodeΛม׵