Skip to content

Instantly share code, notes, and snippets.

@53ningen
Created January 19, 2017 19:33
Show Gist options
  • Save 53ningen/c870e652180f8a5f80415f7d3a890f0c to your computer and use it in GitHub Desktop.
Save 53ningen/c870e652180f8a5f80415f7d3a890f0c to your computer and use it in GitHub Desktop.

この資料はラビットハウス社内で開催される、iOSアプリ開発未経験者向けのハンズオン会向けに作成されたものです。このハンズオンを一通り行うことにより、iOS開発において以下のことができるようになります。

  • 基本的なUIKitコンポーネントの利用
    • UILabel: テキストの表示
    • UIButton: ボタンの表示
    • UIImage: 画像の表示
    • UITableView: テーブルの表示
    • StoryboardとAuto Layoutの利用: UIをデザインベースで構成する仕組み
    • プッシュ遷移とモーダル遷移: iOSの大きな2つの画面遷移パターン
  • 非同期処理: ユーザーの操作感を妨げずに、裏側で処理を走らせるなど
  • HTTP通信: ウェブからデータ取得
  • Defaults, Plist: デバイス内の簡単なストレージ

お品書き

0. はじめに (0分)
1. 環境構築 (5分)
2. プロジェクトの作成と Hello, world (10分)
3. 2種類の画面遷移(10分)
4. 簡単なUIKitコンポーネントの使い方 (20分)
   --------------休憩--------------
5. TODOアプリを作ろう (60分)

0. はじめに

 iOSアプリ開発自体は技術的にはそれほど難しくないため、他の分野のエンジニアに開発に関わってもらえるようにハンズオン会を開催しました。また、場合によっては、簡単なプログラミングができるアプリディレクターやデザイナーにも理解出来るようになるべく平易な内容とするように心がけました。 if, else などのキーワードや変数、関数などについてざっくりと理解していて、簡単な関数を実装できれば、この資料を見ながらきっと誰でもアプリ開発ができると思います。

 2時間で〜〜アプリを作るところをゴールとして進めていきますが、プログラミングに慣れている方であれば1時間で終わってしまう内容かもしれません。しかしながら、iOSアプリ開発のフローは一通り体験でき、Googleで検索をしながらアプリ開発を進めていけるレベルには達するのではないかと思います。また、このハンズオンで実装を進めていくアプリのソースコードは https://github.com/53ningen/iOSHandsOn に置いてありますので、困ったときは参照してみると良いかもしれません。

それでは、iOSアプリ開発の世界に飛び込んでいきましょう。

1. 環境構築

このハンズオンを進める上で必要な Xcode 8.2 / Ruby 2.3.3 の実行環境を構築します

1.1 Xcode 8.2 の導入

 Xcode 8.2 を含めた過去のバージョンの Xcode は以下の URL からダウンロードすることができます。ダウンロード、およびインストールにはかなりの時間がかかりますので、時間に余裕のあるときにやることを強くお勧めします。

https://developer.apple.com/download/more/

1.2 CocoaPods の導入

 今回のハンズオンでは依存ライブラリ管理ツールとして CocoaPods を利用します。これは node における npm や Java における maven などのようなものです。CocoaPods は Ruby で作られていて、Mac には標準で Ruby の実行環境が入っていますが CocoaPods の最新バージョンは古い Mac に標準で入っている Ruby では動作しない可能性があります。

 そこでまず Ruby のバージョン管理ツール rbenv を導入しましょう。続いて Ruby のライブラリ管理ツールである bundler を導入し、それを利用して CocoaPods を導入します。よく分からないと思った方は、とりあえずおまじないだと思って以下のコマンドを実行していってみてください。余談ですがチーム開発においては、手元の環境で CocoaPods が正常に動くとしても新しいメンバーが開発に加わりやすいように rbenv を導入しておくと何かとスムーズで良いと思います。1

1.2.1 rbenv を使った Ruby 2.3.3 の導入

 rbenv とは、複数のRubyバージョンを切り替えることができるツールになります。マシンによっては OS X 標準で入っている Ruby のバージョンが古く、iOSアプリ開発によく使われる Ruby のライブラリ(gem)がうまく利用できない場合があるため、rbenv を利用して特定環境かにおけるつまづきどころをなくす意図があります。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
rbenv install 2.3.3

1.2.2 bundler と CocoaPods の導入

