20190714のiOSに関する記事は13件です。

実務未経験からiOS開発に入ってとりあえず仕事できるようになるためのTips

誰?

僕のスペックとかは下記を読んでください。

メインフレーム5年の経験しかない金融系SEがiOSエンジニアとして転職した話

SIerからWeb企業(っぽいところ。何企業なんだ……?)に行き、2週間たちました。
まだ一人前とは言い難いですが、色々バタバタ足掻きながら、開発業務に入りました。
実務でのiOS開発未経験とはいえ、新人ではなく中途なので、即戦力として採用されているつもりで働いています。
実際は勉強しながらなので即戦力には程遠いですが、姿勢や気持ちはそんな感じです。
(とはいえチームの人はいい人だったので、転職のときイメージしていたよりも手厚いサポートを受けています)

この記事では、その中で独学ではあまり使わなかったけど現場入ったらめっちゃ必要だったことを書こうと思います。
コーディング云々というよりかは、もうちょっとハウツー的な話になると思います。

ソフトスキル面とハードスキル面でわけます。
ハードスキルに関しては完全にiOSの話になっちゃいますが、ただそんな深いところまで踏み込まないので、
他の分野の方でも役立つような気がします。

ソフトスキル

色々アカウントつくる際の権限

GithubやSlack、Apple Developer Programだけでなく、FirebaseだのCIツールだの、作らなければいけないアカウントが最初は本当に多かったです。
よほど人の出入りが激しいプロジェクトじゃない限り、この辺の運用フローが確立していないと思います。
正直それぞれのツールに詳しい人が適宜登録していく感じになりました。

Githubで、Pull Request出そうとしたらリモートリポジトリにPushできなくて、
エラーメッセージも「リポジトリが見つかりません」みたいな感じで、僕もGithub使ったチーム開発が初だったので、
環境要因なのか操作ミスなのかがわからなかったのですが、結局僕のアカウントが閲覧権限しかなかったのが問題でした。

ググり力 + 質問力

前職の金融系システムは、「自己判断するな」の文化でした。
「ちょっとでも疑問持ったら聞け」と言われて育ちました。
質問しないで自己判断する奴は事故る、と言われましたし、実際そうでした。
だから9割こうだろうな……と思うことでも、万が一のために先輩・上司に聞いて確認していました。
質問できない奴=仕事できない奴みたいな感じでした。

ただよく言われるように、Web企業で同じスタイルでやると、「こいつ無能か……?」となってしまう気がします。
郷に入れば郷に従え、ということで、今の職場では極力自走するようにしています。
(もともとそっちのほうが得意ですし)
自走する際に最も必要になるのは、ググる力です。
単にGoogle検索するだけなら誰でもできますが、たとえば検索するワードのチョイスや、
出てきたサイトのタイトルから適切なページにいけるかとか、リテラシーの部分で差が出ます。

自走するのと自己判断で突っ走るのは違います。
自走して正解に行けるのであればチームとしては助かりますが、未経験者が自走しようとして、あらぬ方向に突っ走ってしまうことはよくあると思います。
ある程度自走したところで、適宜質問して確認するのは、Web業界でも大事だと思っています。
Webだとスピード感命なところがあるので、SIと同じ感覚で質問するとめんどくさい奴になるので、
なるべく質問は短くして、相手を拘束する時間を短くすることを意識しています。

いい感じに質問する力、というのは前職で培ったソフトスキルがほぼそのまま使えている気がします。
なるべくOpen Questionにしないことが大事ですね。
「何かパソコンが動かないんですが……」と聞かれると困りますよね。
「◯◯という操作を行ったあと、パソコンがフリーズしました」と聞かれると、少しは答えやすくなります。
もっといいと思うのは、「パソコンがフリーズした。◯◯という操作をしたのですが、これが原因ですかね?」と質問すると、
相手はYes/Noで答えられるので、一番楽かなと思います。

Face to Face or Slack

皆さん、大事な話をするときって、どうします?
営業の人なんかと話していると、「ここは大事なところなんで、メールじゃなくて対面で話せませんか?」と言われたりします。
IT業界の人間の考え方だと、対面で話したこと=記録が残らないものなので、正直大事な話でも文章でして欲しい感覚があると思います。

さて、Web系独特の文化として、Slack中心のコミュニケーションがあると思います。
大事な話をするとき、Face to Faceでどこまでやるか、Slackでどこまでやるかは、その企業やチームの文化が結構あるので、確認した方がいいです。
「え!? そこまでSlackでやっちゃうの?」
→「いやむしろSlackの方が後で見返せるからありがたいじゃん?」
みたいな。

前職だと、Face to Faceの確認というのは重要で、対面で話した上で議事録とって証跡を残そうね、という文化でした。
Web系だと重要な意思決定もSlackでパンパンやりとりしてる内に決定しちゃったりするので、結構面食らいます。
僕の入ったところだと、アイディアを発散させる目的の会議では対面で集まるけれど、その後の意思決定なんかは、
対面でやる意味がそんなにないので、資料送って、Slack上で確認して、OK/NG出す文化っぽいです。

ハードスキル

まず規約を確認

最初に規約を確認しました。
規約ガチガチの会社もあれば、皆自由に書いている会社もあると思うので、そこは最初に確認した方が事故らなくてすむと思います。
たとえばエウレカさんだと下記の規約に従って書いてるみたいです。

エウレカ流Swift Style Guideを公開しました

あとコードレビューの運用とか、テストの運用とか、最初に確認しときましょう。

新規開発かアップデートか

新規開発してるチームに入るか、既存のアプリのアップデートかで、やることは変わると思います。
前提として、僕は既存アプリのアップデートに入りました。

新規開発だと、コードライティング>>>>>>リーディングですが、
既存のアップデートだとコードライティング<<<<<<リーディングになります。

Xcodeのプロジェクト内検索

個人開発と商用アプリで、僕が一番違うなと思ったのがコード量です。
これはもう個人で作ったアプリとは比較にならないと思います。
アプリの持っている機能にもよるとは思いますが、普通に数万〜数十万くらいの規模のコードになります。

個人開発ではほとんど使ったことなかったんですが、Xcodeのプロジェクト内検索をめっちゃ使います。
image.png
てかこれで移動するのがデフォルトになってます。

Jump to Definitionの活用

プロジェクト内検索と合わせて、XcodeのJump to Definitionもめちゃくちゃ使います。
アプリの規模が大きいと、関数の定義が相当離れた場所に書いてあったりするので、Jump to Definitionにめちゃくちゃ助けられました。
⌘キー押しながら、関数名押すと使うことができます。

個人開発だと関数自分が中身書いてたので、ありがたみが全くわからなかったです。

デバッグメッセージがヒントになる

これも既存改修ならではだと思うんですが、コード読んでるだけだとイメージつかないところが、
デバッグログで出してるメッセージでわかることがあります。
ソースコードだけでなく、デバッグでコンソールに吐いてるメッセージも見るといいでしょう。

Gitの操作は慎重に

未経験エンジニアが失敗しがちなのが、Gitの操作だと思います。
僕もかなり勉強した上で現場入って使ってみて、やっとなんとかなるかなと思った感じです。
Pull Request送るまでの流れは、ミスらずできるようにしておきたいですね。

