4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Qiita全国学生対抗戦Advent Calendar 2022

Day 23

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

Last updated at Posted at 2022-12-23

はじめに

iOSで日本語の予測変換を実装したいと思い、ちょっと調べてたらMozc-for-iOSを発見しました!!
最終更新が8年前だったので使えないかなと思いましたが、ちゃんと使うことができたので記事にしておきます。

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

手順

Python2の環境構築

ここの解説は飛ばします。
私はPython 2.7.18を使用しました。

スクリーンショット 2022-12-23 17.11.22.png

クローン

ターミナル
cd ディレクトリの移動
git clone https://github.com/kishikawakatsumi/Mozc-for-iOS.git

スクリーンショット 2022-12-23 17.12.58.png

生成

最後に「INFO: Done」が出力されていれば成功です。

ターミナル
cd Mozc-for-iOS/src
python build_mozc.py gyp

スクリーンショット 2022-12-23 17.15.51.png

xcodeprojの修正

ここがまじ大変です。
71個のxcodeprojを修正していきます。
以下の画像のように「src」の中をxcodeprojで絞り込むと楽です。
スクリーンショット 2022-12-23 17.21.31.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

$(ARCHS_UNIVERSAL_IPHONE_OS)

① プロジェクトを選択します
② ターゲットを選択します(プロジェクト内にある全てのターゲットが対象です)
③ 「Bundle Settings」を選択します
④ Architecturesが$(ARCHS_UNIVERSAL_IPHONE_OS)であればStandaed Architectures (arm64)に変更します
スクリーンショット 2022-12-23 17.34.15.png

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

コンパイル

ターゲット
python build_mozc_ios.py

** BUILD SUCCEEDED **」で終わればコンパイル成功です。
スクリーンショット 2022-12-23 18.53.22.png

プロジェクトの作成

「mozc-for-ios-sample」という名前のプロジェクトを作成しました。
スクリーンショット 2022-12-23 18.56.38.png

「mozc-for-ios-sample」のフォルダの中に「Mozc-for-iOS」を移動します。
スクリーンショット 2022-12-23 18.58.15.png

Link Binary With Librariesの設定

① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Phases」を選択します
④ 「Link Binary With Libraries」のプラスボタンを押します
スクリーンショット 2022-12-23 18.59.58.png

⑤ 「Add Other...」を選択します
⑥ 「Add Files...」を選択します
スクリーンショット 2022-12-23 19.04.40.png

Mozc-for-iOS → src → out_ios → Release-iphoneos
拡張子が.aのものを全て選択します。
選択できたら右下にある「Open」を選択します
スクリーンショット 2022-12-23 19.07.08.png

以下のようになっていればOKです。
スクリーンショット 2022-12-23 19.10.40.png

Header Search Pathsの設定

① プロジェクトを選択します
② ターゲットを選択します
③ 「Build Settings」を選択します
Header Search Pathsと検索します
⑤ 「Header Search Paths」の欄をダブルクリックします
スクリーンショット 2022-12-23 19.12.30.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

Library Search Pathsの設定

こちらは手順通りにやっていれば自動で設定されるはずですが念のため、確認します。
以下の画像のようになっていればOK
スクリーンショット 2022-12-23 19.19.45.png

Objective-C関係?のファイルを作成

注意
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 スクリーンショット 2022-12-23 19.42.16.png スクリーンショット 2022-12-23 19.44.25.png スクリーンショット 2022-12-23 19.43.45.png

作成したファイルをXcodeで読み込みます。
① フォルダ上で右クリックします
② 「Add Files to "プロジェクト名"...」を選択します
スクリーンショット 2022-12-23 19 45 33
③ 先ほど作成したファイルを選択します
④ 「Add」を選択します
スクリーンショット 2022-12-23 19 47 44

以下のアラートが出たら「Create Bridging Header」を選択します
スクリーンショット 2022-12-23 20 04 05

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

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

#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

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)")
    }
}

サンプルアプリの動画

RPReplay_Final1671795982_MP4_AdobeExpress

注意
実機でのみ動作します

おわり

こんなに簡単に日本語変換が実装できて嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?