# bundler の導入
rbenv exec gem install bundler
rbenv rehash

2. プロジェクトの作成、 Hello, world とその周辺

 ここでは実際に開発を進めるためのプロジェクトを作成し、簡単な文字を表示させるところまでを行います。

2.1 プロジェクトの作成

Xcode を起動し、 Create a new Xcode project を選択してください。

xcode

続いて Single View Application を選択してください。

ChooseATemplateForYourNewProject

 ProductName として、とりあえず今回のハンズオンでは iOSHandsOn を指定しておきましょう。また、Organization Name は適当に、Organization Id についてはサービスのドメイン名や、github の id を指定することが多いかもしれません。

 このあたりの指定については実際にプロダクトを AppStore へリリースするなどの際に重要になってきますが、今回のハンズオンアプリではとりあえずの値を指定しておけば良いでしょう。また、後から変更することも可能ですので、必要であればそのときに直せば良いと思います。

ChooseOptionsForYourNewProject

 以上をすすめると最後にプロジェクトを保存するパスを指定するダイアログが出てきますので、お好きな場所に保存すると良いでしょう。これでひとまず iOS アプリ開発のためのひな形を作成することができました。この状態で Command + R (アプリ実行へのショートカットキー) を押すと、iOSシミュレーターが立ち上がり、真っ白な画面が表示されると思います。

2.2 Hello, world

続いて簡単なサンプルとして画面に文字を表示させてみましょう。目標とするイメージは下図のようなものです。

 早速 Main.storyboard を選択し、右下にあるパネルからラベル(UILabel)を選択して上図のような画面を作ってみてください。ラベルに表示される文字はラベルをダブルクリックすると編集できるようになります。作業が終わったら再びシミュレーターで実行してみてください。きっとストーリーボードに入力した文字どおりの表示が出ているかと思います。しかしながら、このままでは画面回転をするときっとレイアウトが崩れてしまうでしょう。試しに Command + → キーを押して画面を回転させてみてください。

 画面が回転した場合でも上下中央位置に表示させたい場合にはConstraints(制約)を指定してあげる必要があります。ラベルを選択したあと、ストーリーボード編集画面右下のボタンを押し、下図のような設定にチェックを入れ Add 2 Constraints ボタンを押してください。それぞれ Horizontally in Container は水平方向中央揃え、Vertically in Container は上下方向中央揃えの意味となります。最後に右下5つアイコンがならんでいる部分の一番左、更新マークのボタンを押すと、追加された制約がストーリーボードに反映されます。

 この状態で再びシミュレーターを立ち上げ画面を回転させてみてください。きっと画面がどのような方向を向いていてもラベルは画面上下中央位置に表示されたのではないでしょうか。もしストーリーボードを使わずコードでUIを構成する際も同じような形で Constraint を指示してあげないと、画面回転時や異なる縦横比のデバイスでの表示がおかしくなることがありますので、デバッグ時に画面を回転させながらデザインが崩れないかをチェックしてみてください。

まとめ

  • 開発中はこまめに画面回転を試し、制約のつけ忘れなどミスがないかを確認しながらすすめる