開発したコードにバグがあったとかはいいと思うんですが、リモートリポジトリ破壊しちゃうとか、
その手のミスはどうしても無能感出ちゃうので、できれば避けたいところです。
(さすがに入りたてのエンジニアにmasterブランチに権限与えないとは思うので、破壊もできないと思うものの……)

初心者が必ず困惑するところなので、Git操作の記事はQiitaにもいっぱいありますが、下記のQiita記事が個人的には好きです。

未経験がWeb系転職成功したいならgithubでissue管理して開発しよう

ホントはGitの操作って本質じゃないところなんですが、
(別にバージョン管理スペシャリストになりたいわけじゃない)
基本的な操作ができないと、その先の開発が円滑にできないので、しっかりやっておきましょう!!

おまけ

Twitterやってます。
よろしければフォローお願いします。

https://twitter.com/eg_19_

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

url(forResource:withExtension:) のありがたい仕様(swift)

はじめに

タイトル通り。swift の Bundleurl(forResource:withExtension:) の動作についてです。

動作

Xcode のプロジェクトに下記のようにファイルを追加します。

resource

ファイル名と拡張子指定

let url = Bundle.main.url(forResource: "test", withExtension: "json")

通常の指定方法です。test.json の URL が取得できます。

指定なし

let url1 = Bundle.main.url(forResource: "", withExtension: "")
let url2 = Bundle.main.url(forResource: nil, withExtension: nil)

引数を無しにします。url1url2 も当然 nil になります。

ファイル名のみ指定(拡張子なし)

let url1 = Bundle.main.url(forResource: "test", withExtension: "")
let url2 = Bundle.main.url(forResource: "test", withExtension: nil)

引数の拡張子指定を無しにします。url1url2nil になります。

ファイル名のみ指定(拡張子あり)

let url1 = Bundle.main.url(forResource: "test.json", withExtension: "")
let url2 = Bundle.main.url(forResource: "test.json", withExtension: nil)

引数のファイル名に拡張子まで指定し、引数の拡張子を無しにします。url1url2test.json の URL が取得できます。

拡張子のみ指定

let url1 = Bundle.main.url(forResource: "", withExtension: "json")
let url2 = Bundle.main.url(forResource: nil, withExtension: "json")

引数のファイル名指定を無しにします。url1url2test.json の URL が取得できます。んっ!? なんか取れた...

公式のドキュメントを見てみると下記のように書いてあります。

If you specify nil, the method returns the first resource file it finds with the specified extension.

なんか最初に見つけたやつ返してくれるみたいです。詳しい動作はわかりませんが、どこかにキャッシュを持ってるらしく clean build してもプロジェクトから指定のファイルを消しても最初に取れた URL が取れました。(プロジェクトから指定のファイルを消してアプリを削除して入れ直すとまた別のファイルが取れました)取れるファイルはアルファベット順とか Copy Bundle Resources の追加順ていうわけではなさそうだったので、どのファイルが取れるのかはよくわかりません。

まとめ

url(forResource:withExtension:) はちゃんと指定しなくても何か返してくれる。ちなみに path(forResource:ofType:) も動作は同じっぽい。

さいごに

ファイル名が nil なのになんかファイルを返却してくれるのはどこかで役に立つんだろうか?

func url(forResource name: String?, withExtension ext: String?) -> URL?

返却値がオプショナルなんで nil でも良さそうなのに...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS Safariでinput要素のfocusを簡単に外す方法

始めに

iOS Safariでinput要素を選択してキーボードが出ているとき、何も設定しないと画面をタップしただけではfocusを外してキーボードを閉じることができません。これを何とかするために今まではJSでblurするようにしていましたが、もっと単純な方法があったのでその方法を紹介したいと思います。

結論

親のDOMにonclickイベントを設定したら外せます。めちゃくちゃ簡単ですね(笑)。onclickの処理を書かなくても問題ないです。また、どの階層にいても問題ないので、一番上のdiv要素にonclickを設定すればいいと思います。

iOSでfocusが外れるようにする設定
<html>
<body>
  <!-- 親要素に空でもいいのでonclickの設定をする -->
  <div onclick="">
    <input text="" />
  </div>
</body>
</html>

なぜこれで上手くいくか

仕様をちゃんと読んだわけではないので憶測ですが、どうやらonclickイベントを設定していないDOMはこのイベント自体を省略してしまうようです。HTMLの挙動的にはどれかをクリックしたらそちらにfocusが移るのですが、イベント自体が省略されているのでfocusが外れないようです。
なので一番親であるdiv要素とかにonclickイベントを設定することで必ずonclickイベントを拾えるようになるので、focusを外せるということです。(bodyタグでいけるかと思ったのですが、どうやらbodyだと拾ってくれなそうです)

サンプルコード

CodePenでサンプルコードを書きましたのでそれをこちらに載せます。

See the Pen iOSでフォーカスを簡単に外す by wintyo (@wintyo) on CodePen.

スマホで動作を確認する方は以下のURLからアクセスしたほうがいいと思います。
https://codepen.io/wintyo/full/wLbqLN

一番上にbodyのクリックイベントをカウントするようにしましたが、カウントされていない場所があると思います。そこはクリックイベントが送られていないのでfocusも外せない状態になっています。
ついでの発見ですが、どうやらonclickイベントを拾った要素にハイライトカラーが当たるようですね。実際はこれは邪魔だと思うのでハイライトカラーを消さないといけなそうですね。

終わりに

iOS Safariでfocusが外せない問題に結構悩まされていましたが、簡単に対応できる方法が見つかってよかったです。ただこのサンプルコードを書いていたら勝手にズームされたり、キーボード表示中にスクロールすると謎の黒い領域が現れたり、iOS特有の挙動に悩まされてしまいました。ズームはなんとかなりましたが、領域のほうは解決できませんでした・・・。もうちょっと普通の挙動をしてくれると嬉しいんですけどねぇ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kerasで作成したモデルを変換してiOSに組み込み

やること

前回記事(【Tensorflow・VGG16】転移学習による画像分類)で作成したモデルを使ってiPhone上で写真を撮影し、それを推定してみる。

手順概要

  1. Kerasで作成したモデル(.h5)を変換
  2. Xcodeでプロジェクトを作成
  3. 画面のパーツ作成 & コードの関連付け
  4. カメラへのアクセス許可 & カメラを起動するコードを追加
  5. ボタン(写真撮影)押下時の挙動
  6. モデルを読み込んで推定

動作環境

  • macOS Catalina 10.15 beta
  • Python 3.6.8
  • keras 2.2.4
  • coremltools 2.1.0
  • Xcode 10.2.1

1. Kerasで作成したモデル(.h5)を変換

  • 前回記事で作成したモデル(vgg16_transfer.h5)を使い、iOSで利用可能なモデルに変換する
モデルの変換
import coremltools

coreml_model = coremltools.converters.keras.convert(
    'vgg16_transfer.h5',
    input_names='image',
    image_input_names='image',
    output_names='Prediction',
    class_labels=['apple', 'tomato', 'strawberry'],
    )

coreml_model.save('./vgg16_transfer.mlmodel')

