20191129のiOSに関する記事は11件です。

[はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)

はじめに

iOSアプリを作ってみたいけど
何から始めて良いのかわからない

とりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思う

という記事の5回目です。

今回は、アプリケーションアイコンを設定します。

アイコン画像作成

  1. センスのある人はカッコよく、センスのない人はそれなりの画像を作成
    • はい「それなりの画像」ができました(png形式)
      shinobee-sky-276px.png
    • 【なぜ?】
      • Simulatorに表示されるアプリケーションアイコンが、あまりにもカッコ悪いから変えたい(笑)
        SimulatorIPhone.png
  2. いろいろなサイズを用意
    • Xcode のファイルツリーから [Assets.xcassets]-[AppIcon]を選択して表示される画面に書かれているサイズの画像を用意する
      ApplicationIcon.png
    • 作成した
      IconFiles.png
    • 【なぜ?】
      • Xcodeの[AppIcon]画面には、いくつものサイズのアイコンを配置することができるから
      • 全部必要なのかわからないけど、できるだけ作成
      • ひとつ作って、プレビューアプリのツール機能を使えば簡単にサイズ違いを作成できるよ
      • サイズがわかりやすいようなファイル名を使用
      • なお、最大サイズ 1024x1024 は用意しなかった
  3. ファイルをxcodeの画面にドラッグ&ドロップ
    ApplicationIconSet.png
    • 【なぜ?】
      • これでxcodeに登録される
      • ファイル名からサイズがわかるようにしたのは、我ながらGoodでした
      • なお、サイズ間違いだと警告表示される
  4. テスト実行
    • Xcode 左上の矢印アイコンをクリック
    • アプリを終了して、アプリアイコンを確認
      SimulatorIPhoneChanged.png

今回の到達点

  • アプリケーションアイコンが「それなり」になった

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
  4. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4)
  5. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhone で運転免許証を読み取ってみよう!【ライブラリを使って】

それでは Core NFC を用いた iPhone アプリ開発の例として、運転免許証の読み取りを行います。今回は OSS ライブラリを用いて、Core NFC のコーディングよりも Core NFC を使うときに必要な Xcode プロジェクトでの各設定を中心に説明していきます。

環境

  • 開発
    • Xcode Version 11.2.1 (11B500)
    • Apple Swift version 5.1.2 (swiftlang-1100.0.278 clang-1100.0.33.9)
    • macOS Catalina 10.15.1(19B88)
  • 実機
    • iPhone 11 Pro (A2215、MWCC2J/A)
    • iOS 13.2.3 (17B111)

Xcode プロジェクトの作成

まずは Create a new Xcode project から iOS の Single View App を作成します。

ライブラリの導入

今回は私が開発・公開しているライブラリ、treastrain/TRETJapanNFCReader を使用します。
このライブラリは Swift Package Manager、Carthage、CocoaPods に対応していますが、今回は Carthage を使うことにします。
※運転免許証の読み取りの場合、Swift Package Manager では文字コードの変換を行うことができずに正しく読み取ることができません。

  1. Cartfilegithub "treastrain/TRETJapanNFCReader" と記述して carthage update --platform iOS する。
  2. Project の Target の Build Phases で新しい Run Script Phase を追加し、shell に /usr/local/bin/carthage copy-frameworks を、Input Files に $(SRCROOT)/Carthage/Build/iOS/TRETJapanNFCReader.framework を記述する。スクリーンショット 2019-11-29 21.36.07.png
  3. Project の Target の General で Frameworks, Libraries, and Embbedded Content で Carthage によって追加された TRETJapanNFCReader.framework を追加する。スクリーンショット 2019-11-29 21.53.12.png

Capability と Entitlements の設定

Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
スクリーンショット 2019-11-29 18.50.17.png
すると、「.entitlements」というファイルが追加されているので、「Near Field Communication Tag Reader Session Formats」に「NFC Data Exchange Format」および「NFC tag-specific data protocol」があることを確認します。
スクリーンショット 2019-11-29 19.01.06.png

Info.plist の設定

Info.plist に「Privacy - NFC Scan Usage Description」と「ISO7816 application identifiers for NFC Tag Reader Session」を追加します。
「Privacy - NFC Scan Usage Description」には NFC を何のために使用するのかについての説明を、「ISO7816 application identifiers for NFC Tag Reader Session」の配下には以下の AID を記述します。

  • A0000002310100000000000000000000
  • A0000002310200000000000000000000
  • A0000002480300000000000000000000

スクリーンショット 2019-11-29 20.39.40.png

コーディング

ではいよいよコーディングをしていきます。今回はすべて ViewController.swift に記述します。

ViewController.swift
import UIKit
import TRETJapanNFCReader // ①

class ViewController: UIViewController, DriversLicenseReaderSessionDelegate { // ②

    var reader: DriversLicenseReader! // ③

    override func viewDidLoad() {
        super.viewDidLoad()

        self.reader = DriversLicenseReader(viewController: self) // ④
        self.reader.get(items: DriversLicenseCardItem.allCases, pin1: "1234", pin2: "5678") // ⑤
    }

