【Swift】Mozc-for-iOSを使って日本語変換を実装してみた

# はじめに
iOSで日本語の予測変換を実装したいと思い、ちょっと調べてたら[Mozc-for-iOS](https://github.com/kishikawakatsumi/Mozc-for-iOS)を発見しました！！
最終更新が8年前だったので使えないかなと思いましたが、ちゃんと使うことができたので記事にしておきます。

先に言っときますが、大変です笑
単純作業が続きます笑

# 手順
### Python2の環境構築
ここの解説は飛ばします。
私はPython 2.7.18を使用しました。

https://retval.jp/blog/python2-install/

![スクリーンショット 2022-12-23 17.11.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/c320f132-2f23-686f-22de-16fb9392fc59.png)

### クローン
```:ターミナル
cd ディレクトリの移動
git clone https://github.com/kishikawakatsumi/Mozc-for-iOS.git
```
![スクリーンショット 2022-12-23 17.12.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/13063313-f942-f864-b694-6338fb6f25d3.png)

### 生成
最後に「INFO: Done」が出力されていれば成功です。
```:ターミナル
cd Mozc-for-iOS/src
python build_mozc.py gyp
```
![スクリーンショット 2022-12-23 17.15.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/2e50bf9c-06d9-914a-bdc0-73bcc9406e3a.png)

### xcodeprojの修正
ここがまじ大変です。
71個のxcodeprojを修正していきます。
以下の画像のように「src」の中を`xcodeproj`で絞り込むと楽です。
![スクリーンショット 2022-12-23 17.21.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/224f9cba-245b-7cb6-4e01-abf25ae67b2e.png)

修正する内容は2個しかありません。
- `macos10.8(SDK not found)`
- `$(ARCHS_UNIVERSAL_IPHONE_OS)`

これは以下の様に修正します。
#### macos10.8(SDK not found)
① プロジェクトを選択します
② ターゲットを選択します(プロジェクト内にある全てのターゲットが対象です)
③ 「Bundle Settings」を選択します
④ Base SDKが`macosx10.8`であれば`macOS 13.1`に変更します(macOSのバージョンは人によって違うかも？)
![スクリーンショット 2022-12-23 17.30.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/c0a6deea-afdc-7142-94d2-156897253567.png)

#### $(ARCHS_UNIVERSAL_IPHONE_OS)
① プロジェクトを選択します
② ターゲットを選択します(プロジェクト内にある全てのターゲットが対象です)
③ 「Bundle Settings」を選択します
④ Architecturesが`$(ARCHS_UNIVERSAL_IPHONE_OS)`であれば`Standaed Architectures (arm64)`に変更します
![スクリーンショット 2022-12-23 17.34.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/de329e5b-09b4-d108-30e0-27a2b47e406c.png)

これを71個のxcodeprojファイルをすべて修正します。
私は全て修正するのに1時間ほどかかりました。

### コンパイル
```:ターゲット
python build_mozc_ios.py
```
「`** BUILD SUCCEEDED **`」で終わればコンパイル成功です。
![スクリーンショット 2022-12-23 18.53.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/d9cfb014-259b-6d5d-8a83-e85b8883f2c4.png)

### プロジェクトの作成
「mozc-for-ios-sample」という名前のプロジェクトを作成しました。
![スクリーンショット 2022-12-23 18.56.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/402fff22-a60e-2d07-4bfe-e0b7d8e77511.png)

「mozc-for-ios-sample」のフォルダの中に「Mozc-for-iOS」を移動します。
![スクリーンショット 2022-12-23 18.58.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/d48a9894-abb2-9253-5865-e770e4c372f6.png)

### Link Binary With Librariesの設定
① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Phases」を選択します
④ 「Link Binary With Libraries」のプラスボタンを押します
![スクリーンショット 2022-12-23 18.59.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/eaddfab1-e9db-9a43-6e76-9d76b98d2bcd.png)

⑤ 「Add Other...」を選択します
⑥ 「Add Files...」を選択します
![スクリーンショット 2022-12-23 19.04.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/c48d70dc-7b24-8f9c-c206-6e1ecb509710.png)

Mozc-for-iOS → src → out_ios → Release-iphoneos
拡張子が`.a`のものを全て選択します。
選択できたら右下にある「Open」を選択します
![スクリーンショット 2022-12-23 19.07.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/56fbb474-9f79-f466-f10b-dd6829b29e1c.png)

以下のようになっていればOKです。
![スクリーンショット 2022-12-23 19.10.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/9e1116d4-b3cd-4b9c-7150-424dc6924425.png)

### Header Search Pathsの設定
① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Settings」を選択します
④ `Header Search Paths`と検索します
⑤ 「Header Search Paths」の欄をダブルクリックします
![スクリーンショット 2022-12-23 19.12.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/1979beea-df4d-09a2-b9b5-7649b2db5496.png)

|**以下の5つを設定する**|
|-|
|$(inherited)|
|/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include|
|$(PROJECT_DIR)/Mozc-for-iOS/src|
|$(PROJECT_DIR)/Mozc-for-iOS/src/out_ios/DerivedSources/Release/proto_out|
|$(PROJECT_DIR)/Mozc-for-iOS/src/third_party/protobuf/src|

以下の画像のようになっていればOK
![スクリーンショット 2022-12-23 19.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/398b013c-7dc0-4f64-f0ca-4cefe525d229.png)

# Library Search Pathsの設定
こちらは手順通りにやっていれば自動で設定されるはずですが念のため、確認します。
以下の画像のようになっていればOK
![スクリーンショット 2022-12-23 19.19.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/f2a2b94f-0bb0-2b5c-84c1-3ef0c0b37ced.png)

### Objective-C関係？のファイルを作成
:::note warn
**注意**
Objective-Cはよく分からないのでVSCodeでファイルを作ります
:::

以下のファイルが必要になります
- InputCandidate.h
- InputCandidate.m
- InputManager.h
- InputManager.mm

|InputCandidate.h|InputCandidate.m|InputManager.h|InputManager.mm|
|-|-|-|-|
|![スクリーンショット 2022-12-23 19.41.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/42fe35d1-4299-c925-e6bf-a09f2d0872ac.png)|![スクリーンショット 2022-12-23 19.42.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/6def2f44-9699-fc7c-844b-8d584dc56c6a.png)|![スクリーンショット 2022-12-23 19.44.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/4b4489f5-d8b0-c74a-5ba0-2fb28a9919cb.png)|![スクリーンショット 2022-12-23 19.43.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/3272f6b9-1027-55a2-f186-58875b28be2d.png)|

作成したファイルをXcodeで読み込みます。
① フォルダ上で右クリックします
② 「Add Files to "プロジェクト名"...」を選択します
<img width="492" alt="スクリーンショット 2022-12-23 19 45 33" src="https://user-images.githubusercontent.com/84154073/209323020-dca55ebf-98a0-4793-b73e-00e85e7687ee.png">
③ 先ほど作成したファイルを選択します
④ 「Add」を選択します
<img width="718" alt="スクリーンショット 2022-12-23 19 47 44" src="https://user-images.githubusercontent.com/84154073/209323018-fbe74b6b-0281-41cf-ade2-c3f5550c428d.png">

以下のアラートが出たら「Create Bridging Header」を選択します
<img width="387" alt="スクリーンショット 2022-12-23 20 04 05" src="https://user-images.githubusercontent.com/84154073/209325323-cf36f660-47e8-4412-b406-41ef44f9a707.png">

### Mozcを呼び出すコードを実装
#### InputCandidate.h
```InputCandidate.h
#import <Foundation/Foundation.h>

@interface InputCandidate : NSObject

@property (nonatomic) NSString *input;
@property (nonatomic) NSString *candidate;

- (id)initWithInput:(NSString *)input candidate:(NSString *)candidate;

@end
```

#### InputCandidate.m
```objc:InputCandidate.m
#import "InputCandidate.h"

@implementation InputCandidate

- (id)initWithInput:(NSString *)input candidate:(NSString *)candidate
{
    self = [super init];
    if (self) {
        _input = input;
        _candidate = candidate;
    }
    
    return self;
}

- (BOOL)isEqual:(id)object
{
    return ([object isKindOfClass:[InputCandidate class]] &&
            [self.input isEqualToString:[object input]] &&
            [self.candidate isEqualToString:[object candidate]]);
}

#define NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
#define NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (NSUINT_BIT - howmuch)))

- (NSUInteger)hash
{
    return NSUINTROTATE([_input hash], NSUINT_BIT / 2) ^ [_candidate hash];
}

@end
```

#### InputManager.h
```InputManager.h
#import <Foundation/Foundation.h>

@protocol InputManagerDelegate;

@interface InputManager : NSObject

@property (nonatomic, readonly) NSArray *candidates;
@property (nonatomic, weak) id delegate;

- (void)requestCandidatesForInput:(NSString *)input;

@end

@protocol InputManagerDelegate <NSObject>

- (void)inputManager:(InputManager *)inputManager didCompleteWithCandidates:(NSArray *)candidates;
- (void)inputManager:(InputManager *)inputManager didFailWithError:(NSError *)error;

@end
```

#### InputManager.mm
```obj-c++
#import "InputManager.h"
#import "InputCandidate.h"

#if !TARGET_IPHONE_SIMULATOR
#define USE_MOZC 1
#endif

#if USE_MOZC

#include <string>

using namespace std;

#include "composer/composer.h"
#include "composer/table.h"
#include "converter/conversion_request.h"
#include "converter/converter_interface.h"
#include "converter/segments.h"
#include "prediction/predictor_interface.h"
#include "engine/engine_factory.h"
#include "engine/engine_interface.h"

void MakeSegmentsForSuggestion(const string key, mozc::Segments *segments) {
    segments->Clear();
    segments->set_max_prediction_candidates_size(10);
    segments->set_request_type(mozc::Segments::SUGGESTION);
    mozc::Segment *seg = segments->add_segment();
    seg->set_key(key);
    seg->set_segment_type(mozc::Segment::FREE);
}

void MakeSegmentsForPrediction(const string key, mozc::Segments *segments) {
    segments->Clear();
    segments->set_max_prediction_candidates_size(50);
    segments->set_request_type(mozc::Segments::PREDICTION);
    mozc::Segment *seg = segments->add_segment();
    seg->set_key(key);
    seg->set_segment_type(mozc::Segment::FREE);
}

@interface InputManager ()

@property (nonatomic, readwrite) NSArray *candidates;
@property (nonatomic) NSOperationQueue *networkQueue;

@end

@implementation InputManager {
    scoped_ptr<mozc::EngineInterface> engine;
    mozc::ConverterInterface *converter;
    mozc::PredictorInterface *predictor;
}

- (id)init
{
    self = [super init];
    if (self) {
        engine.reset(mozc::EngineFactory::Create());
        converter = engine->GetConverter();
        CHECK(converter);
        predictor = engine->GetPredictor();
        CHECK(predictor);
    }
    
    return self;
}

- (void)requestCandidatesForInput:(NSString *)input
{
    mozc::commands::Request request;
    mozc::Segments segments;
    
    mozc::composer::Table table;
    mozc::composer::Composer composer(&table, &request);
    composer.InsertCharacterPreedit(input.UTF8String);
    mozc::ConversionRequest conversion_request(&composer, &request);
    
    converter->StartPredictionForRequest(conversion_request, &segments);
    
    NSMutableOrderedSet *candidates = [[NSMutableOrderedSet alloc] init];
    
    for (int i = 0; i < segments.segments_size(); ++i) {
        const mozc::Segment &segment = segments.segment(i);
        for (int j = 0; j < segment.candidates_size(); ++j) {
            const mozc::Segment::Candidate &cand = segment.candidate(j);
            [candidates addObject:[[InputCandidate alloc] initWithInput:[NSString stringWithUTF8String:segment.key().c_str()] candidate:[NSString stringWithUTF8String:cand.value.c_str()]]];
        }
    }
    
    converter->StartConversionForRequest(conversion_request, &segments);
    
    for (int i = 0; i < segments.segments_size(); ++i) {
        const mozc::Segment &segment = segments.segment(i);
        for (int j = 0; j < segment.candidates_size(); ++j) {
            const mozc::Segment::Candidate &cand = segment.candidate(j);
            [candidates addObject:[[InputCandidate alloc] initWithInput:[NSString stringWithUTF8String:cand.key.c_str()] candidate:[NSString stringWithUTF8String:cand.value.c_str()]]];
        }
    }
    
    self.candidates = candidates.array;
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.delegate inputManager:self didCompleteWithCandidates:self.candidates];
    });
}

@end

#else

@interface InputManager ()

@property (nonatomic, readwrite) NSArray *candidates;
@property (nonatomic) NSOperationQueue *networkQueue;

@end

@implementation InputManager

- (id)init
{
    self = [super init];
    if (self) {
        self.networkQueue = [[NSOperationQueue alloc] init];
        self.networkQueue.maxConcurrentOperationCount = 1;
    }
    
    return self;
}

- (void)requestCandidatesForInput:(NSString *)input
{
    [self.networkQueue cancelAllOperations];
    
    NSString *encodedText =[input stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *URL = [NSURL URLWithString:[NSString stringWithFormat:@"http://www.google.com/transliterate?langpair=ja-Hira%%7Cja&text=%@", encodedText]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0f];
    request.HTTPShouldUsePipelining = YES;
    
    [NSURLConnection sendAsynchronousRequest:request queue:self.networkQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (!connectionError) {
            NSMutableArray *candidates = [[NSMutableArray alloc] init];
            
            NSArray *results = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
            for (NSArray *result in results) {
                NSString *text = result.firstObject;
                NSArray *list = result.lastObject;
                for (NSString *candidate in list) {
                    [candidates addObject:[[InputCandidate alloc] initWithInput:text candidate:candidate]];
                }
                
                self.candidates = candidates;
                
                break;
            }
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.delegate inputManager:self didCompleteWithCandidates:self.candidates];
            });
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.delegate inputManager:self didFailWithError:connectionError];
            });
        }
    }];
}

@end

#endif
```

### Swiftで使えるようにする
```{プロジェクト名}-Bridging-Header.h
#import "InputManager.h"
#import "InputCandidate.h"
```

# 使い方
### View
```swift
import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        List {
            TextField("テキストを入力してください", text: $viewModel.text, axis: .vertical)
                .textFieldStyle(.roundedBorder)
                .padding()
                .onChange(of: viewModel.text) { value in
                    viewModel.requestCandidates(forInput: value)
                }
            ForEach(0..<viewModel.candidates.count, id: \.self) { index in
                Text(viewModel.candidates[index].candidate)
            }
        }
        .listStyle(.grouped)
    }
}
```

### ViewModel
```swift
import SwiftUI

final class ViewModel: NSObject, ObservableObject {
    @Published var text: String = ""

    @Published var candidates: [InputCandidate] = []

    private let manager = InputManager()

    override init() {
        super.init()

        setup()
    }

    private func setup() {
        manager.delegate = self
    }

    func requestCandidates(forInput input: String) {
        manager.requestCandidates(forInput: input)
    }
}

extension ViewModel: InputManagerDelegate {
    func inputManager(_: InputManager!, didCompleteWithCandidates candidates: [Any]!) {
        guard let candidates = candidates as? [InputCandidate] else { return }
        self.candidates = candidates
    }

    func inputManager(_: InputManager!, didFailWithError error: Error!) {
        print("📕: \(error.localizedDescription)")
    }
}
```

# サンプルアプリの動画
![RPReplay_Final1671795982_MP4_AdobeExpress](https://user-images.githubusercontent.com/84154073/209331552-90ee7b3b-2b93-4cd9-bb6c-bc574ffb8217.gif)

:::note warn
**注意**
実機でのみ動作します
:::

# おわり
こんなに簡単に日本語変換が実装できて嬉しいです！