2. Xcodeでプロジェクトを作成

  • Xcodeを起動し、「Create a new Xcode project」を選択 1.png
  • 「Single View App」を選択 2.png
  • 'Product Name'は任意の名称を付ける。Languageは「Swift」、チェックボックス(Use XX, include XX)は全て外し、Nextボタンを押下する

3. 画面のパーツ作成 & コードの関連付け

  • Xcode上で下記3つの画面のパーツを追加する

    • Image View(写真を表示する領域)
    • Text View(推定結果を表示する領域)
    • Button(カメラを起動するボタン)
  • こんな感じのレイアウトにしておく
    3.png

  • 各パーツの関連付けを実施する

  • Image ViewとText Viewは「class ViewController」の下に追加(パーツを選択し、controlキーを押下しながら追加)

  • Buttonは「override func viewDidLoad()」の下に追加

  • Name(とConnection)はそれぞれ下記で設定

    • Image View : imageDisplay(Connection : Outlet)
    • Text View : predictionDisplay(Connection : Outlet)
    • Button : takePhoto(Connection : Action)
各パーツの関連付け
    # Image View
    @IBOutlet weak var imageDisplay: UIImageView!
    # Text View
    @IBOutlet weak var predictionDisplay: UITextView!
    # Button
    @IBAction func takePhoto(_ sender: Any) {
    }

4. カメラへのアクセス許可 & カメラを起動するコードを追加

  • info.plistに「Privacy - Camera Usage Description」を追加し、カメラへのアクセスを許可する
    4.png

  • 下記2つの「delegate」(イベントを検知・処理)を追加する。(カメラを表示する画面、写真を保存した時に元の画面に戻る操作を行うことが可能)

    • UIImagePickerControllerDelegate 
    • UINavigationControllerDelegate 
  • カメラの撮影画面を表示するために「imagePicker」という変数を追加

  • アプリが起動した直後に「imagePicker」を初期化するコードを「viewDidLoad()」内に追加

カメラを起動(before)
class ViewController: UIViewController {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    # ~~~ 省略 ~~~
}
カメラを起動(after)
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!
    var imagePicker: UIImagePickerController!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        # 初期化
        imagePicker = UIImagePickerController()
        # delegate:イベントを受け渡しする変数
        imagePicker.delegate = self
        # sourceType : カメラからデータを撮るか、アルバムから読込か -> 今回はカメラ
        imagePicker.sourceType = .camera
    }

    # ~~~ 省略 ~~~
}

5. ボタン(写真撮影)押下時の挙動

  • 4.で作成しておいたimagePickerを表示するため、present関数を「takePhoto」で呼び出す
before
    @IBAction func takePhoto(_ sender: Any) {
    }
after
    @IBAction func takePhoto(_ sender: Any) {
        present(imagePicker, animated: true, completion: nil)
    }
  • 写真を撮り終わった後の処理を記載(imagePickerController)
  • imageDisplayのimage属性を更新する
  • infoに撮影した画像が入っているため、それを取り出してimageDisplayのimage属性にセットする
  • infoのUIImagePickerController.InfoKeyに各種属性が入っているため、「originalImage」というプロパティを設定する('as? UIImage'でタイプを指定する)
  • 画像を設定したら、先程のImagePickerを閉じる(dismiss)
after
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
    }

6. モデルを読み込んで推定

  • mlmodelを読み込みして、そのモデルに画像ファイルを渡して推定する
  • 1.で変換したモデル(vgg16_transfer.mlmodel)をXcodeのプロジェクトにドラッグ・アンド・ドロップする

7.png

8.png

  • 下記ライブラリを追加する
    • CoreML : 機械学習のライブラリ
    • Vision : 画像ファイルを扱うライブラリ
ライブラリの追加
import CoreML
import Vision
  • 推定処理を加える
  • imagePickerControllerの後ろに推定処理(imagePrediction)を加える
  • 引数に画像ファイルを指定(画面上に表示した内容と同じ(imageDisplay.image))
before
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
    }
after
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
        imagePrediction(image: (info[UIImagePickerController.InfoKey.originalImage] as? UIImage)!)
    }
  • 推定処理(imagePrediction)の関数を作成
  • モデルの読み込み
モデルの読み込み
    func imagePrediction(image: UIImage) {
        guard let model = try? VNCoreMLModel(for: vgg16_transfer().model) else {
            fatalError("Model not found")
        }
  • VNCoreMLRequestのインスタンスを生成
  • 推定結果をresultsに格納する(推定結果は「VNClassificationObservation」という変数に入って返ってくる)
  • results.firstにスコアの高いデータが格納されている
  • predictionDiaplayに推定結果を表示する
    • firstResult.confidence : 確率(少数で入っているので、100倍して%表示)
    • firstResult.identifier : ラベル(推定結果)
モデルによる推定処理
        let request = VNCoreMLRequest(model: model) {
            [weak self] request, error in

            guard let results = request.results as? [VNClassificationObservation],
                let firstResult = results.first else {
                    fatalError("No results found")
            }

            DispatchQueue.main.async {
                self?.predictionDisplay.text = "Accuracy: = \(Int(firstResult.confidence * 100))% \n\nラベル: \((firstResult.identifier))"
            }
        }
  • requestに対する処理を記載
  • ciImageというデータ型に変換できないと推定処理が出来ないため、取得出来ない場合はエラー
  • Visionフレームワークで画像を使うためのimageHandlerという変数を宣言し、VNImageRequestHandlerを生成
  • imageHandlerにrequestを実行させる
        guard let ciImage = CIImage(image: image) else {
            fatalError("Can't convert image.")
        }

        let imageHandler = VNImageRequestHandler(ciImage: ciImage)

        DispatchQueue.global(qos: .userInteractive).async {
            do {
                try imageHandler.perform([request])
            } catch {
                print("Error")
            }
        }
  • コードの全体は以下の通り
ViewController.swift
import UIKit
import CoreML
import Vision

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageDisplay: UIImageView!
    @IBOutlet weak var predictionDisplay: UITextView!
    var imagePicker: UIImagePickerController!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        imagePicker = UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = .camera
    }
    @IBAction func takePhoto(_ sender: Any) {
        present(imagePicker, animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        imagePicker.dismiss(animated: true, completion: nil)
        imagePrediction(image: (info[UIImagePickerController.InfoKey.originalImage] as? UIImage)!)
    }

    func imagePrediction(image: UIImage) {
        guard let model = try? VNCoreMLModel(for: vgg16_transfer().model) else {
            fatalError("Model not found")
        }

        let request = VNCoreMLRequest(model: model) {
            [weak self] request, error in

            guard let results = request.results as? [VNClassificationObservation],
                let firstResult = results.first else {
                    fatalError("No results found")
            }

            DispatchQueue.main.async {
                self?.predictionDisplay.text = "Accuracy: = \(Int(firstResult.confidence * 100))% \n\nラベル: \((firstResult.identifier))"
            }
        }

        guard let ciImage = CIImage(image: image) else {
            fatalError("Can't convert image.")
        }

        let imageHandler = VNImageRequestHandler(ciImage: ciImage)

        DispatchQueue.global(qos: .userInteractive).async {
            do {
                try imageHandler.perform([request])
            } catch {
                print("Error")
            }
        }
    }
}

