20190714のSwiftに関する記事は11件です。

enum は MECE である

MECE_enum.gif
先日
enum はなぜ便利なのか
というタイトルで投稿しました。
今回は MECE な性質がある enum について書きます。

MECE とは?

Mutually Exclusive and Collectively Exhaustive の頭字語でミーシーと読みます。
「重複がなく漏れもない」ことを表します。5択のマークシート試験の答案みたいなもので、5つの選択肢の中に必ず1つだけ正解があるようなものです。
日・月・火・水・木・金・土 などの曜日も MECE な性質があり、どの日も必ずどれか1つの曜日に当てはまります。

Xcode のパーツでは UISegmentedControl も必ずどれか1つの項目が選択されているので MECE な性質であると言えます。

enum で MECE なものの項目を表現する

MECE の「複数の選択肢の中から必ずどれか1つが選択される」という性質を表すのにもってこいなのが enum です。

デフォルトの rawValue

swift では rawValue の記載を省略した場合、以下のようになります。

enum Signal: Int {
        case blue
        case red
        case yellow
    }

とすると、それぞれの rawValue は 0, 1, 2 となります。

enum Signal: String {
        case blue
        case red
        case yellow
    }

とすると、それぞれの rawValue は "blue", "red", "yellow" となります。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【Swift】Protocol で宣言したオブジェクトの等価評価を AnyHashable でシンプルにする

動機づけ

次のようにプロトコルで宣言したオブジェクトの等価性を扱いたいとします。

FooProtocol.swift
public protocol FooProtocol {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}
FooProtocolTest.swift
class FooProtocolTest: XCTestCase {

    var hoge: FooProtocol!
    var fuga: FooProtocol!

    func test_example() {
        hoge = ... // FooProtocol プロトコルに準拠したオブジェクト
        fuga = ... // FooProtocol プロトコルに準拠したオブジェクト

        XCTAssertEqual(hoge.foo, fuga.foo)
        XCTAssertEqual(hoge.bar, fuga.bar)
        XCTAssertEqual(hoge.baz, fuga.baz)
    }
}

このように、プロパティ各々を比較するのではなく、 XCTAssertEqual(hoge, fuga) のようにすることで、プロトコルで定義したプロパティをまとめて比較できれば便利そうです。

そのような単純化の方法について考えてみたいと思います。

【失敗】Equatable 準拠

Swift で等価性を扱う場合、通常は Equatable に準拠することになります。
しかし、この方法は簡単には上手くいきません。

たとえば、次のように Equatable のサブプロトコルとして定義します。

FooProtocol.swift
public protocol FooProtocol: Equatable {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}

このプロトコルに適合することで、== で評価することができるようになります。

Hoge.swift
public struct Hoge: FooProtocol {
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var qux: [String]
}
let hoge1 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
print(hoge1 == hoge2) // true

しかし、これは Hoge としての等価性であって、FooProtocol で定義されたプロパティの等価性ではありません。
上記の場合は、FooProtocol で定義されていない変数 qux まで評価されます。

また、次のように FooProtocol に適合した別の型との比較はできません。

Fuga.swift
public struct Fuga: FooProtocol {
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var quux: [Int : String]
}
let hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge == fuga) // コンパイルエラー

HogeFuga がそれぞれ Equatable に準拠しているだけなので、自分自身と同じ型でなければ == による比較ができないためです。

また、プロトコルを Equatable に準拠させたので、冒頭のテストコードはコンパイルエラーになります。

FooProtocolTest.swift
class FooProtocolTest: XCTestCase {
    // 次の理由でコンパイルエラー
    // Protocol 'FooProtocol' can only be used as a generic constraint 
    // because it has Self or associated type requirements
    var hoge: FooProtocol!
    var fuga: FooProtocol!
}

Self または associated type を使用すると上記のような型宣言ができなくなります。
EquatableSelf を使用したプロトコルなので、上記の通りコンパイルエラーになります。

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

Equatable 準拠の問題点まとめ

失敗作の問題点をまとめると次の2点になります。

  • FooProtocol で定義したプロパティの等価性ではなく、適合先の構造体の等価性になってしまった
  • FooProtocolSelf を使用したプロトコルなので、型宣言ができなくなってしまった

上記から、FooProtocol 自体を Equatable のサブプロトコルにしてしまうのは、問題の解決にならないことがわかりました。

【成功】AnyHashable を利用する

冒頭の XCTAssertEqual(hoge, fuga) のように、FooProtocol 自体を Equatable にすることは諦めて、
次のような作戦をとることにしました。

XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)

この fooHashable の部分が「プロトコルで定義されているプロパティを内部的に保持している何か」で、Equatable に準拠していているようにします。

具体的には、次のような実装になります。

FooProtocol.swift
public protocol FooProtocol {
    var foo: String { get }
    var bar: Int { get }
    var baz: Double { get }
}

public extension FooProtocol {
    var fooHashable: AnyHashable {
        return Anonymous(foo: foo, bar: bar, baz: baz)
    }
}

private struct Anonymous: FooProtocol, Hashable {
    let foo: String
    let bar: Int
    let baz: Double
}

コードを言葉で表現すると次のようになります。

  • Hashable に適合したプライベートな型(Anonymous)を作る
  • プロトコルのデフォルト実装で AnyHashable にラップして上記のオブジェクトを返す

こうすることで、Anonymous で定義されたプロパティの等価評価をすることになり、実質的には FooProtocol で定義しているプロパティをまとめて評価することができるようになります。

次のように異なる型で宣言していても、FooProtocol の意味での等価評価ができます。

let hoge: Hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga: Fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge.fooHashable == fuga.fooHashable) // true

そして、冒頭のようなプロトコルで宣言したオブジェクトの等価評価も、プロパティの各々を比較する必要がなくなります。

FooProtocolTest.swift
class FooProtocolTest: XCTestCase {

    var hoge: FooProtocol!
    var fuga: FooProtocol!

    // 等価比較:FooProtocol の意味で一致していること
    func test_example_1() {
        hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
        fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
        XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)
    }

    // 等価比較:FooProtocol の意味で一致していないこと
    func test_example_2() {
        // 「ふが」と「ほげほげ」の不一致
        hoge = Hoge(foo: "ふが", bar: 23, baz: 42, qux: ["ほげ"])
        fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
        XCTAssertNotEqual(hoge.fooHashable, fuga.fooHashable)
    }
}

ということで、コードを少し簡潔にすることができました。

また、この実装は、HogeFuga 自体の等価性と無関係な実装ですので、必要に応じて適合先の構造体やクラスで Equatable に準拠することも可能です。

そうすると、次のように FooProtocolHoge の2通りの意味での等価評価が可能になります。

Hoge.swift
public struct Hoge: FooProtocol, Equatable { // Equatable に準拠
    public var foo: String
    public var bar: Int
    public var baz: Double

    public var qux: [String]
}
let hoge1: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ", "ふが"])

// `FooProtocol` の意味での評価は qux が無視されるので true
print(hoge1.fooHashable == hoge2.fooHashable) 

// Hoge の意味での評価は qux が不一致で false
print(hoge1 == hoge2) 

結び

AnyHashable と言えば、[AnyHashable: Any] のような辞書のキーとしての利用をよく見かけますが、Equatable から Self を型消去した等価評価のためのオブジェクトとしても利用できることが分かりました。

プロパティの個数が多い場合や、テストケースを増やしたい場合などに役に立ちそうなテクニックですね。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

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で続きを読む