2.3 ボタンを押したら文字がかわる機能をつける

 画面にただ単に Hello, world と表示されているだけではつまらないので、ボタンを設置して、押したときの時刻が表示される機能を搭載してみましょう。まずはストーリーボードに対して下図のようにボタンを追加してください。

 続いてXcodeの右上にある ボタン(真ん中)を押してみてください。ストーリーボードとソースコードの2画面表示になったかと思います。この状態でラベルを選択したのち control キーを押しながらソースコードの適当な位置にドラッグすると下図のようなダイアログが出てくるでしょう。

 Name に label と入力し Enter を押すと @IBOutlet weak var label: UILabel! というコードが追加されると思います。これはストーリーボード上にあるラベルをコードから操作できるようにリンクしてあげたと思っていただければ大体良いと思います。ボタンに関しても同様に操作を行うと @IBOutlet weak var button: UIButton! というコードが追加されるでしょう。

 続いて、ボタンを押したときのイベント処理を書いてみましょう。 ViewController.swift を開いて、次のようなメソッドを追加してください。

    func buttonOnTapped(sender: UIButton) {
        let now = Date() // 現在時刻を取得
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP") // ロケールを指定
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" // 時刻表示のフォーマットを指定
        // 指定したフォーマットで現在時刻を取得してラベルに反映させる
        label.text = dateFormatter.string(from: now) 
    }

 最後にボタンのタップイベント発生時にこのメソッドを呼び出すように設定するコードを追加します。

    override func viewDidLoad() {
        super.viewDidLoad()
        button.addTarget(self, action: #selector(ViewController.buttonOnTapped(sender:)), for: .touchUpInside)
    }

 これでボタンが .touchUpInside、つまりタップして指を離した瞬間に、先ほど定義したメソッドを呼び出すという紐付けができました。早速シミュレーターで動かして試してみてください。おそらく、ボタンを押すと現在時刻が表示されるようになったのではないでしょうか?

 ついでに、同じ仕組みを異なる方法で実装しておきましょう。一旦ストーリーボードのボタンを control キーを押しながらクリックして、New Referencing Outlet に紐付いているものを削除し、もう一度 control キーを押しながらソースコードへドラッグします。その際に下記のように ConnectionAction に変更します。この場合 Name の欄にはボタンの名前ではなく action の名前を指定することになるので buttonOnTouchUpInside など適当な名前を指定してあげてください。すると今度はメソッド定義が生成されたのではないでしょうか。この手法ではこうして生成されたメソッド内に、ボタンが押されたときの挙動を定義していくことになります。

さきほどと同じ内容を実現しようとすると最終的にコードは次のようになるかと思います。

class ViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!

    @IBAction func buttonOnTouchUpInside(_ sender: Any) {
        let now = Date() // 現在時刻を取得
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP") // ロケールを指定
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" // 時刻表示のフォーマットを指定
        // 指定したフォーマットで現在時刻を取得してラベルに反映させる
        label.text = dateFormatter.string(from: now)
    }
    
}

 実装が終わったらシミュレーターで実行してちゃんと動作するかを確認してください。これで iOS で UI を構成する最も基本的なUI部品である UILabelUIButton の使い方の雰囲気は理解できたのではないでしょうか。

まとめ

  • ボタンのイベント処理には @IBOutlet を使う方法と UIButton.addTarget を使う方法がある

2.4 トラブルシューティング

 この先ますます複雑なことをやるとしばしば謎のエラーがでて、うまくシミュレーターで実行できない場合がでてくるかと思います。そんな際は command + shift + alt + K キーを押して、ビルドした成果物をディレクトリごと消すということを試してみてください(mvn clean的なものになります)。また Xcode がおかしな挙動を見せてくることがしばしばありますが、たいてい再起動すれば治るかと思います。

 何か問題が生じた場合は無理に頑張ろうとせず、まず上記の2点を試してみるということを忘れずに、寛容な心で開発を進める必要があります。

3. 2種類の画面遷移

 1画面だけで構成されるアプリはほとんどなく、プロダクトとして出すものは、ほとんどの場合画面遷移が発生します。iOSの画面遷移には主に次の2つのパターンが存在します。

  1. モーダル遷移(下からビューが飛び出してくる、閉じるボタンでビューを閉じる)
  2. プッシュ遷移(右方向にビューがスタックする、スワイプで戻れる)

 iOSヒューマンインターフェースガイドラインにこの2つの遷移の使い分けの思想について書かれているので、Apple 信者の方々は是非そちらをごらんください。そうでない場合には普段のiOSアプリの利用の仕方に照らし合わせて適切な方を選んでいくと良いでしょう。例えばコンテンツの構造として 「漫画アプリ」 ⊃ 「漫画作品」 ⊃ 「漫画エピソード」 という図式が成り立つのであれば、きっと漫画アプリトップから作品への遷移と作品からエピソードへの遷移はプッシュ遷移が適しているはずです。また、この間の任意の場所でログインさせるというアクションをユーザーにさせたい場合はモーダル遷移でログイン画面をユーザーに提示し、ログインが完了したらその画面を閉じてあげると良いでしょう。

 余談になりますが、近年はデバイス自体が大型化してきており、モーダル遷移時の「閉じる」ボタンの位置が左上にあると手の大きさ的に届きにくい位置になるため、モーダルで出すビューの画面デザインをする際にこのあたりのことを気をつけると良いと思います(※ 個人の感想です)。前話が長くなってきたのでこのあたりにして、早速それぞれの遷移を実装してみましょう。