ソースコード

https://github.com/hiraku00/ios_camera
('vgg16_transfer.h5'と'vgg16_transfer.mlmodel'は除外)

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cordovaでアプリ起動時の初期化処理を眺めてみる(iOSプラットフォーム)

概要

このエントリでは、Cordovaでアプリ起動時の初期化処理について眺めてみます。

想定読者

このエントリの読者は、以下の方を想定しています。

  • Cordovaで"Hello World"程度のアプリは作って動かしたことがある方
  • Objective-Cをマスターしている必要はないですが、なんとなくコードの雰囲気が分かっている方で、iOSのライフサイクルについても少し知識がある方
  • 言われてみれば、中の動きってどうなってるんだっけ、ということに関心がある方

こんのエントリの内容は、ブレークポイントを貼って追えばわかるものと同等ですが、さっと土地勘を知る目的で使えるかもしれません。

前提

下記を使用しています。

項目 内容
OS macOS 10.13.6(High Sierra)
cordova 9.0.0 (cordova-lib@9.0.1)
Xcode 10.1

準備

cordovaをインストール

$ npm install -g cordova

カラのプロジェクトを作る

$ cordova create gawaNativeTrial io.hrkt.gawaNativeTrial gawaNativeTrial

platformを追加する

$ cd gawaNativeTrial
$ cordova platform add ios

動きを確認して見る

Xcodeのプロジェクトを開く

以下の位置にある、ワークスペースのファイルを開きます。

スクリーンショット 2019-07-14 6.43.26.png

Xcodeではこのような見た目になるでしょう。

スクリーンショット 2019-07-14 6.44.24.png

AppDelegate

iOSのアプリは、AppDelegateがエントリポイントになります。まずここを確認しましょう。

#import "AppDelegate.h"
#import "MainViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    self.viewController = [[MainViewController alloc] init];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

MainViewController

どんなものなのか知るために、ヘッダファイル(MainViewController.h)を眺めていましょう。

#import <Cordova/CDVViewController.h>
#import <Cordova/CDVCommandDelegateImpl.h>
#import <Cordova/CDVCommandQueue.h>

@interface MainViewController : CDVViewController

@end

@interface MainCommandDelegate : CDVCommandDelegateImpl
@end

@interface MainCommandQueue : CDVCommandQueue
@end

CDVViewControllerが親クラスですね。さて、実装クラス(MainViewController.m)はどうなっているでしょうか。

init

- (id)init
{
    self = [super init];
    if (self) {
        // Uncomment to override the CDVCommandDelegateImpl used
        // _commandDelegate = [[MainCommandDelegate alloc] initWithViewController:self];
        // Uncomment to override the CDVCommandQueue used
        // _commandQueue = [[MainCommandQueue alloc] initWithViewController:self];
    }
    return self;
}

アプリをテンプレートから生成した初期状態では特に何もしておらず、親クラスが処理を実施してくれています。
実際のところ、Cordovaの想定した使い方を外れていない時には、こういったフレームワーク側のコードの中に自分で何か書くことはないでしょう。

親クラスは、開いているワークスペースで、下記の位置にあります。

スクリーンショット 2019-07-14 6.57.35.png

https://github.com/apache/cordova-ios/blob/master/CordovaLib/Classes/Public/CDVViewController.m

initは下記のようになっており、別のメソッドを呼んでいます。

- (id)init
{
    self = [super init];
    [self __init];
    return self;
}

さて、__initですが、

- (void)__init
{
    if ((self != nil) && !self.initialized) {

まず、2つのオブジェクトを作っていますね。コマンド実行のためのキュート、実行のためのデリゲートです。アプリ内でのメッセージ受け渡しのためのNSNotificationCenterに、

        _commandQueue = [[CDVCommandQueue alloc] initWithViewController:self];
        _commandDelegate = [[CDVCommandDelegateImpl alloc] initWithViewController:self];

その後、アプリ内でのイベントの通知機構のNSNotificationCenterに対して、ライフサイクルでの各イベントの時に何を実施したいかについて、一連のハンドラを登録しています。

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillTerminate:)
                                                     name:UIApplicationWillTerminateNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillResignActive:)
                                                     name:UIApplicationWillResignActiveNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidBecomeActive:)
                                                     name:UIApplicationDidBecomeActiveNotification object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillEnterForeground:)
                                                     name:UIApplicationWillEnterForegroundNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground:)
                                                     name:UIApplicationDidEnterBackgroundNotification object:nil];

それぞれ、selectorで指定された先のメソッドを読むとどんなことをしているのかが観察できます。一つだけ例をとり、"onAppWillTerminate"を眺めて見ましょう。

/*
 This method lets your application know that it is about to be terminated and purged from memory entirely
 */
- (void)onAppWillTerminate:(NSNotification*)notification
{
    // empty the tmp directory
    NSFileManager* fileMgr = [[NSFileManager alloc] init];
    NSError* __autoreleasing err = nil;

    // clear contents of NSTemporaryDirectory
    NSString* tempDirectoryPath = NSTemporaryDirectory();
    NSDirectoryEnumerator* directoryEnumerator = [fileMgr enumeratorAtPath:tempDirectoryPath];
    NSString* fileName = nil;
    BOOL result;

    while ((fileName = [directoryEnumerator nextObject])) {
        NSString* filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName];
        result = [fileMgr removeItemAtPath:filePath error:&err];
        if (!result && err) {
            NSLog(@"Failed to delete: %@ (error: %@)", filePath, err);
        }
    }
}

アプリが終了する時に、一時ディレクトリを消す処理が入っています。
このような形で、随所でCordova側が「いい感じの」処理を実行してくれているので、Cordovaの開発者はJavaScript側の実装だけ書けば良い(ことをCordovaは目指している)わけですね。

「CordovaがiOSアプリケーションのライフサイクルで何やってるんだっけ?」ということについて知りたい場合は、このあたりから読み始めるとヒントがあります。

で、そのあとはいくつかのINFOレベルの情報をログに書いて__initはおしまいです。

viewDidLoad

アプリのライフサイクルにより、コントローラのviewDidLoadが呼ばれます。
まず、loadSettingsにて、Cordovaの設定を読み込んでいます。

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
    [super viewDidLoad];

    // Load settings
    [self loadSettings];

loadSettings

loadSettingsの中では、CDVConfigParserを使い、Cordovaの設定ファイルを読み込んでいます。使用プラグインや、指定がなかった場合の各種デフォルト値などが設定されます。このあたりで、デフォルトでは、wwwフォルダ内のindex.htmlを使うことなどが指定されています。

- (void)loadSettings
{
    CDVConfigParser* delegate = [[CDVConfigParser alloc] init];

    [self parseSettingsWithParser:delegate];

    // Get the plugin dictionary, whitelist and settings from the delegate.
    self.pluginsMap = delegate.pluginsDict;
    self.startupPluginNames = delegate.startupPluginNames;
    self.settings = delegate.settings;

    // And the start folder/page.
    if(self.wwwFolderName == nil){
        self.wwwFolderName = @"www";
    }
    if(delegate.startPage && self.startPage == nil){
        self.startPage = delegate.startPage;
    }
    if (self.startPage == nil) {
        self.startPage = @"index.html";
    }

    // Initialize the plugin objects dict.
    self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20];
}

