- 投稿日:2019-07-14T22:53:10+09:00
enum は MECE である
先日
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" となります。
- 投稿日:2019-07-14T21:15:43+09:00
url(forResource:withExtension:) のありがたい仕様(swift)
はじめに
タイトル通り。swift の
Bundle
のurl(forResource:withExtension:)
の動作についてです。動作
Xcode のプロジェクトに下記のようにファイルを追加します。
ファイル名と拡張子指定
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)引数を無しにします。
url1
もurl2
も当然nil
になります。ファイル名のみ指定(拡張子なし)
let url1 = Bundle.main.url(forResource: "test", withExtension: "") let url2 = Bundle.main.url(forResource: "test", withExtension: nil)引数の拡張子指定を無しにします。
url1
もurl2
もnil
になります。ファイル名のみ指定(拡張子あり)
let url1 = Bundle.main.url(forResource: "test.json", withExtension: "") let url2 = Bundle.main.url(forResource: "test.json", withExtension: nil)引数のファイル名に拡張子まで指定し、引数の拡張子を無しにします。
url1
もurl2
もtest.json
の URL が取得できます。拡張子のみ指定
let url1 = Bundle.main.url(forResource: "", withExtension: "json") let url2 = Bundle.main.url(forResource: nil, withExtension: "json")引数のファイル名指定を無しにします。
url1
もurl2
もtest.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
でも良さそうなのに...
- 投稿日:2019-07-14T19:37:02+09:00
【Swift】Protocol で宣言したオブジェクトの等価評価を AnyHashable でシンプルにする
動機づけ
次のようにプロトコルで宣言したオブジェクトの等価性を扱いたいとします。
FooProtocol.swiftpublic protocol FooProtocol { var foo: String { get } var bar: Int { get } var baz: Double { get } }FooProtocolTest.swiftclass 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.swiftpublic protocol FooProtocol: Equatable { var foo: String { get } var bar: Int { get } var baz: Double { get } }このプロトコルに適合することで、
==
で評価することができるようになります。Hoge.swiftpublic 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.swiftpublic 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) // コンパイルエラー
Hoge
とFuga
がそれぞれEquatable
に準拠しているだけなので、自分自身と同じ型でなければ==
による比較ができないためです。また、プロトコルを
Equatable
に準拠させたので、冒頭のテストコードはコンパイルエラーになります。FooProtocolTest.swiftclass 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 を使用すると上記のような型宣言ができなくなります。
Equatable
がSelf
を使用したプロトコルなので、上記の通りコンパイルエラーになります。public protocol Equatable { static func == (lhs: Self, rhs: Self) -> Bool }
Equatable
準拠の問題点まとめ失敗作の問題点をまとめると次の2点になります。
FooProtocol
で定義したプロパティの等価性ではなく、適合先の構造体の等価性になってしまったFooProtocol
がSelf
を使用したプロトコルなので、型宣言ができなくなってしまった上記から、
FooProtocol
自体をEquatable
のサブプロトコルにしてしまうのは、問題の解決にならないことがわかりました。【成功】
AnyHashable
を利用する冒頭の
XCTAssertEqual(hoge, fuga)
のように、FooProtocol
自体をEquatable
にすることは諦めて、
次のような作戦をとることにしました。XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)この
fooHashable
の部分が「プロトコルで定義されているプロパティを内部的に保持している何か」で、Equatable
に準拠していているようにします。具体的には、次のような実装になります。
FooProtocol.swiftpublic 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.swiftclass 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) } }ということで、コードを少し簡潔にすることができました。
また、この実装は、
Hoge
やFuga
自体の等価性と無関係な実装ですので、必要に応じて適合先の構造体やクラスでEquatable
に準拠することも可能です。そうすると、次のように
FooProtocol
とHoge
の2通りの意味での等価評価が可能になります。Hoge.swiftpublic 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
を型消去した等価評価のためのオブジェクトとしても利用できることが分かりました。プロパティの個数が多い場合や、テストケースを増やしたい場合などに役に立ちそうなテクニックですね。
- 投稿日:2019-07-14T18:07:21+09:00
Kerasで作成したモデルを変換してiOSに組み込み
やること
前回記事(【Tensorflow・VGG16】転移学習による画像分類)で作成したモデルを使ってiPhone上で写真を撮影し、それを推定してみる。
手順概要
- Kerasで作成したモデル(.h5)を変換
- Xcodeでプロジェクトを作成
- 画面のパーツ作成 & コードの関連付け
- カメラへのアクセス許可 & カメラを起動するコードを追加
- ボタン(写真撮影)押下時の挙動
- モデルを読み込んで推定
動作環境
- 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」を選択
- 「Single View App」を選択
- 'Product Name'は任意の名称を付ける。Languageは「Swift」、チェックボックス(Use XX, include XX)は全て外し、Nextボタンを押下する
3. 画面のパーツ作成 & コードの関連付け
Xcode上で下記3つの画面のパーツを追加する
- Image View(写真を表示する領域)
- Text View(推定結果を表示する領域)
- Button(カメラを起動するボタン)
各パーツの関連付けを実施する
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」を追加し、カメラへのアクセスを許可する
下記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)
afterfunc 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のプロジェクトにドラッグ・アンド・ドロップする
- 下記ライブラリを追加する
- CoreML : 機械学習のライブラリ
- Vision : 画像ファイルを扱うライブラリ
ライブラリの追加import CoreML import Vision
- 推定処理を加える
- imagePickerControllerの後ろに推定処理(imagePrediction)を加える
- 引数に画像ファイルを指定(画面上に表示した内容と同じ(imageDisplay.image))
beforefunc imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { imageDisplay.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage imagePicker.dismiss(animated: true, completion: nil) }afterfunc 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.swiftimport 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'は除外)参考文献
- 投稿日:2019-07-14T12:39:13+09:00
UIViewとCALayerの階層構造
UIViewとCALayerを複数重ねたときの順番でハマったのでメモ。
UIViewとCALayerを用意する
ViewController.swiftimport 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 両方とも同じ結果になると思っていたので、Viewがうまく表示されずハマってしまった。
Debug View Hierarchy
で確認するとどちらの実装も同じ表示だった。
まとめ
UIViewとCALayerは追加した順に表示されるっぽい
- 投稿日:2019-07-14T11:53:48+09:00
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つあるものとして以下を記述します。
次に、画面右上にあるライブラリアイコン(寛永通宝のような丸の中に四角のアイコン)をクリックすると、UIオブジェクトを選択できるダイアログが表示されます。
ダイアログ上部の検索窓で、「web」と入力すると、「WebKit View」が現れます。
「WebKit View」をStoryboardのViewControllerへドラッグ&ドロップすると、WKWebViewの配置が完了します
※なお、ここでは説明しませんが、以降の説明のため、WKWebViewが画面いっぱいに表示されるようにレイアウトしておきます。
次に、配置したWKWebViewをコードへ紐づけます。
画面右上にある「Show the Assistant editor」アイコン(丸を2つ重ねたアイコン)をクリックすると、Storyboard画面の隣に、ViewControllerのコードが表示されます。この状態で、「control」キーを押しながら、StoryboardのWKWebViewをドラッグし、ViewControllerのコードの方へドロップすると、ダイアログが開きますのでNameにしかるべき名前(ここではwebviewとしています)を入力し、「Connect」ボタンをクリックします。
すると、「@IBOutlet ~」なる行が追加されますが、このままでは「Use of undeclared type 'WKWebView'」なるエラーが表示されますので、WebKitをインポートする記述をしておきます。
これで、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)) } }以上です。
- 投稿日:2019-07-14T11:53:44+09:00
【iOS/Swift/Beginner】JSONをDecodeしてTableViewに表示する
ゴール
APIから取得したJSONの情報をTable Viewに表示する
アジェンダ
- 下準備
- データをfetchする関数を作成
- UITableViewControllerを作成
- まとめ
1. 下準備
1.1. プロジェクトの作成
xcodeでプロジェクトを作成。Single View Appを選択する
1.2. Codableを使って雛形を作る
Newから新規swiftファイルを作成する
今回は以下のようにCup.swift作成Cup.swiftimport Foundation struct Cup: Codable { var id: Int var name: String var description: String var price: Int }ViewController.swiftに以下を追記
ViewController.swiftvar cups = [Cup]()2. データをfetchする関数を作成
2.1. ViewController.swiftに以下の関数を追加する
ViewController.swiftfunc 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.swiftoverride func viewDidLoad() { super.viewDidLoad() fetchData() }2.3. ここまでのまとめ
ViewController.swiftは以下のようになっているはず
ViewController.swiftimport 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
Storyboard右下の以下のアイコンをクリックしてEmbed In > Navigation Controllerを選択
Attributes Inspectorでcellにidentifierを設定する。ここでは仮に"Cell"とする
Identity InspectorでViewControllerを選択する
3.2. ViewController.swiftの編集
ViewController.swiftに戻る。UIViewControllerをUITableViewControllerに書き換える
ViewController.swiftclass ViewController: UITableViewController {override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Intを追記する
ViewController.swiftoverride func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cups.count }override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellを追加する
ViewController.swiftoverride 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 }まとめ
ViewController.swiftの全量は以下
ViewController.swiftimport 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 } }
- 投稿日:2019-07-14T11:47:06+09:00
アプリが起動している間、画面を表示し続ける(端末をスリープさせない)
本記事は「デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介」の子記事です。
目的
アプリが起動している間、端末をスリープさせず、画面を表示しつづけるためのコードを紹介します。
開発・実行環境
- 開発環境:macOS、Xcode(9~10)、Swift(4~5)
- 実行環境:iOS 11以上
コード
画面を表示しつづけたい(その画面が表示されている間は、スリープさせたくない)ViewControllerのviewWillAppear()に、以下を記述します。
ViewController.swiftoverride func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) UIApplication.shared.isIdleTimerDisabled = true // この行 }なお、この画面から別の画面に遷移したときに、遷移先の画面ではスリープがされるように(上記の設定が維持されないように)するには、同じViewControllerのviewWillDisappear()に、以下を記述します。
ViewController.swiftoverride func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) UIApplication.shared.isIdleTimerDisabled = false // この行 }以上です。
- 投稿日:2019-07-14T11:41:00+09:00
アプリ画面をフルスクリーン表示にする(ステータスバーを表示しない)
本記事は「デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介」の子記事です。
目的
アプリ全体として、画面上部にステータスバーを表示せず、フルスクリーンアプリとして実行するための設定を紹介します。
開発・実行環境
- 開発環境:macOS、Xcode(9~10)、Swift(4~5)
- 実行環境:iOS 11以上
設定
Xcodeのナビゲータエリア上部のフォルダアイコン(一番左)をクリックし、表示されるツリーの中のプロジェクトルート(一番上)をクリックします。さらに、エディタエリア上部の「General」をクリックし、「Deployment Info」の「Hide status bar」にチェックを入れます。
さらに、エディタエリア上部の「Info」をクリックし、「Custom iOS Target Properties」のいずれかの項目にマウスカーソルを乗せると表示される「+」マークをクリックします。
開いたプルダウンから「View controller-based status bar appearance」を選び、その値(Value)を「NO」にします。
以上です。
- 投稿日:2019-07-14T11:28:47+09:00
ナンプレ解答アプリ「ナンプレ自動解答」(iOS版)とその実装機能の紹介
よちよちサンデープログラミングトップへ
はじめに
「ナンプレ自動解答」(iOS版)は、ナンプレ(数独)の問題を写真に撮ると、その問題を自動的に解き、答えを示すアプリです。
そのため、例えばナンプレの懸賞本の大量の問題を解く時間を短縮したり、解答が載っていない問題の答えを確認したり、あるいは自作した問題が正しく答えを導き出せるものであるかどうかを確認したりするのに使えます。
本記事では、ナンプレ自動解答で実装している各機能をTipsとして解説します。
開発・実行環境
ナンプレ自動解答は、以下の環境で開発・実行しています。
- 開発環境 : 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)
- 投稿日:2019-07-14T11:16:32+09:00
デジタルサイネージアプリ「Sign!」(iOS版)とその実装機能の紹介
よちよちサンデープログラミングトップへ
はじめに
デジタルサイネージアプリ「Sign!」(iOS版)は、iPadやiPhoneをシンプルなデジタルサイネージとして使うためのアプリです。
WebページやGoogleスライドで作ったスライドショーや、固定のテキストメッセージやTwitterへの投稿をティッカー(流れるテキスト)としてフルスクリーンかつノースリープで表示し続けるもので、複数のページをめくって切り替えられるものです。
そのため、例えば商品POP、レストランのメニュー、展示ブースの看板、電光掲示板などに使えます。
- アプリのダウンロードは、こちら。
本記事では、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)