はじめに
とうようです。この記事はIS17er Advent Calendarの21日目の記事です。
今回はかねてから開発していたFFMultiplierというお勉強ゲームアプリのiOS/Android両バージョンをリリースできたことを記念して、その開発の中で感じたどっちのどこらへんが開発しやすかったかしにくかったかみたいなところを徹底比較してみたいと思います。
アプリはこちら
今までも両方開発した人がその所感を述べるというのはよく行われていたと思うので今回はある機能を技術的にどう実現するかという観点にしぼって比較していければと思っています。
タイマー
Swift
Swiftでのタイマーは基本的にはTimer(旧NSTimer)
を使います。何秒かごとに同じ処理を実行したければ
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
こんなかんじ。#selector
が導入されてタイプミスも減りましたし、基本的にはスレッドなどを気にせずに安全に書けるのでとても便利です。
Java
Javaでも基本的にタイマーはTimer
クラスを使って実装します。しかし、Android timer
やJava Timer
をググった時のならびをみてもらうとわかりますが、Timer
へのTimerTask
クラスを渡す方法がいくつかあって初心者は面食らうかもしれません。
今回はゲームのカウントダウンだったので当初CountDownTimer
というそのまんまのクラスを使おうと思っていたのですが、よく調べてみるとこれは正確に1秒をはかってくれないという欠点があるらしく、Timer
に落ち着きました。クラスを別につくれば書き方自体は似ています。
timer.schedule(timerTask, 0, 1000);
ただAndroidの場合注意したいのはTimer
の処理が実行されるのが別スレッドで描画処理を書けないので処理の中からメインスレッドを呼び出さないと行けないという点です。この点でAndroidのタイマーは少し初心者にとって敷居が高いのかなと感じました。
グリッドレイアウト
iOS
今回みるグリッドレイアウトというのはCollectionViewのようなスクロールビューが背景に入ってしまわない、純粋なグリッドをつくる作り方です。
iOSでの実現方法としては
- AutoLayoutで地道にやる
- コードから計算して生成
- 新機能StackViewを利用する
- CoollectionViewでつくってスクロールしないようにする
あたりがあるでしょうか。今回はStackViewを採用しました。ただこの機能は後方互換性がなく、iOS9より前では動かないという所が少し欠点ではあると思います。ただ概ね自分の意図したように配置することができました。デフォルトが中央寄せかつ全体に引き伸ばすようになっているところがよいですね。
Android
Androidの場合はもちろんLinearLayoutを利用します。こちらは本当に初期からある機能なのでどんな機体でも動きますし、比率などの設定に関しても資料が多いので学習コストも低いところがいいですね。
またそうでなくてもGridLayoutというそのまんまの機能があるのでかなり楽に実現することができます。
中央寄せや親要素いっぱいに広げるというのは設定をするのは自分でということになりますが、この部分に関してはやはり先をいっていたAndroidに軍配があがりそうです。
関連付け
iOS
レイアウトファイル(つまりはストーリーボード)とコードのヒモ付はやはりiOSの簡単さに右に出るものはいないと言ってもいいかもしれませんね。厳密にやると、AutoLayoutがかかるタイミングやらコードとつながるタイミングをライフサイクルの中で考慮しなきゃいけないなど少し不自由なケースも出てきますが基本的に学習コストがとても低いというのは魅力です。
また複数のボタンに一つの@IBAction
を関連付けして、tagで処理を分けると言ったこともGUI上で出来てしまいます。
ただCustomView内のボタンの処理となってくると少し厄介です。というのも親となっているViewControllerから直接関連付けすることが出来ないので、ViewControllerの要素に影響を与えるような処理の場合少し工夫しなければ実現出来ません。パッと思いついたのでは
- Delegateをつくる
- ViewController自身をプロパティとして渡す
- 返り値の無いクロージャーを渡す
- Outletとして関連付けしてコードからタッチイベントをつける
僕の場合最近はもっぱら三番目の方法を好んで使っている気がします。すなわちCustomView内で
var action: (()->())!
という感じで宣言してインスタンス化する時に処理を渡してあげるという方法です。
Android
Androidでは基本的にRという一括管理するファイルがあって、xmlからidを登録してコード上でfindViewById
をつかって要素をとってくるという感じですね。
コードで書かなきゃいけない分、少し面倒なところもありますがその代わり適切に記述すれば比較的好きな時に紐付けすることができます。
また過去にはonClick
イベントは必ずsetOnClickListener
で行っていましたが、AndroidStudioが公式になってxmlからonClick
メソッドを設定することができるようになりました。
この分野に関しては本当にお互い一長一短といえると思います。
フォント
iOS
フォント、特に自分でデフォルトでは入っていない好きなフォントを使う時にですが、iOSであればプロジェクトに入れてbuild phaseのcopy bundle resourcesを設定すればコードからでもストーリーボードからでも使えるようになります。簡単ですね。
Android
Androidの場合カスタムフォントは少し厄介です。まずassetsに入れたいフォントを入れた後通常はフォントを変えたい要素のカスタムクラスを作らなければいけません。しかも何も考えずにやっていると使う度にフォントファイルがコピーされるという厄介な仕様なので、しっかりFontHolder
みたいなものを自作してTypeface
を管理しなければなりません。
そこでそれを解決するためのライブラリとしてCalligraphyというものがあります。これによってカスタムクラスなどをつくることなく簡単に管理することが出来るようになります。
ここだけは本当にiOSに寄せてほしいです...汗
アラート
iOS
アラートを出すにはUIAlertController
を使います。テキストフィールドなど簡単な要素であればコードから設定することが可能です。たとえばOK,Cancelボタン、およびテキストフィールド一個のアラートであれば
let alert = UIAlertController(title: "register name", message: "please set your username", preferredStyle: .alert)
alert.addTextField {
textField in
textField.placeholder = "user name"
}
alert.addAction(UIAlertAction(title: "OK", style: .default) {
_ in
let textfield = alert.textFields?.first
if let name = textfield?.text {
self.storage.set(name, forKey: "playername")
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
簡単ですね。
Android
AndroidはAlertDialog
ですね。ただテキストフィールドなどは用意されていないのでxmlをつくって適用してあげます。
LayoutInflater inflater = LayoutInflater.from(gameActivity);
View dialog = inflater.inflate(R.layout.input_dialog, null);
final EditText editText = (EditText)
dialog.findViewById(R.id.editNameText);
new AlertDialog.Builder(gameActivity).setTitle("please set your name").setView(dialog).setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
final String userName = editText.getText().toString();
sp.edit().putString("name", userName).commit();
}
}).show();
少し面倒は増えますが、その分自分の好きなものを追加して作れちゃうので自由度が高いといえるかもしれませんね。
PopupView的なもの
iOS
ポップアップ的なものは、デフォルトのものだと親となる要素がないといけないなど制約が大きいのでライブラリをつかってViewを上に表示する形式にするのが一番いいと思います。
というわけで僕はコチラを使いました。
Storyboardあるいはxibでレイアウトを用意してあげればかなり簡単に表示することができます。
Android
一方Androidでは色々と実現方法はあると思いますが、PopupWindowというまさにという機能があります。
これによって少し設定項目は多いですがしっかりやりたいことを実現することができます。
ですがこの項目はむしろAlertDialogをカスタマイズしてしまう方が簡単かもしれませんね...
Firebaseの扱い
基本的にはどちらも扱い方は同じですが多少扱いやすい使い方というものに言語の特性に応じた違いが現れています。例えば登録のときは
ref.child("hello").child(device_id).setValue(["id": id, "rank": rank as NSNumber], andPriority: -newScore.score)
このようにiOSではDictionaryを使って登録できます。一方AndroidはMap<String, Object>
を使えば同じように使えますがこれはハッシュ木で使われているもので純粋に標準とはいえないのでそのかわりに以下のようにクラスを定義して
@IgnoreExtraProperties
public class DatabaseScore {
public String id;
public int rank;
public DatabaseScore() {}
public DatabaseScore(String id, int rank) {
this.id = id;
this.rank = rank;
}
public int getId() {
return id;
}
public String getRank() {
return rank;
}
}
これのインスタンスを渡すことで変数名をキーに自動で変換して扱ってくれます。こんな感じで
ref.child("hello").child(id).setValue(databaseScore);
データベースを読み出してくるときはSwiftはobserve(.value, with: handler)
で、AndroidはaddValueEventListener(valueEventListener)
です。それぞれの特性が出ていますね。
登録したときと同じ形式で読み出すことが可能です。
Realmの扱い
Realmの扱い方に関してもFirebaseと似たような傾向が見られます。
単純な読み書きについて比較してみましょう。まずiOSでは以下のようになります。
import RealmSwift
final class Score: Object {
dynamic var date = NSDate(timeIntervalSince1970: 1)
dynamic var score: Int = 0
}
まずこれでモデル定義します。Realmの内部処理のために保存したいものにはdynamic
修飾子をつけるのでしたね。そして
let newScore = Score(value: ["date": NSDate(), "score": acceptedNum])
let realm = try! Realm()
try! realm.write {
realm.add(newScore)
}
このようにtry
で書き込みスレッドに入って追加すると
let scores = realm.objects(Score.self).sorted(byProperty: "score", ascending: false)
このようにobjects
というメソッドで保存したものを取り出すことができるのでした。
次に同じことをAndroidでやってみましょう。するとモデル・書き込み・読み込みは以下のようになります。
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import java.util.Date;
public class ScoreModel extends RealmObject {
private int score;
private Date date;
public void setScore(int score) {
this.score = score;
}
public void setDate(Date date) {
this.date = date;
}
public Date getDate() {
return date;
}
public int getScore() {
return score;
}
}
final ScoreModel scoreModel = new ScoreModel();
scoreModel.setScore(correctCnt * 10);
scoreModel.setDate(cal.getTime());
Realm realm = Realm.getDefaultInstance();
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.copyToRealm(scoreModel);
}
});
RealmResults<ScoreModel> results = realm.where(ScoreModel.class).findAllSorted("score", Sort.DESCENDING);
流れとしては同じなのですが、まずモデルにgetterとsetterを設定する必要があること、interfaceの中で非同期処理として書き込みをするということ、結果がRealmResultsという型で返ってくるところなどが特徴的かなと思います。
どちらがいいというのは難しいですが、Swiftの方が簡潔に書けるという印象がつよいですね。
Developer登録
iOS
年間11,800円ですね。支払い方法はApple Storeギフトカードまたはクレジットなのでクレジットカードを持っていなかったら毎年Apple Storeに行くイベントができるわけですね。
いや高い。。。
Android
登録料$25以外は一切かかりません。良心的!
支払いはクレジットカードまたはデビットカードですが、どうやらVプリカというVISAのサービスを使えばクレジットカードを持てない人もコンビニでプリペイドカードみたいに支払えるようになるみたいです。
まとめ
今となってはスマホのアプリはアプリケーション開発の主力となっていますがやはりどちらもで似たようなことを実現するというのはまだまだかなり難しいようですね。
特にAndroidはスレッドをしっかり意識しなければならない場面がとても多く、またJavaだとNull安全ではないのでそこへの気遣いも必要です。逆にiOSはしっかり極めないとかなり自由度が下がってしまうというところがあります。
そういう意味ではXamarinなど一つのコードで双方のバイナリをつくれるツールが出てきたのはこれからのモバイル開発でターニングポイントになってくるでしょうね。といってもどちらもで同じことが出来るように勉強するというのは非常に勉強になると思うのでこれを機会にぜひみなさんも挑戦してみてください!!