AndroidとiOSの実装を徹底比較する

  • 14
    いいね
  • 0
    コメント

はじめに

とうようです。この記事はIS17er Advent Calendarの21日目の記事です。

今回はかねてから開発していたFFMultiplierというお勉強ゲームアプリのiOS/Android両バージョンをリリースできたことを記念して、その開発の中で感じたどっちのどこらへんが開発しやすかったかしにくかったかみたいなところを徹底比較してみたいと思います。

アプリはこちら

今までも両方開発した人がその所感を述べるというのはよく行われていたと思うので今回はある機能を技術的にどう実現するかという観点にしぼって比較していければと思っています。

タイマー

Swift

Swiftでのタイマーは基本的にはTimer(旧NSTimer)を使います。何秒かごとに同じ処理を実行したければ

timer
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)

こんなかんじ。#selectorが導入されてタイプミスも減りましたし、基本的にはスレッドなどを気にせずに安全に書けるのでとても便利です。

Java

Javaでも基本的にタイマーはTimerクラスを使って実装します。しかし、Android timerJava Timerをググった時のならびをみてもらうとわかりますが、TimerへのTimerTaskクラスを渡す方法がいくつかあって初心者は面食らうかもしれません。

今回はゲームのカウントダウンだったので当初CountDownTimerというそのまんまのクラスを使おうと思っていたのですが、よく調べてみるとこれは正確に1秒をはかってくれないという欠点があるらしく、Timerに落ち着きました。クラスを別につくれば書き方自体は似ています。

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内で

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ボタン、およびテキストフィールド一個のアラートであれば

Alert
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をつくって適用してあげます。

AlertDialog
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を上に表示する形式にするのが一番いいと思います。

というわけで僕はコチラを使いました。

STZPoupupView

Storyboardあるいはxibでレイアウトを用意してあげればかなり簡単に表示することができます。

Android

一方Androidでは色々と実現方法はあると思いますが、PopupWindowというまさにという機能があります。

これによって少し設定項目は多いですがしっかりやりたいことを実現することができます。

ですがこの項目はむしろAlertDialogをカスタマイズしてしまう方が簡単かもしれませんね...

Firebaseの扱い

基本的にはどちらも扱い方は同じですが多少扱いやすい使い方というものに言語の特性に応じた違いが現れています。例えば登録のときは

Firebase/setvalue
ref.child("hello").child(device_id).setValue(["id": id, "rank": rank as NSNumber], andPriority: -newScore.score)

このようにiOSではDictionaryを使って登録できます。一方AndroidはMap<String, Object>を使えば同じように使えますがこれはハッシュ木で使われているもので純粋に標準とはいえないのでそのかわりに以下のようにクラスを定義して

Firebase/class
@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;
    }
}

これのインスタンスを渡すことで変数名をキーに自動で変換して扱ってくれます。こんな感じで

Firebase/setvalue
ref.child("hello").child(id).setValue(databaseScore);

データベースを読み出してくるときはSwiftはobserve(.value, with: handler)で、AndroidはaddValueEventListener(valueEventListener)です。それぞれの特性が出ていますね。
登録したときと同じ形式で読み出すことが可能です。

Realmの扱い

Realmの扱い方に関してもFirebaseと似たような傾向が見られます。

単純な読み書きについて比較してみましょう。まずiOSでは以下のようになります。

Realmのモデル
import RealmSwift

final class Score: Object {
    dynamic var date = NSDate(timeIntervalSince1970: 1)
    dynamic var score: Int = 0
}

まずこれでモデル定義します。Realmの内部処理のために保存したいものにはdynamic修飾子をつけるのでしたね。そして

Realm書き込み
let newScore = Score(value: ["date": NSDate(), "score": acceptedNum])
let realm = try! Realm()
try! realm.write {
    realm.add(newScore)
}

このようにtryで書き込みスレッドに入って追加すると

Realm読み込み
let scores = realm.objects(Score.self).sorted(byProperty: "score", ascending: false)

このようにobjectsというメソッドで保存したものを取り出すことができるのでした。

次に同じことをAndroidでやってみましょう。するとモデル・書き込み・読み込みは以下のようになります。

Realmのモデル
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;
    }
}
Realmの書き込み
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);
    }
});
Realmの読み込み
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など一つのコードで双方のバイナリをつくれるツールが出てきたのはこれからのモバイル開発でターニングポイントになってくるでしょうね。といってもどちらもで同じことが出来るように勉強するというのは非常に勉強になると思うのでこれを機会にぜひみなさんも挑戦してみてください!!