    func driversLicenseReaderSession(didRead driversLicenseCard: DriversLicenseCard) { // ②・⑥
        let matters = driversLicenseCard.matters!
        let registeredDomicile = driversLicenseCard.registeredDomicile!
        print("氏名", matters.name)
        print("呼び名(カナ)", matters.nickname)
        print("生年月日", matters.birthdate)
        print("本籍", registeredDomicile.registeredDomicile)
        print("住所", matters.address)
        print("交付年月日", matters.issuanceDate)
        print("照会番号", matters.referenceNumber)
        print("有効期間の末日", matters.expirationDate)
        print("免許の条件", matters.condition1, matters.condition2, matters.condition3, matters.condition4)
        print("公安委員会名", matters.issuingAuthority)
        print("免許証の番号", matters.number)
        print("普通免許の年月日", matters.ordinaryVehicleLicenceDate)
        print("中型免許の年月日", matters.mediumVehicleLicenceDate)

        // let image = UIImage(data: driversLicenseCard.photo!.photoData)
    }

    func japanNFCReaderSession(didInvalidateWithError error: Error) { // ②・⑥
        print(error.localizedDescription)
    }
}
  1. ライブラリ TRETJapanNFCReader をインポートします。
  2. ViewControllerDriversLicenseReaderSessionDelegate を適用させます。必要な protocol stubs を追加します。
  3. クラス内に DriversLicenseReader型の変数を用意します。
  4. 3 で用意した変数を初期化します。
  5. reader.get で運転免許証の読み取りに入ります。pin1pin2にはそれぞれ、運転免許証が交付されたときに自分が設定した暗証番号1、暗証番号2を指定します。絶対に間違えないでください(理由は後述)。
  6. 取得した運転免許証のデータを print します。

特に、5 の reader.get は以下のように定義されています。

/// 運転免許証からデータを読み取る
/// - Parameter items: 運転免許証から読み取りたいデータ
/// - Parameter pin1: 暗証番号1
/// - Parameter pin2: 暗証番号2
func get(items: [DriversLicenseCardItem], pin1: String = "", pin2: String = "")

また、ここで指定する DriversLicenseCardItem は以下のように定義されています。

TRETJapanNFCReader/TRETJapanNFCReader/ISO14443/DriversLicense/DriversLicenseCard.swift
/// 日本の運転免許証から読み取ることができるデータの種別
public enum DriversLicenseCardItem: CaseIterable {
    /// MF/EF01 共通データ要素
    case commonData
    /// MF/EF02 暗証番号(PIN)設定
    case pinSetting
    /// DF1/EF01 記載事項(本籍除く)
    case matters
    /// DF1/EF02 記載事項(本籍)
    case registeredDomicile
    /// DF2/EF01 写真
    case photo
}

上記に挙げたサンプルでは DriversLicenseCardItem.allCases としてすべて指定していましたが、運転免許証の仕様上、「記載事項(本籍除く)」の読み取りには暗証番号1が、「記載事項(本籍)」、「写真」の読み取りには暗証番号1と暗証番号2の両方が必要です。さらにその暗証番号は3回間違えると免許証がロックされ、警察署等でロック解除をしてもらわなければ運転免許証を読み取ることができなくなってしまいます。読み取る際は慎重に試しましょう……。

例: 2回誤った暗証番号を入力 → 3回目で正しい暗証番号だった → 暗証番号試行回数は残り3回にリセットされる

コンソールに表示される内容を確認する

それではいよいよ実機の iPhone で実行してみましょう。
暗証番号が正しい場合は情報がずらーっと表示されます。もし、暗証番号が間違っている場合は画面に残りの試行回数が表示されます。
スクリーンショット 2019-11-29 22.17.37.png

まとめ

  • Core NFC で運転免許証を読み取ることができる
  • 暗証番号は間違ってはいけない
  • 実際の利用ケースとしては IDセルフィーの置き換え・補助になるかも

今回のファイルツリー

スクリーンショット 2019-11-29 22.23.35.png

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

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'presentingViewController must be set.' のエラー解決方法【GoogleSignIn for iOS】

SwiftでFirebaseを使ってGoogleログインするときにでたエラーの解決方法です。

原因

出たエラーはこちら

Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'presentingViewController must be set.'

ボタンをクリックしたときグーグル認証されるというもの。
ボタンをタップしたときに上記のエラーが起こる。

googleLogin.py
    @IBAction func loginGoogleButton(_ sender: Any) {
         GIDSignIn.sharedInstance()?.signIn()
    }

解決方法

これを

GIDSignIn.sharedInstance()?.uidelegate = self

こう変える

GIDSignIn.sharedInstance()?.presentingViewController = self

Google Sign inがバージョン5.0.0になったことで色々仕様が変わったようですね。
以下引用
公式ドキュメント
https://developers.google.com/identity/sign-in/ios/release
github
https://github.com/EddyVerbruggen/nativescript-plugin-firebase/issues/1370

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

【Swift】Swiftの"?"と"!",はじめからていねいに (1/2)

はじめに

この記事は明治大学/明治大学大学院 Advent Calendar 2019の7日目の記事です.
お久しぶりです. 昨年度までプログラミング演習を中心にTAを担当していました河野です.卒業後はiOSアプリエンジニアにジョブチェンジして日々修行しています.

