はじめに
iOSで日本語の予測変換を実装したいと思い、ちょっと調べてたらMozc-for-iOSを発見しました!!
最終更新が8年前だったので使えないかなと思いましたが、ちゃんと使うことができたので記事にしておきます。
先に言っときますが、大変です笑
単純作業が続きます笑
手順
Python2の環境構築
ここの解説は飛ばします。
私はPython 2.7.18を使用しました。
クローン
cd ディレクトリの移動
git clone https://github.com/kishikawakatsumi/Mozc-for-iOS.git
生成
最後に「INFO: Done」が出力されていれば成功です。
cd Mozc-for-iOS/src
python build_mozc.py gyp
xcodeprojの修正
ここがまじ大変です。
71個のxcodeprojを修正していきます。
以下の画像のように「src」の中をxcodeproj
で絞り込むと楽です。
修正する内容は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のバージョンは人によって違うかも?)
$(ARCHS_UNIVERSAL_IPHONE_OS)
① プロジェクトを選択します
② ターゲットを選択します(プロジェクト内にある全てのターゲットが対象です)
③ 「Bundle Settings」を選択します
④ Architecturesが$(ARCHS_UNIVERSAL_IPHONE_OS)
であればStandaed Architectures (arm64)
に変更します
これを71個のxcodeprojファイルをすべて修正します。
私は全て修正するのに1時間ほどかかりました。
コンパイル
python build_mozc_ios.py
「** BUILD SUCCEEDED **
」で終わればコンパイル成功です。
プロジェクトの作成
「mozc-for-ios-sample」という名前のプロジェクトを作成しました。
「mozc-for-ios-sample」のフォルダの中に「Mozc-for-iOS」を移動します。
Link Binary With Librariesの設定
① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Phases」を選択します
④ 「Link Binary With Libraries」のプラスボタンを押します
⑤ 「Add Other...」を選択します
⑥ 「Add Files...」を選択します
Mozc-for-iOS → src → out_ios → Release-iphoneos
拡張子が.a
のものを全て選択します。
選択できたら右下にある「Open」を選択します
Header Search Pathsの設定
① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Settings」を選択します
④ Header Search Paths
と検索します
⑤ 「Header Search Paths」の欄をダブルクリックします
以下の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 |
Library Search Pathsの設定
こちらは手順通りにやっていれば自動で設定されるはずですが念のため、確認します。
以下の画像のようになっていればOK
Objective-C関係?のファイルを作成
注意
Objective-Cはよく分からないのでVSCodeでファイルを作ります
以下のファイルが必要になります
- InputCandidate.h
- InputCandidate.m
- InputManager.h
- InputManager.mm
InputCandidate.h | InputCandidate.m | InputManager.h | InputManager.mm |
---|---|---|---|
作成したファイルをXcodeで読み込みます。
① フォルダ上で右クリックします
② 「Add Files to "プロジェクト名"...」を選択します
③ 先ほど作成したファイルを選択します
④ 「Add」を選択します
以下のアラートが出たら「Create Bridging Header」を選択します
Mozcを呼び出すコードを実装
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
#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
#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
#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で使えるようにする
#import "InputManager.h"
#import "InputCandidate.h"
使い方
View
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
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)")
}
}
サンプルアプリの動画
注意
実機でのみ動作します
おわり
こんなに簡単に日本語変換が実装できて嬉しいです!