Xcode 9でApple Watch版とiPhone版両方のアプリを同時にデバッグする方法

Xcode 9でターゲットを選んで実行すると、iPhone版またはApple Watch版それぞれのアプリをシミュレーター上で実行することができますが、困るのは両機でデータをやり取りしたい時にブレイクポイントが片方でしか効かず、手探りでしかデバッグできないことです。

しかし、次の方法を使うとApple WatchとiPhoneの両方でアプリを実行でき、デバッガも両方で機能します。

  1. XcodeのターゲットをApple Watch版にします。
  2. ビルドして実行(⌘R)します。
  3. Xcode上からではなくSimulator上のiPhoneアプリをタップして実行します。
  4. Xcodeに戻って、メニューからDebug > Attach to process > シミュレーターで今実行したばかりのiPhoneアプリを選びます

ポイントは「iPhone版は実行してからXcodeに付け足す」ことと「XcodeのターゲットはApple Watch版のままにしておく」ことです。

 

Apple WatchとiPhoneでデータをやり取りする方法については次回以降ご紹介します(お急ぎの方は直接ご連絡またはコメントください)。

Objective CアプリをSwiftに移行する方法 (3)

Swiftに書き換える際に最初分からなくて困ったのは、SwiftNSArrayArrayの両方のクラスが存在することでした。自動変換ツールSWIFTIFYを使ったのですが、自動変換では変換後のコードにエラーが出て使えなかった上、配列を必要としたのがTableViewControllerだったため、随所にエラーが現れ、その数の多さにしばらく頭を抱えてしまいました。

SWIFTIFY: https://objectivec2swift.com/#/home

結果的にArrayNSArray2つのクラスの違いはこうでした。

  • Arrayクラスは値型、NSArrayは参照型
  • NSArrayだったものをArrayにしたければ「let」で宣言。NSMutableArrayだったものをArrayにしたければ「var」で宣言。
  • ArrayNSArrayそれぞれ使えるメソッドが違うので、片方にしかないメソッドがどうしても必要な場合はそちらを使うしかない。

【事例その1

変換前

@property (nonatomic,strong) NSMutableArray *notes;

自動変換後

var notes : [Any] = []

手直し後

var notes : [NSManagedObject] = []

配列はvar notes: Array = Array()のように書くのだとてっきり思っていたので、自動変換後の式はあまりにシンプルで最初意味がわかりませんでした。[Any]は「中身はなんでもありの配列」ということですね。なんでもありはいいのですが、Objective Cではそれすらもわざわざ宣言したりしないので「違うな」と思いました。NSArrayでは、配列の中身のクラスがなんであるかは、そのクラスのメソッドが必要になるまで何も言う必要がないですよね。必要になったときに[(NSString*)[notes objectAtIndex:0] メソッド名]のようにキャストすればいい。自動変換したnotesというオブジェクトの中身はAnyなので、そこからさらにメソッドを呼ぶにはObjective Cにならってnotes[0] as! NSManagedObjectのようにしても良かったのですが、試行錯誤していくうちに、なんのことははない、中身は常にNSManagedObjectなんだから[NSManagedObject]で良いということで手直ししました。asでキャスティングが必要なのは配列に複数のクラスのオブジェクトが混在する場合ですね。そんな怖いことはしませんが。

あと、NSMutableArrayをそのままSwiftで使うことも考えたのですが、実際にはmutablecopyで複製して使っていたので、参照型のNSMutableArrayではなく、値型のArrayが良いということになりました。

【事例その2

変換前

NSString *string = [[_notes objectAtIndex:indexPath.row] valueForKey:@”textString”];

自動変換後

var string = notes[indexPath.row][“textString”] as? String

手直し後

let string : String = notes[indexPath.row].value(forKeyPath: “textString”) as? String ?? “”

Core Dataに保存されたデータをTableViewに表示するときの処理です。notesにはNSManagedObjectが入ってます。「textString」という名前は私が決めたものでattribute名です。データベースで言うところのフィールド名ですね(Core Dataはデータベースではないとあちこちに書かれていますが厳密な定義は入門者を混乱させるだけだと個人的には思っていて、最初はとにかく動けばいいと思います)。自動変換後の[“textString”]valueForKeyから来ていますが、Objective CvalueForKeyで呼び出していた値はSwiftではvalue(forKeyPath:)と微妙に異なる呼び出し方が必要だったので手直ししました。さらにnilが存在しうるオプショナル型(String?)ではなく、確実に値の存在するStringオブジェクトが欲しかったので、nilだったときは””を入れるように最後に?? “”を追加しました。