今回の投稿では,Swiftの1大テーマである"?"と"!"について一度整理しようと思い,このテーマにしました.既存の解説記事とはやや切り口が異なりますが,個人的にこのまとめ方で理解するのもわかりやすいのでは,と試行錯誤してまとめてみたので,よろしくお願いします.

型の後ろの"?"と"!"

Swiftでは変数を宣言する際に型の後ろに"?"・"!"をつけることがあります.これは変数をオプショナル型として宣言する場合につけるものです.Swiftでは変数にnilを代入することができません.nilの代入を許容するには,変数宣言するときにその変数をオプショナル型変数として宣言する必要があります.Swiftのオプショナル型変数にはオプショナル型(Optional Value)と暗黙的アンラップ型(Implicitly Unwrapped Optional)の2種類があります.

var hoge: Int //Int型(nilを許容しない)
var fuga: Int? //オプショナルInt型(nilを許容する)
var piyo: Int! //暗黙的アンラップInt型(nilを許容する)

オプショナル型変数と非オプショナル型変数は同じInt型であっても異なるデータ型という扱いになります.

var hoge: Int = 10 //Int型(nilを許容しない)
var fuga: Int? = 10 //オプショナルInt型(nilを許容する)
var piyo: Int! = 10 //暗黙的アンラップInt型(nilを許容する)

print(hoge) //10
print(fuga) //Optional(10)
print(piyo) //Optional(10)

したがって,これらを二項演算すると型が異なる変数同士を演算することになるためエラーになります.

var hoge: Int = 10//Int型(nilを許容しない)
var fuga: Int? = 10//オプショナルInt型(nilを許容する)

print(hoge + fuga)

//---- 以下出力されるエラー ----//
error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
print(hoge + fuga)

このとき,この演算を実行するためにはオプショナル型変数に対して,オプショナル変数に格納されている値を取り出すアンラップ (unwrap)という操作が必要になります.

オプショナル型変数のうち暗黙的アンラップ型は演算が実行されるタイミングで自動的に(暗黙的に)アンラップの操作を行います.

var hoge: Int = 10//Int型(nilを許容しない)
var piyo: Int! = 10//暗黙的アンラップInt型(nilを許容する)

print(piyo)//Optional(10)
print(hoge + piyo)//20
print(piyo == 10)//true

しかし,piyo = nilの場合にアンラップを行なった場合,nilと(nilを許容しない)非オプショナル型を二項演算することになるためエラーが発生します.したがって,nilの可能性があるからという理由で闇雲に変数を暗黙的オプショナル型として宣言することは危険です.

var hoge: Int = 10//Int型(nilを許容しない)
var piyo: Int! = nil//オプショナルInt型(nilを許容する)

print(piyo)//nil
print(hoge + piyo)//エラー
print(piyo == 10)//エラー

名前の後ろの"?"と"!"

Swiftのコードに出てくる"?"と"!"のうち,変数やメソッドなどの名前の後ろについてくる"?"と"!"は前述のアンラップに関わる操作を行うための記号です.名前の後ろに"?"や"!"をつける処理としてForced Unwrapping (強制的アンラップ)とOptional Chaining (オプショナルチェイニング)をここではまとめます.

<オプショナル型変数>! ← Forced Unwrapping (強制的アンラップ)
<クラス>.<オプショナル型のプロパティ>?.<プロパティ他>  Optional Chaining (オプショナルチェイニング)

Forced Unwrapping (強制的アンラップ)

Forced Unwrapping (強制的アンラップ)とはオプショナル型変数の中にどんな値が入っていてもアンラップをするという方法です.以下の記法でオプショナル型変数を非オプショナル型変数に変換します.

<オプショナル型変数>!

var fuga: Int? = 10//オプショナルInt型(nilを許容する)

print(fuga)//Optional(10)
print(fuga!)//10

しかし,fugaにnilが入っている場合にもアンラップを強行しようとするため,その場合にはエラーになります.

var fuga: Int? = nil//オプショナルInt型(nilを許容する)

print(fuga!)//エラー

したがって,Forced Unwrappingによるアンラップを行う場合には,その変数に値が必ず格納されていることが保証されていることを確認して,行う必要があります.(公式ドキュメントにもこの"!"について以下のように記述がありました)

The exclamation mark effectively says, “I know that this optional definitely has a value; please use it.”

Optional Chaining (オプショナルチェイニング)

Optional Chaining (オプショナルチェイニング)とはクラスが持つオプショナル型のプロパティやメソッドを安全に呼び出す方法です.以下の例(公式ドキュメントを引用)をみてください.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

このようにPersonクラスを定義した上でPersonクラスのインスタンスjohnのresidence.numberOfRoomsを取得することを考えます.以下の手順でnumberOfRoomsを取得しようとすると,エラーが発生します.

let john = Person()//この時点で特段イニシャライザの処理をしていないのでjohn.residenceはnil
let roomCount = john.residence!.numberOfRooms//john.residence!のアンラップ失敗でnumberOfRoomsが取得できずエラー
print(roomCount)//エラー

上のケースではオプショナル型のオブジェクトがnilを含む可能性のある状況下でForced Unwrappingをしたためにエラーが発生してしまいました.このような場面において,Optional Chainingを活用すれば,より安全にresidence.numberOfRoomsを取得することができます.