3.1 プッシュ遷移の実装

 プッシュ遷移をするためには現在の ViewControllerUINavigationController に属している必要があります(正確にいうと UINavigationControllerchildViewControllers の要素である必要ということ)。こんなこと言葉で言われてもわからないと思うので、早速作業をしながら理解していきましょう。

 ここではボタンを押すとプッシュ遷移をする仕組みを実装していきたいので、その準備段階として Main.storyboard でいままで「時刻を表示」となっていた部分を「遷移する」というテキストに変えてみましょう。続いて、ViewControllerを選択し、右カラムのメニュー Identity -> Storyboard ID の部分に Main というテキストを入力しましょう。

 そして「上部メニュー:Editor」→「Embed in」→「Navigation Controller」を選択してください。すると Navigation Controller とかかれたよく分からない画面が出現したと思います。と、同時にさきほどまでのビューの上部にも謎の灰色のスペースが生じてしまいました。とりあえず起動して動作確認してみましょう。

 上図のような状態になっているのではないでしょうか。さて、ここからプッシュ遷移を実装をしていきます。新しい画面を作るのは一旦後回しにして、まずは同じ画面に遷移するコードを書き足してみましょう。そのためにボタンのタップイベント処理のメソッド内を次のように書き換えます。

    @IBAction func buttonOnTouchUpInside(_ sender: Any) {
        let vc = UIStoryboard(name: "Main", bundle: Bundle.main)
            .instantiateViewController(withIdentifier: "Main") as! ViewController
        navigationController?.pushViewController(vc, animated: true)
    }

 この状態でシミュレーターで実行すると、ボタンを押したときにきちんとプッシュ遷移できるようになっているのではないでしょうか? またスワイプや左上の < ボタンで前の画面に戻れるようになっていると思います。

まとめ

  • プッシュ遷移には navigationController?.pushViewController を使う

3.1.1 (プログラマ向けトピック) UINavigationController のふるまい

 UINavigationController の振る舞いについてちょっとだけ見ておきましょう。冒頭で UINavigationController は UIViewController をスタックしていくことが役割だと言いました。その様子を実際に見るためにページ遷移が終わった瞬間(viewDidAppear)に navigationControllerchildViewControllers をログ出力するコードを ViewController に仕込みます。

    // 画面が表示された直後に呼ばれる
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        NSLog("\(navigationController?.childViewControllers)")
    }

この状態でシミュレーター実行をし、遷移をしたり戻りながら、ログ出力を観察してみましょう。

# 起動直後
iOSHandsOn[43460:5142923] Optional([<iOSHandsOn.ViewController: 0x7fc5836077d0>])
# 一回プッシュ遷移
iOSHandsOn[43460:5142923] Optional([<iOSHandsOn.ViewController: 0x7fc5836077d0>, <iOSHandsOn.ViewController: 0x7fc583618ca0>])
# 戻る
iOSHandsOn[43460:5142923] Optional([<iOSHandsOn.ViewController: 0x7fc5836077d0>])

きちんとスタック構造になっていることがわかるかと思います。

3.1.2 (プログラマ向けトピック) Storyboard と 型

 静的型付け言語になれたプログラマであれば、ViewController を取得するときのコードについて文字列で指定しているあたりに恐怖心を覚える方もいるのではないでしょうか? 実際この部分の指定を間違えると、静的チェックが走らず実行時にアプリがクラッシュします。

 この部分に対する対応策は各自で取らざるを得ないのが現状で、例えば次のようなやり方をしている人が多いです。

  1. 動作確認を徹底することによる対応(筋肉系)
  2. ViewController を生成するファクトリを作り、そのファクトリのすべてのメソッドについてテストを書き、確実にインスタンスが得られることを保証する
  3. ViewControllerStoryboard ID(または Storyboard のファイル名) の命名規則を定める、ViewController の型を受け取り、そのインスタンスを生成するファクトリメソッドを用意し、アプリ内ではそれを通してインスタンスを得る

 3. については具体的には、MainViewController に対する Storyboard の名前は必ず MainViewController にするという規則を設け、次のように ViewController の型名を取得して上記のようなことを実現するという手法になります。命名をミスれば安全でもなんでもない手法ですが、StoryboardやViewControllerの名前を変更することはまれであるため、手軽な方法ではないかと考えています。

extension NSObject {    
    // 型から型名を取得するユーティリティ関数
    static func getClassName() -> String {
        return NSStringFromClass(self)
            .components(separatedBy: ".").last! as String
    }
}