【事例その3

変換前

[_notes insertObject:objectToMove atIndex:toIndexPath.row];

自動変換後

notes.insert(objectToMove, at: to.row)

TableViewの行を並べ替えたときの処理です。objectToMoveは一時的に用意した移動するNSManagedObjectオブジェクト。Objective CtoIndexPathだったものはSwiftではtoに変わっています。英語圏の人にはswiftの方がより読みやすくなっているのでしょうが、英語の構文で考えていない日本人にはtoだけだと言葉が短すぎて分かりにくい。でも、TableViewDelegate標準のプロパティ名なので慣れるしかないですね。

【事例その4

変換前

[_notes removeObjectAtIndex:indexPath.row];

自動変換後

notes.remove(at: indexPath.row)

TableViewの列を削除したときに使います。これは見たまんまですね。removeObjectAtIndexremove(at:)と簡略化されています。同じSwift同士でもバージョンアップごとに言い回しがよく変わるので、私はあえて暗記しないことにしています。Xcodeのコードセンス機能で自動補完させてリターンキーで決定したほうが間違いがなくていいです。

【番外編】

変換前

for (int i = 0; i < _notes.count ; i++) {

自動変換後

for i in 0..<notes.count {

別候補

for note in notes {

直接Arrayの話ではありませんが、配列といえば必ずforループが必要になるので追記します。Swiftではforではなくfor inしか使えません。何番目の要素を処理しているかが大事な場合は自動変換のように整数iを使い、大事でない場合はnotesの各要素をnoteに代入する別候補の式が使えます。また、逆順にしたい場合は

for i in (0..<notes.count).reversed() {

また特定の範囲を飛び飛びに回したいときは

for i in stride(from: 0, to: 10, by: 2) {

のように記述します。上の式だとi0,2,4,6,8,10の順に変化していきます。

Objective CアプリをSwiftに移行する方法 (2)

2018-06-06 13-08-52 292e1718

私のアプリ「Sums Up」では正規表現を使ってテキストフィールド内の文字列から数字だけを検知して、その合計を表示するということをやっています。計算機とメモ帳を行ったり来たりする必要がこのアプリを使うとなくなります。

https://appsto.re/jp/FBd-6.i

訳あってこのアプリをObjective CからSwiftに完全移行したのですが、エラーは消えたものの問題が一つ発生してしまいました。アラビア語キーボードで数字が入力されると落ちてしまうのです。

正常に動いていたObjective Cのコード

大まかな流れとしてはまずpatternというNSStringオブジェクトに正規表現式を入れて、文字列全体から数字を検索します。

    NSString *pattern = [[NSString alloc]initWithFormat:@”(正規表現式は省略させてください)”,_thousandSeparatorForRegEx,_decimalCharacterForRegEx,_thousandSeparatorForRegEx];

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:NULL];

_thousandSeparatorForRegExの中身は@”,”か@”.”。これは地域によっては数字の千を1,000ではなく1.000と書くためです。

_decimalCharacterForRegExも同様の趣旨です。小数点をコンマ(,)で書く地域に対応させます。

for (int i = 0; i < paragraphs.count; i++) {

     NSString *thisLine = [paragraphs objectAtIndex:i];

     NSArray *words = [thisLine componentsSeparatedByString:@” “];

     NSMutableAttributedString *thisLineWithAttribution = [[NSMutableAttributedString alloc]init];

forループでパラグラフ、つまり一行ごとに切り分けた文字列をthisLineに格納し、さらに空白で分割してwordsという配列に入れました。thisLineWithAttributionは後で文字に色を付けるためのものですが今回のテーマとは関係ないので省略します。

     for (int i = 0 ; i < words.count ; i++) {

            NSArray *matches = [regex matchesInString:word options:0 range:NSMakeRange(0, [word length])];

            if (matches.count != 0) {

                for (int j = 0; j < matches.count; j++) {

                    NSTextCheckingResult *match = [matches objectAtIndex:j];

                    NSString *matchString = [word substringWithRange:match.range];

                    NSString *numberPart = [regex stringByReplacingMatchesInString:matchString options:0 range:NSMakeRange(0, [matchString length]) withTemplate:@”$3″];

                    double thisNumber = [[[[numberPart stringByReplacingOccurrencesOfString:_thousandSeparator withString:@””]stringByReplacingOccurrencesOfString:@”\n” withString:@””] stringByReplacingOccurrencesOfString:_decimalCharacter withString:@”.”]

                                         doubleValue];

さらにwordごとに巡回するforループを中に作ります。そして正規表現に当てはまるもの全てをmatchesに格納します。そしてmatchesの各要素を再度forループでmatchに格納して、ようやく数字部分の【文字列の】抽出が完了です。matchからmatchStringというNSStringに変換して、さらに$3、つまりテンプレートの三番目の要素のみを取り出します。テンプレートは例えば「-$1,234.56」という文字列があったときに

  • $1 = – (マイナス記号)
  • $2 = $ (通貨記号)
  • $3 = 1,234.56 (数字部分)

というように切り分けるようになっています。thisNumberには最後の数字部分からさらにコンマを取り除き、小数点をピリオド(.)に統一したものを文字列として代入します。

この方法で、日本における1,234.56も、ヨーロッパにおける1.234,56も、アラビア語圏における١ ٢ ٣ ٤ .٥ ٦も正しく「1234.56」に変換されていました。

Swift変換直後のコード

問題になったのは上のコードの最後の一行、thisNumberの部分です。Swiftでは当初次のように記述しました。

let thisNumber = Double(numberPart?.replacingOccurrences(of: thousandSeparator, with: “”).replacingOccurrences(of: “\n”, with: “”).replacingOccurrences(of: decimalCharacter, with: “.”) ?? “0”)!

やろうとしていることは上と同じでnumberPartというStringからthousandSeparatorを取り去って、小数点をピリオドで置き換えようというものです。最後についている

?? “0”

はXcodeの警告メッセージに従って加えた変更です。一番最後の

)!

も同様です。最初はエラーが消えたからまあいいかぐらいに考えていました。

 

Swift独自のコンセプト、?と!

しかし、Objective Cからやってきた私にはこの?と!が曲者でした。Swiftのインスタンスにオプショナル型(optional)とそうでない型の2つがあることはぼんやり知っており、Objective Cのようにnilをすんなり渡せないことは知っていました。だから、うっかりnilを渡さないために

例) let text = someString ?? “”

オプショナル型のsomeStringがもしnilだったら初期値””をtextに代入せよ

とか

例) let text2 = someString!

someStringは絶対にnilではないから強制的にアンラップ(unwrap)してオプショナル型を解除せよ

というように処理したのでした。実際、上のSwiftコードで1234567890は認識できていたのです。

※ちなみにただunwrapするだけならsomeString?で大丈夫です。この場合はnilが返ることがあるので要注意ですが

 

アラビア語キーボードへの対応

本記事冒頭のスクリーンショットのキーボード一番上の段を見てもらうと分かりますが、アラビア語キーボードにおける数字は我々が使っている数字とは異なります。1、2、3と9と0は似てますね。なるほど、アラビア語が由来だからアラビア数字というのかと思いましたが、どちらも呼び名がアラビア数字なのが困りものです。が、それはさておき。

上述のthisNumberの式の最後で!で強制アンラップすると、アラビア語キーボードの数字のときだけクラッシュしてしまうということが、ユーザーさんからの報告で分かりました。

では!をやめて、thisNumberをオプショナル型にすればいいのでは、と思ったのですが、それも問題がありました。一旦Doubleに変換したthisNumberを後で全て足し算するために再びString型の変数に変換しなければならないのですが、

String(format: format, thisNumber)

この式がthisNumberがオプショナル型だと処理を受け付けてくれないのです。

 

解決策

結局次のように書き換えることで解決しました。

let thisNumberReplacedThousandAndDecimal =                                 numberPart?.replacingOccurrences(of: thousandSeparator, with: “”).replacingOccurrences(of: “\n”, with: “”).replacingOccurrences(of: decimalCharacter, with: “.”)

var thisNumber : Double = 0.0

if let thisNumberReplacedThousandAndDecimal = thisNumberReplacedThousandAndDecimal {

     let Formatter: NumberFormatter = NumberFormatter()

     Formatter.locale = NSLocale(localeIdentifier: “EN”) as Locale?

     let final = Formatter.number(from: thisNumberReplacedThousandAndDecimal)

     if final != 0 {

          thisNumber = Double(truncating: final!)

     }

}

つまりアラビア語キーボードの数字をUSロケールの1234567890に置き換えてから強制案ラップするというやりかたです。

 

実は全角数字でも同じ処理が必要だった

あとから分かったことですが、日本語キーボードで全角数字を入れた場合でも、この修正を加えないとやはり落ちてしまうのでした。正規表現上は数字として拾い上げられるが、数字には変換できない文字種が複数あるということですね。

最初は解決策がなかなかみつからず、いっそ0〜9までの文字を一個ずつ置き換えて取り急ぎのクレーム対策としようかとも思ったのですが、世界中で使われている自分のアプリが一体何語で使われているか見当もつきませんから、クレームのたびにアップデートするのはやはり得策ではない。頑張って正しい処理方法が見つけることができてよかったです。

それにしても、日本のユーザーさんからクレームが来る前に、アラビア語圏の方が先に教えてくれて本当にラッキーでした(その方の優しい口調にも救われました)。いずれにせよ、自分のアプリが世界中で使われていると思うと嬉しいですね。これからもがんばります!

Objective CアプリをSwiftに移行する方法 (1)

私の開発したメモ帳のような足し算アプリ「Sums Up」をObjective CからSwiftに書き換えた時の体験談です。

https://appsto.re/jp/FBd-6.i

【大まかな流れ】

・.hファイル、.mファイルに相当する.swiftファイルを追加する。

・変換ツールを使って.h、.mファイル内のコードをSwiftに書き換え、.swiftファイルに貼り付ける

・エラーが出た場合は手直しする

・必要があればStoryboardと.swiftファイル内のIBOutlet、IBActionを繋ぎ直す

・AppDelegateを新規のswiftプロジェクトからコピーする

・.h、.mファイルの中身を全てコメントアウトして無効にし、.mainと移行中に発生したbridging-header.hファイルを削除する。

【もう少し具体的な手順】

・Fileメニューから > New > File > Cocoa Touch Class > Nextの順に進む

・Class = .mファイルと同じ名前にする

Subclass of = .mファイルと同じクラスにする

Language: Swift

・Nextを押すと「Would you like to configure an Objective-C bridging header?」と訊かれるので、Yesを選びます。これはObjective CのプロジェクトにSwiftファイルを追加して、両言語を混在させたいときに両者の間の橋渡しをするヘッダーファイルです。今回は完全にSwiftに移行したいので後で削除するのですが、移行途中、つまりまだObjective CベースのViewControllerが他に残っていても、アプリを走らせてテストしたかったので追加しました。

・コードをObjective CからSwiftに書き換えるのには以下のサイトを使いました。

https://objectivec2swift.com/#/converter/

無料で変換できるのは1000文字までで、.hファイルは収まっても、mファイル丸ごとは無理でした。有料プランも考えたのですが、まず第一に私がケチなのと(笑)、後から分かったことですが、どの道手直しが必要になるので、コードを少しずつ(メソッドごとかもっと狭い範囲で)コピペして、このサイトとXcodeを行き来しました。他のツールも試しましたが、やり方が悪かったのか上手く行かず、途中で嫌になってやめました。

・全てのViewControllerをSwift化してからテストしようとすると、大量のコンパイルエラーを先に解決しなければならないのでお勧めしません。私の場合は合計で約200個のエラーを手作業で取り除かなければなりませんでした。

・比較的シンプルな作りのViewControllerをまず1つSwiftに書き換えて、Objective Cの方(.hと.m)をコメントアウトして(ショートカットは⌘/です)、プロジェクトを実行して動作を確認してから次のViewControllerに取り掛かるようにしました。この時、Objective CクラスからSwiftクラスを呼ぶにはObjective Cクラスのヘッダーに

#import “<#YourProjectName#>-Swift.h”

を追加します。逆にSwiftからObjective Cを呼ぶときは、YourProject-Bridging-Header.hに以下を加えます。

#import “YourObjectiveCClass.h”

出展: Stack Overflow

・AppDelegateをSwift化します。やりかたは

File > New > File > Cocoa Touch Class > Next

Class: AppDelegate

Subclass: UIResponder

Language: Swift

Nextを押す

これでAppDelegate.h、.mファイルをコメントアウトして上手く走ればOKですが、ダメな場合はSwiftで新規プロジェクトを作成して、そのAppDelegate.swiftからコピペするのが良いでしょう。

・元のAppDelegate.mに書き加えたメソッドなどもSwiftに書き換えてAppDelegate.swiftに書き加えます。

・AppDelegateがSwiftになったのでmain.mを削除します。

・全てのクラス、ViewControllerがSwiftに書き換えられたら、不要になったYourApp-Bridging-Header.hも削除します。


理屈ではこれでSwiftへの移行が完了するわけですが、エラーがなくなったとしても、Objective Cの時とは挙動が変わってしまっていることがあります。次回はそのことについて書きます。