Optional Chainingでは以下の記法でプロパティやメソッドを参照します.

<クラス>.<オプショナル型のプロパティ/メソッド>?.<プロパティ/メソッド>...

Optional Chainingは,Forced Unwrappingとは異なり,この<オプショナル型のプロパティ/メソッド>がnilだった時点で,それ以降のプロパティやメソッドを参照せずにnilを返します.

let john = Person()

if john.residence?.numberOfRooms != nil {
    let roomCount = john.residence?.numberOfRooms ?? 0
    print(roomCount)
} else {
    print("住所なし")//こちらが出力される
}

したがって,強制的なアンラップを行わずに安全にオプショナル型のオブジェクトを扱うことができます.上の例でも存在しないかもしれないjohn.residence.numberOfRoomsを直でアクセスせずに,residenceがnilかそうでないかによって後続の処理を変えることができます.

さらに,前述したようなオプショナル型の変数がnilであるかそうでないかによって後続の処理を分岐させるためのより工夫された仕組みがあります.次回はそれらを中心についてまとめていこうと思います.(長くなってしまったので)

次回予告

  • 安全なアンラップ

    • if let
    • guard let
  • asの後ろの"?"と"!"

  • まとめ

参考

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

スマホアプリのバグ調査Tips(勉強会用)

はじめに

バグ調査のやり方が人それぞれ違うなと感じている

私がやってることの共有と、他の知見を集めたいという目的

設計/実装に求められる能力と、バグの原因特定に求められる能力は、別物である

バグ調査は推理ゲーム


例えば、印刷してもプリンターが動かない場合

  • 同じ端末から別のプリンターで印刷してみる
  • 別の端末から同じプリンターで印刷してみる
  • プリンタのプロパティからテスト印刷してみる

まずはこのように原因特定を始める

  • いきなりプリンタドライバーを再インストールしてみる

みたいな闇雲な始め方は望ましくない

参考: 「原因が特定されました!」などと言うのは10年、、いや5ステップほど早い


強制終了する場合

以下は基本

  • Stack trace
  • BreakpointとDebug実行
  • printf的ログ出力を細かく仕込む

以下も使えると良い

参考
Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定
Gradle Build Scanを使ってみる


エラーメッセージが出る場合

以下は基本

  • そのメッセージで全文検索
  • ユーザーに聞く
    1. ユーザーは何をしていたか
    2. ユーザーは何を期待していたか
    3. 実際に何が起きたか

以下も使えると良い

  • Layout Inspector

参考: Layout Inspector を使用してレイアウトをデバッグする


動作が遅い、重い、固まる

以下は基本

以下も使えると良い

参考: Android PからのSystem TracingとPerfettoを使ってパフォーマンスを確認しよう
参考: iOS — Identifying Memory Leaks using the Xcode Memory Graph Debugger


共通していること

  • 現象を再現させる
  • 実装を理解する
    • 人のコードを読む癖をつけておく
    • デバッガーの呼び出し履歴を使う
  • 原因を推理する
    • ケアレスミス(Null、範囲外Index、定数の間違い、演算子の間違い(=/==))
    • 考慮してない入力値
    • 意図しない型キャスト(小数→整数)
    • 複数スレッド非同期実行
    • サーバーレスポンス異常

参考: How to fix bugs, step by step - Software Engineering Tips


共通していること

  • わからなければ二分探索しよう!
    • 一部をばっさりコメントアウトして実行
  • めんどくさければモックを書こう!
    • 同じレスポンスをサーバーで用意するのはとっても非効率
    • 実体←→モックの差し替えが容易かどうかはアーキテクチャー次第
  • 上手くいってる修正方法を探そう!
    • GitHubはオープンソースの集まりで、トラブルシューティングの宝庫
  • 英文に慣れよう!
    • トラブルシューティングは大体英文
    • 言語の壁が、探せる範囲の壁を作る
    • 知らない言語でもそこに唯一の解決策があるかもしれないので、Translatorを使おう(Chromeアドオンおすすめ)
    • とはいえ、怪しいサイトには注意
    • Forumの回答で"same problem"はNGワード扱い

参考: 【バグ発見のコツ】二分探索でバグ発生個所を特定する
参考: クリーンアーキテクチャーでスマホアプリ開発した感想


まとめ

バグとの戦いは開発言語の知識よりも、頭の中に引き出しがどれくらいあるかの方が大事だと思っています。

ちなみに僕はF1とかのモタスポが好きですが、F1でも車が予想外の挙動を示した時に、引き出しが多くて対応できるドライバー(どんな車でもそこそこ速いタイプ)と、そうじゃないドライバー(車を完璧に仕上げることが必須のタイプ)がいると思って見てます。どちらを目指すかは人それぞれ自由ですが、個人的には前者がいいな〜と思います。


さいごに

若手エンジニア向けの発表でしたが、私も今のお仕事は2年目でまだまだ無知な若手エンジニアなので、足りない観点があればぜひ教えてください!

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

【Swift】画面遷移時の値・処理の受け渡し方法(俯瞰用)