extension UIViewController {
    static func of<T: UIViewController>(_ cls: T.Type) -> T {
        return UIStoryboard(name: cls.getClassName(), bundle: Bundle.main).instantiateViewController(withIdentifier: cls.getClassName()) as! T
    }
}

3.1.3 (プログラマ向けトピック) Storyboard ID を使わず Storyboard に紐づくViewController を取得する

以下のようなコードで可能です。口頭で説明いたします。

    @IBAction func buttonOnTouchUpInside(_ sender: Any) {
        // Main.storyboard を取得
        // Main.storyboard の InitialViewController は
        // UINavigationController なので as! 以後はそれを指定
        let nvc = UIStoryboard(name: "Main", bundle: Bundle.main)
            .instantiateInitialViewController() as! UINavigationController
        
        // 遷移先としたいの ViewController は 
        // childViewControllers の 1つ目の要素なので
        let vc = nvc.childViewControllers[0] as! ViewController
        navigationController?.pushViewController(vc, animated: true)
    }

3.2 プッシュ遷移画面から戻るボタンの実装

 プッシュ遷移をしたあとにスワイプで前の画面に戻れますが、ある条件のときに強制的に前の画面に戻したいことがあるかと思います。ここでは、ボタンを押すと前の画面に戻る機能を実装してみましょう。まずはおなじみ Main.storyboard を次のような状態に改変しましょう。またトラブル防止のため、各UIButton を control + クリックし Outlet を一旦クリアにしましょう。

 続いて、ストーリーボード上のボタンとコードを結びつける作業をもう一度やりましょう。その作業が終わったら、まず「プッシュ遷移する」ボタンについて以前やったとおりに addTarget を利用してプッシュ遷移の実装を行ってください。これがおわるとコードは以下のような状態になっているかと思います。

class ViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var pushButton: UIButton!
    @IBOutlet weak var popButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        pushButton.addTarget(self, action: #selector(ViewController.pushButtonTapped(_:)), for: .touchUpInside)
    }
        
    func pushButtonTapped(_ sender: Any) {
        let nvc = UIStoryboard(name: "Main", bundle: Bundle.main)
            .instantiateInitialViewController() as! UINavigationController        
        let vc = nvc.childViewControllers[0] as! ViewController
        navigationController?.pushViewController(vc, animated: true)
    }
    
}

 つづいてポップボタンについてですが、こちらについては最初の画面では戻り先が存在しないのでボタンを無効化しておくのが親切だと思います。そのためにビューが読み込みされた時点(viewDidLoad)で navigationController!.childViewControllers.count (スタックされているViewControllerの数) が 1 より大きかったら有効にしてあげる処理を記述します。

 また、プッシュ遷移から戻る処理は navigationController?.popViewController(animated: true) という命令を呼び出してあげれば実現できるため、この処理とボタンのタップイベントを addTarget で紐付けましょう。

    override func viewDidLoad() {
        super.viewDidLoad()
        pushButton.addTarget(self, action: #selector(ViewController.pushButtonTapped(_:)), for: .touchUpInside)

        // 戻り先の画面が存在するときだけ、ボタンを有効にする
        popButton.isEnabled = navigationController!.childViewControllers.count > 1
        popButton.addTarget(self, action: #selector(ViewController.popButtonTapped(_:)), for: .touchUpInside)
    }

    func popButtonTapped(_ sender: Any) {
        _ = navigationController?.popViewController(animated: true)
    }

 シミュレーターで実行すれば期待したとおりの動作になっているのではないでしょうか。

まとめ

  • プッシュ遷移からもどるときには navigationController?.popViewController を使う

3.3 モーダル遷移

 同様のノリでモーダル遷移についても実装していきましょう。モーダル遷移については UINavigationController うんぬんは一切関係なく、どのビューからもこの遷移が使えます。

4.1 TODOアプリの作成

 UITableView(テーブルビュー)はiOSアプリでしばしば見かけるUIコンポーネントのひとつです。たとえば標準のメールアプリや設定画面などに使われています。

 さて、ここからは簡単なTODOリスト管理アプリを作っていきましょう。TODOリストの表示にはもちろん

HTTPリクエスト

仕様

Footnotes

  1. rbenv を利用した開発フローに載せてくださった @koki_cheese さん、ありがとうございます

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment