xyk blog

最近は iOS 開発の記事が多めです。

UserDefaults に保存されているデータをすべて表示する

環境:Swift3

UserDefaults に保存されているデータをすべて表示する

for (key, value) in UserDefaults.standard.dictionaryRepresentation().sorted(by: { $0.0 < $1.0 }) {
    print("- \(key) => \(value)")
}

または

if let appDomain = Bundle.main.bundleIdentifier,
    let dic = UserDefaults.standard.persistentDomain(forName: appDomain) {
            
    for (key, value) in dic.sorted(by: { $0.0 < $1.0 }) {
        print("- \(key) => \(value)")
    }
}

キーの辞書順にソートしてから表示している。

Firebaseでプッシュ通知したが既読数が計測されていなかった件

環境: iOS9

Firebaseのプッシュ通知機能を使い、iOS端末に向けてのプッシュ通知をしたところ、送信は問題なくできたのだが、管理画面から確認できる既読数が0にままだったので原因を調べた。

f:id:xyk:20161104132640p:plain

で原因だが以下ドキュメント

Receive Messages in an iOS App  |  Firebase
https://firebase.google.com/docs/cloud-messaging/ios/receive

Handling messages with method swizzling disabledの項にちゃんと書いてあるのだが Method swizzling をオフにした場合にはFIRMessaging.messaging().appDidReceiveMessage(userInfo)を実装する必要があるが、これを実装していなかった。

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // Let FCM know about the message for analytics etc.
    FIRMessaging.messaging().appDidReceiveMessage(userInfo)
    // handle your message
  }

これを実装したところ、既読数も計測されるようになった。

ちなみにプッシュ通知を受信した時に呼ばれるメソッドは以下記事書いたように2種類あるのだが

xyk.hatenablog.com

fetchCompletionHandler 付きのメソッドの方はアプリの状態によらず必ず呼ばれるのでこちらでappDidReceiveMessage(userInfo)を実装すること。

あと、管理画面上に計測結果が反映されるには、少し時間がかかるので注意。
まず送信数の方が数十分〜数時間後に反映され、それからさらに時間をおいて既読数の方が反映される。

また、iOS10ではプッシュ通知周りの実装が少し変わったので以下を参考に今後修正予定。
(以前の実装のままでもiOS10で動作はする)

quickstart-ios/AppDelegate.swift at master · firebase/quickstart-ios
https://github.com/firebase/quickstart-ios/blob/master/messaging/FCMSwift/AppDelegate.swift

プッシュ通知受信時に呼ばれるメソッドについて

環境: Swift2.3

前提となるCapabilitiesの設定

- Push Notifications -> ON  
- Background Modes -> OFF  

プッシュ通知受信時に呼ばれるメソッド

プッシュ通知受信時に呼び出されるメソッドに
1. application:didReceiveRemoteNotification:
2. application:didReceiveRemoteNotification:fetchCompletionHandler:
という似たメソッドが存在するがこの違いについてメモしておく。

// 1
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
    // userInfo の処理

}

// 2
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject],
                 fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // userInfo の処理

}

基本的には、この2つのメソッドのどちらか片方のみ実装することになる。
両方とも実装していた場合は
2. application:didReceiveRemoteNotification:fetchCompletionHandler:
の方が優先されて呼び出され、
1. application:didReceiveRemoteNotification:
の方は呼び出されないので注意。


1. application:didReceiveRemoteNotification:を実装してプッシュ通知を受信

アプリ未起動時

画面上部に通知表示。
通知をタップすると
application:didFinishLaunchingWithOptions:のみ呼び出される。
application:didReceiveRemoteNotification:は呼ばれない。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    if let userInfo = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] {
        // userInfo の処理
    }

}
フォアグラウンド時

通知表示はない。
application:didReceiveRemoteNotification:のみ呼び出される

バックグラウンド時

画面上部に通知表示。
通知をタップするとapplication:didReceiveRemoteNotification:のみ呼び出される


2. application:didReceiveRemoteNotification:fetchCompletionHandler: を実装してプッシュ通知を受信

アプリ未起動時

画面上部に通知表示。
通知をタップすると
application:didFinishLaunchingWithOptions:が呼び出される。
その後
application:didReceiveRemoteNotification:fetchCompletionHandler:が呼び出される。
ここが1とは違い必ず呼び出される。

フォアグラウンド時

通知表示はない。
application:didReceiveRemoteNotification:fetchCompletionHandler:のみ呼び出される

バックグラウンド時

画面上部に通知表示。
通知をタップするとapplication:didReceiveRemoteNotification:fetchCompletionHandler:のみ呼び出される

Swiftのバージョンを調べる

ターミナルで以下コマンドswift -vを実行する。

$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -v
Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1)
Target: x86_64-apple-macosx10.9
/Applications/Xcode.app/Contents/Developer/usr/bin/lldb "--repl=-target x86_64-apple-macosx10.9 -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -color-diagnostics"
Welcome to Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1). Type :help for assistance.
  1> :q