そうこうして、viewDidLoadの続きに戻ります。

ついに、WebViewを作る時がきたようです。

    // // Instantiate the WebView ///////////////

    if (!self.webView) {
        [self createGapView];
    }

飛び先のメソッドは、

- (void)createGapView
{
    CGRect webViewBounds = self.view.bounds;

    webViewBounds.origin = self.view.bounds.origin;

    UIView* view = [self newCordovaViewWithFrame:webViewBounds];

さらに先のメソッドnewCordovaViewWithFrameへ。

- (UIView*)newCordovaViewWithFrame:(CGRect)bounds
{
    NSString* defaultWebViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaDefaultWebViewEngine"];
    NSString* webViewEngineClass = [self.settings cordovaSettingForKey:@"CordovaWebViewEngine"];

    if (!defaultWebViewEngineClass) {
        defaultWebViewEngineClass = @"CDVUIWebViewEngine";
    }
    if (!webViewEngineClass) {
        webViewEngineClass = defaultWebViewEngineClass;
    }

    // Find webViewEngine
    if (NSClassFromString(webViewEngineClass)) {
        self.webViewEngine = [[NSClassFromString(webViewEngineClass) alloc] initWithFrame:bounds];
        // if a webView engine returns nil (not supported by the current iOS version) or doesn't conform to the protocol, or can't load the request, we use UIWebView
        if (!self.webViewEngine || ![self.webViewEngine conformsToProtocol:@protocol(CDVWebViewEngineProtocol)] || ![self.webViewEngine canLoadRequest:[NSURLRequest requestWithURL:self.appUrl]]) {
            self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds];
        }
    } else {
        self.webViewEngine = [[NSClassFromString(defaultWebViewEngineClass) alloc] initWithFrame:bounds];
    }

    if ([self.webViewEngine isKindOfClass:[CDVPlugin class]]) {
        [self registerPlugin:(CDVPlugin*)self.webViewEngine withClassName:webViewEngineClass];
    }

    return self.webViewEngine.engineWebView;
}

ここで作られているCDVWebViewEngineProtocolを実装したクラスは、以下の位置にあります。

スクリーンショット 2019-07-14 8.54.39.png

registerPluginでは、下記のような処理が走り、ViewControllerやCommandDelegateからの呼び出しに答えるプラグインである場合、呼び出しに必要な登録処理を実施しています。

プラグインの管理マップに登録し、初期化を走らせます。

- (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName
{
    if ([plugin respondsToSelector:@selector(setViewController:)]) {
        [plugin setViewController:self];
    }

    if ([plugin respondsToSelector:@selector(setCommandDelegate:)]) {
        [plugin setCommandDelegate:_commandDelegate];
    }

    NSString* className = NSStringFromClass([plugin class]);
    [self.pluginObjects setObject:plugin forKey:className];
    [self.pluginsMap setValue:className forKey:[pluginName lowercaseString]];
    [plugin pluginInitialize];
}

今の流れでは、WebViewEngineプラグイン(CDVUIWebViewEngine)の初期化が呼ばれるので、そこを眺めてみましょう。

- (void)pluginInitialize
{
    // viewController would be available now. we attempt to set all possible delegates to it, by default

    UIWebView* uiWebView = (UIWebView*)_engineWebView;

    if ([self.viewController conformsToProtocol:@protocol(UIWebViewDelegate)]) {
        self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:(id <UIWebViewDelegate>)self.viewController];
        uiWebView.delegate = self.uiWebViewDelegate;
    } else {
        self.navWebViewDelegate = [[CDVUIWebViewNavigationDelegate alloc] initWithEnginePlugin:self];
        self.uiWebViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self.navWebViewDelegate];
        uiWebView.delegate = self.uiWebViewDelegate;
    }

    [self updateSettings:self.commandDelegate.settings];
}

ViewControllerが使えるようになっているので、必要なデリゲートを追加しています。
ページがNavigation付きかどうかで処理を振り分け、Navigationの時にはそれようのデリゲートを指定しています。

補足:この部分などの動きで、実際どちらを走っているかについては、
ブレークポイントを貼って実行して調べられます。止まったところで、デバッガのウインドウから
「p self.viewController」で調べると、下記とわかります。

(lldb) p self.viewControllerK
(MainViewController *) $1 = 0x00007fe020e08a10

一番最後の行のupdateSettingsでは、WebViewに対する初期設定(ViewPortScaleを許すかどうか、メディア再生を許すかどうか、など)

- (void)updateSettings:(NSDictionary*)settings
{

さて、CDVViewController.mのviewDidLoadに戻りまして、URLを指定してページをロードするリクエストを出します。

 // /////////////////
    NSURL* appURL = [self appUrl];
    __weak __typeof__(self) weakSelf = self;

    [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) {
        // Fix the memory leak caused by the strong reference.
        [weakSelf setLockToken:lockToken];
        if (appURL) {
            NSURLRequest* appReq = [NSURLRequest requestWithURL:appURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
            [self.webViewEngine loadRequest:appReq];

WebViewEngineのloadRequestを見て見ましょう。

- (id)loadRequest:(NSURLRequest*)request
{
    [(UIWebView*)_engineWebView loadRequest:request];
    return nil;
}

UIWebViewのloadRequestが呼ばれていますね。
UIWebViewのリクエストが走り、このなかのイベントに対応したデリゲートが呼び出されていきます。

CDVUIWebViewDelegate

UIWebViewDelegate

UIWebViewDelegateに従ってアプリケーションフレームワーク側から呼び出される処理を順番に見ていきます。
「gap://」で始まるURLであれば、Cordovaのコマンドキューに入れる処理が見られますが、
JS->ネイティブ側の呼び出しは、このようなURLベースで実現しています。
このエントリの流れでは、ページをロードしているのでここは通りません。

- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL* url = [request URL];
    CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController;

    /*
     * Execute any commands queued with cordova.exec() on the JS side.
     * The part of the URL after gap:// is irrelevant.
     */
    if ([[url scheme] isEqualToString:@"gap"]) {
        [vc.commandQueue fetchCommandsFromJs];
        // The delegate is called asynchronously in this case, so we don't have to use
        // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes.
        [vc.commandQueue executePending];
        return NO;
    }

ロードの際、プラグインで処理したい対象かどうかを判定しています(tel:などはこのあとの後続処理でメインのWebView上で取り扱います)。

    /*
     * Give plugins the chance to handle the url
     */
    BOOL anyPluginsResponded = NO;
    BOOL shouldAllowRequest = NO;

    for (NSString* pluginName in vc.pluginObjects) {
        CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName];
        SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:");
        if ([plugin respondsToSelector:selector]) {
            anyPluginsResponded = YES;
            shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, request, navigationType));
            if (!shouldAllowRequest) {
                break;
            }
        }
    }

    if (anyPluginsResponded) {
        return shouldAllowRequest;
    }

その後いくつかの処理があり、webViewDidFinishLoadが呼ばれます。

/**
 Called when the webview finishes loading.  This stops the activity view.
 */
- (void)webViewDidFinishLoad:(UIWebView*)theWebView
{
    NSLog(@"Finished load of: %@", theWebView.request.URL);
    CDVViewController* vc = (CDVViewController*)self.enginePlugin.viewController;

    // It's safe to release the lock even if this is just a sub-frame that's finished loading.
    [CDVUserAgentUtil releaseLock:vc.userAgentLockToken];

    /*
     * Hide the Top Activity THROBBER in the Battery Bar
     */
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];

    [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPageDidLoadNotification object:self.enginePlugin.webView]];