先日投稿した画面遷移で書ききれなかったところについて、続きを書き加えます。
先日の記事と同様に備忘録の意味合いが強めです。

先日の記事はこちら:【Swift】画面遷移の方法(俯瞰用)

動作環境

  • macOS : Mojave 10.14.6
  • Swift : 5.0.1
  • iOS : 13.1.3
  • Xcode : 11.0

前回のポイント - 代表的な画面遷移の方法

画面遷移の実装方法は主に以下の三つ。
A. InterfaceBuilder(ボタンと画面をSegueで接続)
B. InterfaceBuilder(Segue Identifier) + コーディング
C. InterfaceBuilder(Storyboard ID) + コーディング

今回書く内容

  • 遷移先画面への値渡しの方法
  • 遷移先の結果を遷移元に反映させる方法

遷移先画面への値渡しの方法

基本的な考え方

基本的に遷移先への値の渡し方は以下の手順
(A_ViewController -> B_ViewController に遷移することを考えます)

遷移先のB_ViewControllerの実装
1. 遷移先に渡したい値を格納する変数を用意する

遷移元のA_ViewControllerの実装
2. 遷移先のViewControllerを取得
3. 1で用意した遷移先の変数に値を渡す
4. 画面遷移実行

上記はSegueにIDをつけてperformSegueメソッドを使用する場合でも、StoryboardIDをつけpresentメソッドを呼び出すような場合でも同様です。

Bパターン【InterfaceBuilder(Segue Identifier) + コーディング】での実装

画像のように遷移元A_ViewControllerでテキストフィールドに値を入力して、
遷移先のB_ViewControllerで表示するような場合を考えます。

まず画像右上の赤枠部分のStoryboard Segue -> IdentifierにsegueのIDを付与します。
今回はtoBViewControllerとしました。

image.png

遷移先のB_ViewControllerの実装

B_ViewController
import UIKit

class B_ViewController: UIViewController {

    @IBOutlet weak var outputLabel: UILabel!

    // 1. 遷移先に渡したい値を格納する変数を用意する
    var outputValue : String?

    override func viewDidLoad() {
        super.viewDidLoad()
        outputLabel.text = outputValue
    }
}

遷移元のA_ViewControllerの実装

A_ViewController
import UIKit

class A_ViewController: UIViewController {

    @IBOutlet weak var inputField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // segueが動作することをViewControllerに通知するメソッド
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

        // segueのIDを確認して特定のsegueのときのみ動作させる
        if segue.identifier == "toBViewController" {
            // 2. 遷移先のViewControllerを取得
            let next = segue.destination as? B_ViewController
            // 3. 1で用意した遷移先の変数に値を渡す
            next?.outputValue = self.inputField.text
        }
    }

    @IBAction func tapTransitionButton(_ sender: Any) {
        // 4. 画面遷移実行
        performSegue(withIdentifier: "toBViewController", sender: nil)
    }

}

遷移元のA_ViewControllerではsegueのidentifierをみて、処理を行うか決めています。
prepareメソッドはsegueが動作するたびに呼ばれるので、
(今回は遷移先は一つなので必要ありませんが)遷移先が増えた時に適切な処理をするために必要です。

C. InterfaceBuilder(Storyboard ID) + コーディング

このパターンでは、segueはつながずviewControllerにIDを付与することで遷移するパターンですが、
基本パターンの1-4の手順は同一です。

まず画像のように遷移先であるB_ViewControllerにIDを付与します。

image

遷移先のB_ViewControllerの実装

(Bパターンと同一のため割愛)

遷移元のA_ViewControllerの実装

A_ViewController
import UIKit

class A_ViewController: UIViewController {

    @IBOutlet weak var inputField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func tapTransitionButton(_ sender: Any) {
        let storyboard = self.storyboard!
        // 2. 遷移先のViewControllerを取得
        let next = storyboard.instantiateViewController(withIdentifier: "B_ViewController") as! B_ViewController
        //  3. 1で用意した遷移先の変数に値を渡す
        next.outputValue = self.inputField.text
        // 4. 画面遷移実行
        self.present(next, animated: true)
    }

}

結果

遷移先に入力したテキスト「test」が反映されています。

practiceSegue

遷移先の結果を遷移元に反映させる方法

今度は遷移先の処理結果を遷移元に戻った時に反映させる処理をします。
delegateパターンやAppDelegateパターンなどがあるとのことですが、
今回は「prepareメソッドでクロージャで処理ごと渡す」パターンで実装をしてみます。

画面イメージ

①ボタンをタップして遷移先へ
②値を入力後、ボタンをタップして遷移元へ戻る
③ラベルの値が変わっているはず

image.png

実装手順

上記画面のように遷移のパターンとしてはBパターンのsegueにIDを付与し、prepareメソッド呼び出し時に値を渡す処理とします。
この時、遷移先に「値」を渡すのではなく、「遷移先の値を使って遷移元の値を更新してね」という処理ごとクロージャで渡します。

遷移元のA_ViewControllerの実装

A_ViewController
import UIKit

class A_ViewController: UIViewController {

    @IBOutlet weak var outputLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func tapTrasitionButton(_ sender: Any) {
        // segueを使って画面遷移
        performSegue(withIdentifier:  "toBViewController", sender: nil)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // segueのIDを確認して特定のsegueのときのみ動作させる
        if segue.identifier == "toBViewController" {
            // 遷移先のViewControllerを取得
            let next = segue.destination as? B_ViewController
            // 遷移先のプロパティに処理ごと渡す
            next?.resultHandler = { text in
                // 引数を使ってoutputLabelの値を更新する処理
                self.outputLabel.text = text
            }
        }
    }
}

遷移先のB_ViewControllerの実装

B_ViewController
class B_ViewController: UIViewController {

    @IBOutlet weak var inputField: UITextField!

    // 遷移元から処理を受け取るクロージャのプロパティを用意
    var resultHandler: ((String) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func tapBackButton(_ sender: Any) {
        // nilチェック
        guard let text = self.inputField.text else { return }

        // 用意したクロージャに関数がセットされているか確認する
        if let handler = self.resultHandler {
            // 入力値を引数として渡された処理の実行
            handler(text)
        }

        // 遷移元へ戻る
        self.dismiss(animated: true, completion: nil)
    }
}

通常、遷移先の画面から、遷移元の画面の要素にアクセスしたい時はdelegateパターンを使う場合が多いようですが、簡単な処理ならこちらで書くのも良いかなと思いました。

結果

元々のテキスト「Label」から遷移先で入力したテキスト「test」が反映されています。
(少し分かりづらいですが)

PracticeSegue2.gif

まとめ

  • 遷移先画面への値渡しの方法する場合の考え方は以下

    1. 遷移先に渡したい値を格納する変数を用意する
    2. 遷移先のViewControllerを取得
    3. 1で用意した遷移先の変数に値を渡す
    4. 画面遷移実行
  • 遷移先の結果を遷移元に反映させる方法

    • クロージャで処理ごと反映する処理ごと渡す
    • 今回の方法以外にDelegateパターンや、AppDelegateを経由するパターンもあり

参考

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

NativeとWeb側の接続

Native側で、JavaScript機能の呼び出し、

func getImages() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            appWebView?.evaluateJavaScript(
            "getLastImages()") { (result, error) -> Void in
                if error != nil {
                    print(error as Any)
                }
                print(result as Any)
            }
        }
    }

JavaScriptからJsonポストして、

getLastImages: function() {
      let message = {
        cmd: 'lastimages',
        count: '1',
        callbackFunc: function(responseAsJSON) {
          let response = JSON.parse(responseAsJSON)
          let image = response['image']
          if(image){
            self.form.image_white = image
          }
        }.toString()
      }
      window.webkit.messageHandlers.native.postMessage(message)
    }

Native側のHandlerは、

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    let sentData = message.body as! NSDictionary
    let command = sentData["cmd"] as! String
    var response = Dictionary<String,AnyObject>()
    if command == "lastimages"{
                appWebView?.evaluateJavaScript(
                "document.getElementById('fetchimage').style.display = 'none';") { (result, error) -> Void in
                    if error != nil {
                        print(error as Any)
                    }
                    print(result as Any)
                }
                concurrentQueue.sync {
                    var imageString = [String]()
                    imageString = imgProcessHandler.identifyFetchResult()
                    response["image"] = imageString as AnyObject
                    let callbackString = sentData["callbackFunc"] as? String
                    self.sendResponse(aResponse: response, callback: callbackString)
                }
        }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS] アプリが終了している状態でもヘルスケアデータの更新を検知できる

はじめに

iOSでは、iOSや各アプリから"ヘルスケア"にいろいろなヘルスケアデータが書き込まれます。
iOSアプリはヘルスケアデータをHealthkit経由で読み書きが可能で、またデータの更新を監視することができます。

そして更新の監視はアプリが終了している状態でも有効にすることができる、というのがこの記事の主旨です。

アプリの状態 ヘルスケアデータの更新検知
フォアグラウンドで実行されている 可能
実行されているがフォアグラウンドにない 可能
アプリが終了している 可能 ←ここ

↓のサンプルでは、アプリが終了している状態でヘルスケアへのデータ書き込みを検知して、アプリからローカル通知を送信してます。
※最初左右にスワイプしてるのはヘルスケア以外にアプリ起動してないアピールです

サンプル
sample.gif

実装方法

  1. Healthkitで任意の種類のヘルスケアデータに対して HKObserverQuery を実行する
  2. enableBackgroundDelivery(for:frequency:withCompletion:) を実行する
let objectType = HKSampleType.quantityType(forIdentifier: .bodyMass)!
let query = HKObserverQuery(sampleType: objectType, predicate: nil, updateHandler: { query, completionHandler, error in
    // 更新検知
})
HKHealthStore().execute(query)

// バックグランドでのヘルスケアデータの更新検知を有効にする
HKHealthStore().enableBackgroundDelivery(for: objectType, frequency: .immediate, withCompletion: { success, error in
})
  • 検知できる更新タイミングはenableBackgroundDelivery(for:frequency:withCompletion:)の引数として渡す HKUpdateFrequency で指定した期間に最大1回
    • 指定できる期間は immediate hourly daily weekly 、ただし特定のヘルスケアデータ(歩数など更新頻度が非常に高いものなど)は hourly 以上の頻度でしか検知できない