REPLが起動するので:qで終了する。

iPhoneのHTTPプロキシを設定してもオフになってしまう件

環境:iOS10.1

新しいiPhoneを購入後、WifiのHTTPプロキシ設定を「手動」で行うとしたところ、入力しても保存されず、なぜかオフになってしまう現象が起きた。
どうやら前のiPhoneのバックアップから復元した場合に発生するようだ。
「このネットワーク設定を削除」から設定を一度削除し、パスワードを入力して接続し直したところ、プロキシ設定ができるようになった。

iPhoneのUDIDを調べる方法

Xcode(8.0) で調べる

Window -> Devices and Simulators からiPhoneバイスを選択、identifierの部分が UDIDになる。

f:id:xyk:20161019103032p:plain

iTunes で調べる

初期表示はシリアル番号となっているがその辺りをクリックすると表示項目が切り替わり、UDIDが現れる。
シリアル番号 -> UDID -> ECID -> 機種ID の順に切り替わる。
右クリックからコピーできる。
f:id:xyk:20161019103034p:plain

instruments コマンドで調べる

ターミナルから以下コマンドを実行。Xcodeをインストールしていれば使える

$ instruments -s devices

f:id:xyk:20161019105019p:plain

追記:

instruments コマンドがいつの間にか deprecated になっていた。

`instruments` is now deprecated in favor of 'xcrun xctrace' (see `man xctrace` for more information on its replacement)

代わりに以下コマンドを使う。

$ xcrun xctrace list devices

いつも設定するAppearanceのメモ

環境:
Swift2.2
iOS8以降対象

いつも設定している Appearance のコピペ用メモ。
以下を AppDelegate で呼ぶ。
mainColor は extension で独自に設定したもの。

private func setupAppearance() {

    // アプリケーション全体のtintColor設定
    self.window?.tintColor = UIColor.mainColor

    // ステータスバーの文字色を白に。
    // プラス`Info.plist`に`View controller-based status bar appearance = NO`を追加
    UIApplication.sharedApplication().statusBarStyle = .LightContent

    // ナビゲーションバーの色
    UINavigationBar.appearance().barTintColor = UIColor.mainColor
    // ナビゲーションバーボタンの色を白に。
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()
    // ナビゲーションバーのタイトル文字色を白に。
    UINavigationBar.appearance().titleTextAttributes = [
        NSForegroundColorAttributeName: UIColor.whiteColor()
    ]
}

こんな感じに。

f:id:xyk:20160813124200p:plain

SwiftでTupleとCaseを組み合わせて使う

環境: Swift2.2

タプルとcaseを組み合わせて使うと便利だったのでメモ。

Switch(case)文で使う

オプショナル型な複数の値の組み合わせで場合分けしたい場合に使う。
また値はアンラップして取り出して変数にバインドする。
ポイントはcase部分で「?」をつけること。これでオプショナル型でnilでない場合にマッチする。

let num: Int? = ...
let str: String? = ...

switch(num, str) {

case let (num?, str?): // num, strともオプショナル型でnil以外にマッチ
// case let (.Some(num), .Some(str)): // これでもよい
    print("num: \(num), str: \(str)")

case (let num?, nil): // numはオプショナル型でnil以外、strはnilにマッチ
// case (.Some(let num), nil): // これでもよい
    print("num: \(num)")

case case (nil, _?): // numはnil、strはオプショナル型でnil以外にマッチ
// case (nil, .Some(_)): // これでもよい
    break

default: // num、strともにnilにマッチ
    break
}

if letの代わりにif case letに使う

if case letでパターンマッチが利用できる。
オプショナルをアンラップして変数にバインド。
これもポイントはcase部分で「?」をつけること。

// a は Optional
if case let x? = a {
}

// 以下と同じ
if case .Some(let x) = a {
}

そしてタプルを組み合わせて使うと、複数の値を一気にバインドできる。
if letよりスッキリ書ける気がする。

if let

// a, b, c は Optional
if let x = a,
  let y = b,
  let z = c {

}

if case let

if case let (x?, y?, z?) = (a, b, c) {

}

参考:

Swift2のパターンマッチ構文集(ほぼ翻訳) - Qiita
http://qiita.com/mono0926/items/f2875a9eacef53e88122

MySQLに街区レベル位置参照情報のCSVデータをインポートする

環境
Mac
MySQL Server version: 5.7.13

位置参照情報ダウンロードサービス
http://nlftp.mlit.go.jp/isj/

今回はこちらから東京都の大字・町丁目レベルのデータをダウンロードする。
13000-09.0b.zipというファイルがダウンロードされる。
これを解凍すると13_2015.csvというCSVファイルがあるのでこのデータをMySQLにインポートする。

まず先に文字コードShift_JISからUTF-8に変換しておく。
nkfコマンドを使おうと思ったがMacには入っていなかったのでhomebrewでインストールした。

$ brew install nkf

UTF-8に変換

$ nkf -w 13_2015.csv > 13_2015_utf8.csv

中身を確認してみる。

$ head 13_2015_utf8.csv
"都道府県コード","都道府県名","市区町村コード","市区町村名","大字町丁目コード","大字町丁目名","緯度","経度","原典資料コード","大字・字・丁目区分コード"
"13","東京都","13101","千代田区","131010001001","一ツ橋一丁目","35.691634","139.756685","1","3"
"13","東京都","13101","千代田区","131010001002","一ツ橋二丁目","35.692947","139.757320","1","3"
"13","東京都","13101","千代田区","131010002000","一番町","35.687723","139.739668","1","1"
"13","東京都","13101","千代田区","131010003001","永田町一丁目","35.676328","139.745749","1","3"
"13","東京都","13101","千代田区","131010003002","永田町二丁目","35.675705","139.740497","1","3"
"13","東京都","13101","千代田区","131010004001","猿楽町一丁目","35.698471","139.759949","1","3"
"13","東京都","13101","千代田区","131010004002","猿楽町二丁目","35.700021","139.758377","1","3"
"13","東京都","13101","千代田区","131010005001","霞が関一丁目","35.674720","139.753419","1","3"
"13","東京都","13101","千代田区","131010005002","霞が関二丁目","35.675706","139.750734","1","3"

10列の項目があるが今回は使いたいのは名称と緯度経度のみなのでそれだけ入れるテーブルを用意する。
名称はすべて連結しnameカラムへ、緯度経度はPointにしてgeometry型のlatlonカラムに突っ込む。

-- MySQLにログイン
$ mysql -uroot

-- 適当なDBを作成
mysql> create database geo;
mysql> use geo;

-- テーブル作成
mysql> CREATE TABLE `spots` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `latlon` geometry NOT NULL,
  PRIMARY KEY (`id`),
  SPATIAL KEY `index_spots_on_latlon` (`latlon`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

※注 MySQL5.7.5以降でInnoDBでもSPATIALインデックスが使えるようになった

そして、LOAD DATA INFILEコマンドでCSVデータをインポートする。

LOAD DATA INFILE '/path/to/13_2015_utf8.csv'
REPLACE INTO TABLE
geo.spots
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\r\n' STARTING BY ''
IGNORE 1 LINES
(@dummy, @prefecture, @dummy, @city, @dummy, @area, @lat, @lon, @dummy, @dummy)
SET
name = CONCAT(@prefecture, @city, @area), 
latlon = ST_GeomFromText(CONCAT('POINT(', @lon, ' ', @lat, ')'));

Query OK, 5666 rows affected (0.20 sec)
Records: 5666  Deleted: 0  Skipped: 0  Warnings: 0

データを確認してみる。

mysql> SELECT id, name, ST_AsText(latlon) FROM spots limit 10;

+----+-----------------------------------------+-----------------------------+
| id | name                                    | ST_AsText(latlon)           |
+----+-----------------------------------------+-----------------------------+
|  1 | 東京都千代田区一ツ橋一丁目              | POINT(139.756685 35.691634) |
|  2 | 東京都千代田区一ツ橋二丁目              | POINT(139.75732 35.692947)  |
|  3 | 東京都千代田区一番町                    | POINT(139.739668 35.687723) |
|  4 | 東京都千代田区永田町一丁目              | POINT(139.745749 35.676328) |
|  5 | 東京都千代田区永田町二丁目              | POINT(139.740497 35.675705) |
|  6 | 東京都千代田区猿楽町一丁目              | POINT(139.759949 35.698471) |
|  7 | 東京都千代田区猿楽町二丁目              | POINT(139.758377 35.700021) |
|  8 | 東京都千代田区霞が関一丁目              | POINT(139.753419 35.67472)  |
|  9 | 東京都千代田区霞が関二丁目              | POINT(139.750734 35.675706) |
| 10 | 東京都千代田区霞が関三丁目              | POINT(139.748265 35.671608) |
+----+-----------------------------------------+-----------------------------+
10 rows in set (0.00 sec)

※注 STプレフィックスが付く空間用関数はMySQL5.6以降、追加されていっているもの。

CSVデータの中身は何もいじらずにサクッとできた。LOAD DATA INFILE 便利!

参考:
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.6 LOAD DATA INFILE 構文
https://dev.mysql.com/doc/refman/5.6/ja/load-data.html

LaunchScreen.storyboard に貼り付けた画像が表示されない

シミュレータでは表示されるが、実機だと表示されない。
バグらしい、とりあえず自分の場合は iPhone 側の再起動で表示されるようになった。

Launch Storyboard not showing image when projec... | Apple Developer Forums
https://forums.developer.apple.com/message/131986

Launchscreen storyboard doesn't display image | Apple Developer Forums
https://forums.developer.apple.com/thread/17146