メソッド末尾で「CDVPageDidLoadNotification」のNSNotirficationを使っていますが、これは次に示すプラグインでハンドラが登録されています。

JavaScript側

JavaScript側では、index.html上にあるcordova.jsの中で各種初期化が実施され、最後にユーザ側のコードに"deviceready"イベントを送る流れとなります。

cordova.jsは、そのまま読んでいっても読める形ではありますが、https://github.com/apache/cordova-jsの「Project Structure」にある一言説明を参照しながら、1つのファイルにビルドされるまえのソースコード読むとわかりやすいかもしれません。

"How It Works"のあたりに各モジュールの説明があります。
Cordovaフレームワーク側でのイベントの伝搬は、channel.jsでpub-sub型のチャネルを実装し、実現されています。アプリ側で、実行準備完了のイベントである"deviceready"もこのチャネルで伝搬されるものです。

各種初期化を担当しているinit.js
の最後の方に、以下のようなコードがあります。

     // Fire onDeviceReady event once page has fully loaded, all |
     // constructors have run and cordova info has been received from native 
     // side. 
     channel.join(function () { 
         require('cordova').fireDocumentEvent('deviceready'); 
     }, channel.deviceReadyChannelsArray); 

これが、pub-subのチャネルを通して、JavaScript側のイベントリスナーに届くことにより、Cordovaがアプリケーションとして実行可能になった状態となります。

おわりに

Cordovaでアプリ起動時の初期化処理を眺めてみました。
Cordovaを使っていて動きがもう少し知りたくなった時など、各プラットフォーム側のネイティブの実装も必要な範囲で眺めておくと、理屈がわかり、実装にも役立つと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIViewとCALayerの階層構造

UIViewとCALayerを複数重ねたときの順番でハマったのでメモ。

UIViewとCALayerを用意する

ViewController.swift
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let viewA = UIView()
        viewA.frame = CGRect(x: 30, y: 100, width: 100, height: 100)
        viewA.backgroundColor = UIColor.blue

        let viewB = UIView()
        viewB.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
        viewB.backgroundColor = UIColor.red

        let viewC = UIView()
        viewC.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
        viewC.backgroundColor = UIColor.green

        let layerA = CALayer()
        layerA.frame = CGRect(x: 10, y: 10, width: 80, height: 80)
        layerA.backgroundColor = UIColor.yellow.cgColor

        let layerB = CALayer()
        layerB.frame = CGRect(x: 10, y: 10, width: 80, height: 80)
        layerB.backgroundColor = UIColor.purple.cgColor

        let layerC = CALayer()
        layerC.frame = CGRect(x: 10, y: 10, width: 80, height: 80)
        layerC.backgroundColor = UIColor.brown.cgColor

        /*
         * todo: ここを後述のコードで置き換える
         */
    }
}

CALayer → UIView の順で追加

先に各UIViewにCALayerを追加しておくパターン。

viewA.layer.addSublayer(layerA)
viewB.layer.addSublayer(layerB)
viewC.layer.addSublayer(layerC)

view.addSubview(viewA)
viewA.addSubview(viewB)
viewB.addSubview(viewC)

UIView → CALayer の順で追加

先にUIViewを重ねて、その後にCALayerを追加するパターン

view.addSubview(viewA)
viewA.addSubview(viewB)
viewB.addSubview(viewC)

viewA.layer.addSublayer(layerA)
viewB.layer.addSublayer(layerB)
viewC.layer.addSublayer(layerC)

実行結果

CALayer → UIView UIView → CALayer
Simulator Screen Shot - iPhone Xʀ - 2019-07-13 at 12.19.27.png Simulator Screen Shot - iPhone Xʀ - 2019-07-13 at 12.20.31.png

両方とも同じ結果になると思っていたので、Viewがうまく表示されずハマってしまった。
Debug View Hierarchyで確認するとどちらの実装も同じ表示だった。

まとめ

UIViewとCALayerは追加した順に表示されるっぽい :thinking:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webページを表示する

本記事は「デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介」の子記事です。

目的

StoryboardでWKWebViewを配置し、Webページを読み込んで表示するための操作やコードを紹介します。

開発・実行環境

  • 開発環境:macOS、Xcode(9~10)、Swift(4~5)
  • 実行環境:iOS 11以上

StoryboardでWKWebViewを配置する

まず、Xcodeのナビゲータエリア上部のフォルダアイコン(一番左)をクリックし、表示されるツリーの中の「Main.storyboard」(Storyboardの名前は、みなさんのプロジェクトごとに異なると思います)をクリックし、Storyboard画面を開きます。
ここでは、StoryboardにViewControllerが1つあるものとして以下を記述します。
1

次に、画面右上にあるライブラリアイコン(寛永通宝のような丸の中に四角のアイコン)をクリックすると、UIオブジェクトを選択できるダイアログが表示されます。
2

ダイアログ上部の検索窓で、「web」と入力すると、「WebKit View」が現れます。
3

「WebKit View」をStoryboardのViewControllerへドラッグ&ドロップすると、WKWebViewの配置が完了します
※なお、ここでは説明しませんが、以降の説明のため、WKWebViewが画面いっぱいに表示されるようにレイアウトしておきます。
4

次に、配置したWKWebViewをコードへ紐づけます。
画面右上にある「Show the Assistant editor」アイコン(丸を2つ重ねたアイコン)をクリックすると、Storyboard画面の隣に、ViewControllerのコードが表示されます。この状態で、「control」キーを押しながら、StoryboardのWKWebViewをドラッグし、ViewControllerのコードの方へドロップすると、ダイアログが開きますのでNameにしかるべき名前(ここではwebviewとしています)を入力し、「Connect」ボタンをクリックします。
5

すると、「@IBOutlet ~」なる行が追加されますが、このままでは「Use of undeclared type 'WKWebView'」なるエラーが表示されますので、WebKitをインポートする記述をしておきます。
6

これで、ViewControllerのコードでWKWebViewを扱えるようになりました。

WKWebViewでWebページを表示する

WKWebViewでWebページを読み込み、表示するには、load()メソッドを使います(以下はViewControllerのviewDidLoad()で実行しています)。

ViewController.swift
@IBOutlet weak var webview: WKWebView!

override func viewDidLoad() {
  super.viewDidLoad()
  if let url = URL(string: "https://ojami.net/") {  // URL文字列の表記間違いなどで、URL()がnilになる場合があるため、nilにならない場合のみ以下のload()が実行されるようにしている
    self.webview.load(URLRequest(url: url))
  }
}

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS/Swift/Beginner】JSONをDecodeしてTableViewに表示する

ゴール

APIから取得したJSONの情報をTable Viewに表示する

goal.png