詳しくは公式ドキュメント参照。

サンプル

以下、体重データの更新を検知してローカル通知を送るサンプルです。

サンプルの動作環境: Xcode11.2.1 iOS13.2

プロジェクト設定

  1. ヘルスケアからデータの読み出しを許可するため、Info.plistに Privacy - Health Share Usage Description をセットする
  2. project -> target -> Signing&Capabilities で以下を設定する
    • HealthKit を有効にする
    • PushNotifications を有効にする(このサンプルでローカル通知を送るため) スクリーンショット 2019-12-05 0.23.03.png

サンプルコード

AppDelegate.swift
import UIKit
import UserNotifications
import HealthKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // プッシュ通知を有効にする
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge], completionHandler: { _, _ in
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        })

        // ヘルスケアから体重データの読み込みを許可
        let objectType = HKSampleType.quantityType(forIdentifier: .bodyMass)!

        HKHealthStore().requestAuthorization(toShare: nil, read: Set([objectType]), completion: { success, error in

        })

        // ヘルスケアデータの更新を検知するクエリ
        let query = HKObserverQuery(sampleType: objectType, predicate: nil, updateHandler: {
            query, completionHandler, error in
            if error != nil {
                return
            }
            // ヘルスケアデータの更新を検知したらローカル通知を送る
            let content = UNMutableNotificationContent()
            content.body = "体重データが更新されました"
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false)
            let request = UNNotificationRequest(identifier: "detection-test", content: content, trigger: trigger)
            UNUserNotificationCenter.current().add(request)
            completionHandler()
        })
        HKHealthStore().execute(query)

        // バックグランドでのヘルスケアデータの更新検知を有効にする
        HKHealthStore().enableBackgroundDelivery(for: objectType, frequency: .immediate, withCompletion: { success, error in
        })

        return true
    }
}

参考URL

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

MPMediaLibraryへアクセス許可を促す(不許可されても)

オークファンAdvent Calendar 12日です。

弊社に19新卒で入社して半年強が経ちました。
業務でPHPを使っており、プライベートでモバイルアプリ開発しています。
自作のアプリは審査の時でリジェクトされた問題点を書きます。

環境

Xcode 11.2
Swift 4.2
IOS 13.2

問題点

Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
IMG_0008.PNG

解決策

UIAlertControllerで選択画面を作り、設定画面まで飛ばせる。

authorizationStatus 説明
.notDetermined 未選択
.denied 不許可
.authorized 許可

音楽選択ボタンを押す時

@IBAction func pickMusic(_ sender: Any) {

        let status = MPMediaLibrary.authorizationStatus()
     //MPMediaLibraryにアクセスできない、選択画面を表示
        if status == .denied {
            self.displayPermissionViewController()
        } else {
            let picker = MPMediaPickerController(mediaTypes: MPMediaType.music)
            picker.delegate = self
            self.present(picker, animated: true, completion: nil)
        }
    }

IMG_0009.PNG

選択画面で設定しますを押す時

func displayPermissionViewController() {
        let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle:  UIAlertController.Style.alert)

        alert.popoverPresentationController?.sourceView = self.view

        let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil)
        alert.addAction(cancelAction)

        let okAction = UIAlertAction(title: "設定します", style: .default) { _ in
       // セッティング画面に行く
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
    }

IMG_0010.PNG

全体コード

import UIKit
import MediaPlayer
class ViewController: UIViewController, MPMediaPickerControllerDelegate{

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func pickMusic(_ sender: Any) {

        let status = MPMediaLibrary.authorizationStatus()
        //MPMediaLibraryにアクセスできない、選択画面を表示
        if status == .denied {
            self.displayPermissionViewController()
        } else {
            let picker = MPMediaPickerController(mediaTypes: MPMediaType.music)
            picker.delegate = self
            self.present(picker, animated: true, completion: nil)
        }
    }

    func displayPermissionViewController() {
        let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle:  UIAlertController.Style.alert)

        alert.popoverPresentationController?.sourceView = self.view

        let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil)
        alert.addAction(cancelAction)

        let okAction = UIAlertAction(title: "設定します", style: .default) { _ in
            // セッティング画面に行く
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
    }

    func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
        dismiss(animated: true, completion: nil)
    }

    func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        let musicPlayer = MPMusicPlayerController.applicationMusicPlayer
        musicPlayer.setQueue(with: mediaItemCollection)
        musicPlayer.play()
        dismiss(animated: true, completion: nil)
    }
}


作った目覚ましアプリはこちらです

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

MPMediaLibraryへアクセス許可を促す(不許可されても)

環境

Xcode 11.2
Swift 4.2
IOS 13.2

問題点

Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
自作のアプリは審査の時でこの問題でリジェクトされたことがあります。
IMG_0008.PNG

解決策

UIAlertControllerで選択画面を作り、設定画面まで飛ばせる。

authorizationStatus 説明
.notDetermined 未選択
.denied 不許可
.authorized 許可

音楽選択ボタンを押す時