アジェンダ

  1. 下準備
  2. データをfetchする関数を作成
  3. UITableViewControllerを作成
  4. まとめ

1. 下準備

1.1. プロジェクトの作成

xcodeでプロジェクトを作成。Single View Appを選択する

1.2. Codableを使って雛形を作る

Newから新規swiftファイルを作成する
今回は以下のようにCup.swift作成

Cup.swift
import Foundation

struct Cup: Codable {
    var id: Int
    var name: String
    var description: String
    var price: Int
}

ViewController.swiftに以下を追記

ViewController.swift
var cups = [Cup]()

2. データをfetchする関数を作成

2.1. ViewController.swiftに以下の関数を追加する

ViewController.swift
    func fetchData() {
        let url = URL(string: "http://hereComesYourURL/cups")!

        URLSession.shared.dataTask(with: url) { data, response, error
            in
            guard let data = data else {
                print(error?.localizedDescription ?? "Unknown error")
                return
            }

            let decoder = JSONDecoder()

            if let cups = try? decoder.decode([Cup].self, from: data) {
                DispatchQueue.main.async {
                    self.cups = cups
                    self.tableView.reloadData()
                    print("Loaded \(cups.count) cups" )
                }
            } else {
                    print("Unable parse JSON response")
            }
        }.resume()
    }

2.2 作成したfetchData()をviewDidLoad()直下に宣言する

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        fetchData()   
    }

2.3. ここまでのまとめ

ViewController.swiftは以下のようになっているはず

ViewController.swift
import UIKit

class ViewController: UIViewController {
    var cups = [Cup]()

    override func viewDidLoad() {
        super.viewDidLoad()

        fetchData()   
    }

    func fetchData() {
        let url = URL(string: "http://hereComesYourURL/cups")!

        URLSession.shared.dataTask(with: url) { data, response, error
            in
            guard let data = data else {
                print(error?.localizedDescription ?? "Unknown error")
                return
            }

            let decoder = JSONDecoder()

            if let cups = try? decoder.decode([Cup].self, from: data) {
                DispatchQueue.main.async {
                    self.cups = cups
                    self.tableView.reloadData()
                    print("Loaded \(cups.count) cups" )
                }
            } else {
                    print("Unable parse JSON response")
            }
        }.resume()
    }
}

3. UITableViewControllerを作成

3.1. Storyboard編集

Main.storyboardへ移動
既存のViewControllerを選択して削除する
Library(com + shift + L)からTable View Controllerを選択してdrug and drop
librray.png

Storyboard右下の以下のアイコンをクリックしてEmbed In > Navigation Controllerを選択
embedin.png

Table View Cellを選択
tableviewcell.png

Attributes Inspectorでcellにidentifierを設定する。ここでは仮に"Cell"とする
cell.png

Identity InspectorでViewControllerを選択する
viewcontroller.png

3.2. ViewController.swiftの編集

ViewController.swiftに戻る。UIViewControllerをUITableViewControllerに書き換える

ViewController.swift
class ViewController: UITableViewController {

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Intを追記する

ViewController.swift
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cups.count
    }

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellを追加する

ViewController.swift
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let cake = cups[indexPath.row]

        cell.textLabel?.text = "\(cup.name) = $\(cup.price)"
        cell.detailTextLabel?.text = cup.description

        return cell
    }

まとめ

JSONを用意すれば以下のように表示できるはず
goal.png

ViewController.swiftの全量は以下

ViewController.swift
import UIKit

class ViewController: UITableViewController {
    var cups = [Cup]()

    override func viewDidLoad() {
        super.viewDidLoad()

        fetchData()        
    }

    func fetchData() {
        let url = URL(string: "http://hereComesYourURL/cups")!

        URLSession.shared.dataTask(with: url) { data, response, error
            in
            guard let data = data else {
                print(error?.localizedDescription ?? "Unknown error")
                return
            }

            let decoder = JSONDecoder()

            if let cakes = try? decoder.decode([Cup].self, from: data) {
                DispatchQueue.main.async {
                    self.cups = cups
                    self.tableView.reloadData()
                    print("Loaded \(cups.count) cups" )
                }
            } else {
                    print("Unable parse JSON response")
            }
        }.resume()
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cups.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let cup = cups[indexPath.row]

        cell.textLabel?.text = "\(cup.name) = $\(cup.price)"
        cell.detailTextLabel?.text = cup.description

        return cell 
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリが起動している間、画面を表示し続ける(端末をスリープさせない)

本記事は「デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介」の子記事です。

目的

アプリが起動している間、端末をスリープさせず、画面を表示しつづけるためのコードを紹介します。

開発・実行環境

  • 開発環境:macOS、Xcode(9~10)、Swift(4~5)
  • 実行環境:iOS 11以上

コード

画面を表示しつづけたい(その画面が表示されている間は、スリープさせたくない)ViewControllerのviewWillAppear()に、以下を記述します。

ViewController.swift
override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  UIApplication.shared.isIdleTimerDisabled = true  // この行
}

なお、この画面から別の画面に遷移したときに、遷移先の画面ではスリープがされるように(上記の設定が維持されないように)するには、同じViewControllerのviewWillDisappear()に、以下を記述します。

ViewController.swift
override func viewWillDisappear(_ animated: Bool) {
  super.viewWillDisappear(animated)
  UIApplication.shared.isIdleTimerDisabled = false  // この行
}

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ画面をフルスクリーン表示にする(ステータスバーを表示しない)

本記事は「デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介」の子記事です。

目的

アプリ全体として、画面上部にステータスバーを表示せず、フルスクリーンアプリとして実行するための設定を紹介します。

開発・実行環境

  • 開発環境:macOS、Xcode(9~10)、Swift(4~5)
  • 実行環境:iOS 11以上

設定

Xcodeのナビゲータエリア上部のフォルダアイコン(一番左)をクリックし、表示されるツリーの中のプロジェクトルート(一番上)をクリックします。さらに、エディタエリア上部の「General」をクリックし、「Deployment Info」の「Hide status bar」にチェックを入れます。
1

さらに、エディタエリア上部の「Info」をクリックし、「Custom iOS Target Properties」のいずれかの項目にマウスカーソルを乗せると表示される「+」マークをクリックします。
2

開いたプルダウンから「View controller-based status bar appearance」を選び、その値(Value)を「NO」にします。
3

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ナンプレ解答アプリ「ナンプレ自動解答」(iOS版)とその実装機能の紹介

よちよちサンデープログラミングトップへ

はじめに

「ナンプレ自動解答」(iOS版)は、ナンプレ(数独)の問題を写真に撮ると、その問題を自動的に解き、答えを示すアプリです。
そのため、例えばナンプレの懸賞本の大量の問題を解く時間を短縮したり、解答が載っていない問題の答えを確認したり、あるいは自作した問題が正しく答えを導き出せるものであるかどうかを確認したりするのに使えます。
ナンプレ自動解答画面

本記事では、ナンプレ自動解答で実装している各機能をTipsとして解説します。

  • アプリのダウンロードは、こちら
  • Android版「ナンプレ自動解答」の解説記事は、こちら

開発・実行環境

ナンプレ自動解答は、以下の環境で開発・実行しています。