@IBAction func pickMusic(_ sender: Any) {

        let status = MPMediaLibrary.authorizationStatus()
     //MPMediaLibraryにアクセスできない、選択画面を表示
        if status == .denied {
            self.displayPermissionViewController()
        } else {
            let picker = MPMediaPickerController(mediaTypes: MPMediaType.music)
            picker.delegate = self
            self.present(picker, animated: true, completion: nil)
        }
    }

IMG_0009.PNG

選択画面で設定しますを押す時

func displayPermissionViewController() {
        let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle:  UIAlertController.Style.alert)

        alert.popoverPresentationController?.sourceView = self.view

        let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil)
        alert.addAction(cancelAction)

        let okAction = UIAlertAction(title: "設定します", style: .default) { _ in
       // セッティング画面に行く
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
    }

IMG_0010.PNG

全体コード

import UIKit
import MediaPlayer
class ViewController: UIViewController, MPMediaPickerControllerDelegate{

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func pickMusic(_ sender: Any) {

        let status = MPMediaLibrary.authorizationStatus()
        //MPMediaLibraryにアクセスできない、選択画面を表示
        if status == .denied {
            self.displayPermissionViewController()
        } else {
            let picker = MPMediaPickerController(mediaTypes: MPMediaType.music)
            picker.delegate = self
            self.present(picker, animated: true, completion: nil)
        }
    }

    func displayPermissionViewController() {
        let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle:  UIAlertController.Style.alert)

        alert.popoverPresentationController?.sourceView = self.view

        let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil)
        alert.addAction(cancelAction)

        let okAction = UIAlertAction(title: "設定します", style: .default) { _ in
            // セッティング画面に行く
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
    }

    func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
        dismiss(animated: true, completion: nil)
    }

    func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        let musicPlayer = MPMusicPlayerController.applicationMusicPlayer
        musicPlayer.setQueue(with: mediaItemCollection)
        musicPlayer.play()
        dismiss(animated: true, completion: nil)
    }
}


作った目覚ましアプリはこちらです

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

Core NFC ってなんだ【iPhone で ピッと NFC】

Advent Calendar 2019 始まりました。
この Advent Calendar 自体をやってみるのも初めてなんですけど…。
見様見真似でチャレンジします。

NFC (Near Field Communication)

ここでいう NFC とは Near Field Communication の略です。
何かの機器に対してカードを近づけてピッとさせるアレ、あれが NFC です。

NFC Forum の文書にはこのように書かれています。

魔法の杖をひと振りしたら何でも望みがか なうといいのに――昔から人はそんな夢を 見てきました。その長年の夢が、NFC(Near Field Communication)テクノロジーの登場 で現実のものになりました。

日本で NFC が用いられている場面は、Suica、PASMO、ICOCA 等に代表される交通系ICカードから、楽天Edy、nanaco、WAON などの電子マネーカード、自動車の運転免許証、個人番号(マイナンバー)カード、パスポートにも含まれていますね。
おサイフケータイ、最近はコンタクトレス決済(NFC Pay)もじわじわとやってきています。
会社や学校での入退出管理でカードをピッとやっている場所はだいたい NFC です(カードをシュッとスライドさせるタイプは異なります)。

Core NFC

そして、この Advent Calendar のテーマである「Core NFC」とは、iOS 端末で NFC を利用するアプリケーションを開発する際に必ず用いることとなる Framework です。
Android であれば android.nfc.tech Package に対応するかなと思います。

Apple Developer Documentation にはこう書かれています。

Detect NFC tags, read messages that contain NDEF data, and save data to writable tags.

この Framework は iOS 11.0 のときに登場しましたが、そのときは NFC Data Exchange Format (NDEF) のタグの読み取りができるだけのものであり、利用できる場面が非常に限定されていました。

しかし、iOS 13.0 にて NDEF に依らない情報の読み取り、そして情報の書き込みが可能となり、いよいよ本格的に NFC 技術を iPhone で活用するときがきた!という状況になっています。

Android では昔からできた

Core NFC で何ができるか

日本の場合、一番大きな活用場面は「キャッシュレス決済」もしくは「電子マネーカードとの情報のやりとり」かなと思います。
iOS 13.0 が登場した2019年9月、多くのメディアで取り上げられた「iPhone で電子マネーカードの残高をチェックできるようになった」という情報、あれは Core NFC を用いて開発されたアプリ群を紹介するものでした。

個人的 3大 iPhone の電子マネーカードリーダー
- Japan NFC Reader (本記事執筆者が開発)
- ICリーダー
- CardPort

これらのアプリは Core NFC を用いて、電子マネーカードと NFC 通信を行うことでカード内に記録されている残高、利用履歴、ポイント等を取得し、端末に表示するということを行っています。

また、技術的には電子マネーカードのチャージも iPhone で行うことができたり、スマートロックの解除などスマートホームでの活用も考えられます。

まとめ

  • NFC とは機器に対してカードを近づけてピッとさせるアレ
  • 日本では Suica 等の交通系ICカードや電子マネーカード、運転免許証やパスポートにも含まれている
  • Core NFC は NFC を iPhone で使うために必要な Framework
  • iOS 13.0 で Core NFC の機能が大幅に追加され、実用的になった

Core NFC Advent Calendar 2019、隔日で記事の執筆を予定しているので、はじめはなかなか内容が薄い記事が続くかな…と思います。どうかお付き合いください…。

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