  • 開発環境 : macOS、Xcode(9~10)、Swift(4~5)
  • 実行環境 : iOS 11以上

アプリの構成の概要

ナンプレ自動解答は、2つの画面を持ちます。1つは、ナンプレの問題(9x9のマス目や各マスの数字)を表示し、数字を編集するため操作を受け付ける独自ビューと、機能を呼び出す複数のボタンと、広告(AdMob)が表示されます。もう1つは、ナンプレの問題を写真に撮るためのAVFoundationを使ったカメラ画面です。
本アプリでは、カメラで撮影した写真から、ナンプレの問題(9x9のマス目)を切り出し、さらにその各マスの数字を認識し、解答処理を行う対象データとして取り込みますが、マス目の切り出しにはOpenCVを使い、数字の認識にはOCRライブラリ(Tesseract OCR iOS)を使っています。

実装している機能

本アプリを実現するために、以下の機能を実装しており、それぞれTipsとして解説していきます。
※リンクをクリックするとTips解説記事に飛びます。リンクされていない項目は、記事を鋭意作成中です。

  • OpenCVライブラリを組み込む
  • 写真から、四角形領域を切り出す(OpenCV)
  • OCRライブラリを組み込む(Tesseract OCR iOS)
  • OCR機能で文字を認識する(Tesseract OCR iOS)
  • 独自ビューに描画する(UIView)
  • 独自ダイアログを実装する(UIView)
  • 画面レイアウト決定後に、ビューを再描画する(UIView)
  • 画像を拡縮する(UIImage)
  • StoryBoardで定義した画面(ViewController)をコードから呼び出して開く
  • 独自カメラ機能を実装する(AVFoundation)
  • 非同期処理(別スレッド処理)をする(DispatchQueue)
  • 処理待ちのインジケータ(くるくる)を表示する・消す(UIActivityIndicatorView)
  • Webページを外部ブラウザで表示する(UIApplication)
  • アプリのバージョン番号を取得する(Bundle)
  • デバイスがiPadか否かを判定する(UIDevice)
  • ラベルやボタンのフォントサイズをコードで変更する(UILabel、UIButton)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介

よちよちサンデープログラミングトップへ

はじめに

デジタルサイネージアプリ「Sign!」(iOS版)は、iPadやiPhoneをシンプルなデジタルサイネージとして使うためのアプリです。
WebページやGoogleスライドで作ったスライドショーや、固定のテキストメッセージやTwitterへの投稿をティッカー(流れるテキスト)としてフルスクリーンかつノースリープで表示し続けるもので、複数のページをめくって切り替えられるものです。
そのため、例えば商品POP、レストランのメニュー、展示ブースの看板、電光掲示板などに使えます。
Sign!利用イメージ

  • アプリのダウンロードは、こちら

本記事では、Sign!で実装している各機能をTipsとして解説します。

開発・実行環境

Sign!は、以下の環境で開発・実行しています。

  • 開発環境 : macOS、Xcode(9~10)、Swift(4~5)
  • 実行環境 : iOS 11以上

アプリの構成の概要

Sign!は、Webページやティッカーテキストを表示するためのWebビュー(WKWebView)が1画面に1つ表示されており、複数の画面をページめくり(UIPageViewController)で切り替えられるようになっています。
Webビューには、HTTPS(やHTTP)でアクセスできるWebページやWebスライドショー(Googleスライドなど)の他、文字列をティッカー表示するためにアプリ内部で保持する固定的なHTMLを表示します。
ティッカー表示する文字列としては、ユーザが入力した固定のテキストメッセージの他、Twitterのツイートも表示できます。ツイートは、特定のユーザ名やハッシュタグのものを定期的に検索し、最も新しいもの1つが表示されます。
また、アプリの画面下部には、広告(AdMob)が、画面サイズや向きに応じて異なるサイズで表示されます。

実装している機能

本アプリを実現するために、以下の機能を実装しており、それぞれTipsとして解説していきます。
※リンクをクリックするとTips解説記事に飛びます。リンクされていない項目は、記事を鋭意作成中です。

  • アプリ画面をフルスクリーン表示にする(ステータスバーを表示しない)
  • アプリが起動している間、画面を表示し続ける(端末をスリープさせない)
  • Webページを表示する(WKWebView)
  • 文字列として定義したHTMLをWebページとして表示する(WKWebView)
  • HTTP Webページを読み込めるようにする(WKWebView)
  • Webページに組み込まれた音楽を鳴らせるようにする(WKWebView)
  • アプリ画面に流れる文字列(ティッカー、マーキー)を表示する(WKWebView)
  • Webページの読み込み待ちにインジケータ(くるくる、ナビゲータ)を表示する(WKNavigation)
  • ページめくりで、表示する画面を切り替える(UIPageViewController)
  • アプリ画面の長押しを検出する(UILongPressGestureRecognizer)
  • ダイアログを表示する(UIAlertController)
  • iPadでアクションシート(.actionSheet)が開かない問題に対処する(UIAlertController)
  • ダイアログにテキスト入力欄を設ける(UIAlertController)
  • ダイアログに設けたテキスト入力欄に入力された値を、ダイアログを閉じた後に利用する
  • TwitterKitを組み込む
  • Twitterにログインする(認証を受ける)、ログアウトする(TwitterKit)
  • Twitterのツイートを取得する(TwitterKit)
  • タイマーを使って、定期的に処理を行う(Timer)
  • AdMob SDKを組み込む
  • アプリ画面に広告を表示する(AdMob)
  • アプリ画面の下部に、広告をレイアウトする
  • ページをめくっても、広告が再読込されないように広告を配置する
  • 表示する文字列を多言語対応する
  • アプリのアイコンを設定する
  • アプリの設定を保持する(UserDefaults)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よちよちサンデープログラミング(トップ)

本記事群について

本記事群は、私自身が開発したスマートフォン(Android、iOS)やWebのアプリで実装した機能をコードや図表で解説します。
これにより、プログラミング初心者の方にとっての、アプリを開発する上で役に立つTips集となることを目指します。

解説の対象となるアプリは、以下の通りです。(2019.7.14現在)

Sign! (iOS版)

iPadやiPhoneをシンプルなデジタルサイネージとして使うためのアプリです。
WebページやGoogleスライドで作ったスライドショーや、固定のテキストメッセージやTwitterへの投稿をティッカー(流れるテキスト)としてフルスクリーンかつノースリープで表示できるもので、例えば商品POP、レストランメニュー、展示ブースの看板、電光掲示板などに使えるものです。
Xcode(10)、Swift(5)で開発し、ページめくり、Web表示、タイマー、多言語対応、Twitter SDK、広告(AdMob SDK)などの機能を実装しています。
Sign!利用イメージ

ナンプレ自動解答 (iOS版)

ナンプレ(数独)の問題を写真に撮ると、その解答が得られるアプリです。
Xcode(10)、Swift(5)で開発し、独自ビュー、非同期処理、カメラ機能(AVFoundation)、OpenCV、OCR(Tesseract OCR iOS)、広告(AdMob SDK)などの機能を実装しています。
ナンプレ自動解答画面

ナンプレ自動解答 (Android版)

iOS版と同様の機能を実装しています。OCRにはtess-twoを使用しています。
Android Studio(3.4)、Javaで開発しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む