2017年11月8日水曜日

変なメッセージ: You are currently using version x.xx.x of the SDK.

AdMobを使ってると、時々以下のメッセージがコンソールに出る。
これはSDKが最新版じゃないからアップデートしてちょということなので、そうすればいい。
ただ、書いてあるアドレスはリリースノートなので、実際のダウンロードは
https://developers.google.com/admob/ios/download
から。
まあ書いてあるアドレスのページからも行けるけど。

<Google:HTML> You are currently using version 7.24.1 of the SDK. Please consider updating your SDK to the most recent SDK version to get the latest features and bug fixes. The latest SDK can be downloaded from https://goo.gl/UoiJ8F. A full list of release notes is available at https://developers.google.com/admob/ios/rel-notes.

変なメッセージ:[BoringSSL] Function boringssl_context_get_peer_sct_list: line 1754 received sct extension length is less than sct data length

アプリ作ってたら、エラーもWarningもなく正常に動くのに、以下のようなメッセージが出てる。

[BoringSSL] Function boringssl_context_get_peer_sct_list: line 1754 received sct extension length is less than sct data length

【直訳】
[BoringSSL] 関数boringssl_context_get_peer_sct_list:1754行が受信したsctエクステンションの長さがsctのデータ長より短い

わかんねーよ!
BoringSSLは、オープンソースで開発されているセキュリティ通信プロトコル(SSL)である、OpenSSLのセキュリティや不具合を改善するためにGoogleが出した派生版で、その関係のメッセージ。
Googleの広告システムAdMobを入れとくと出るらしく、特に問題が起きてなければ無視しちゃっていいようだ。

2017年11月5日日曜日

画像のメタデータを解析

画像のメタデータ、つまりEXIFの撮影データや、GPSの位置情報などを取り出す。

実際のメタデータはCIImageのpropertiesプロパティで取れる。
辞書の中の一部に配列が入れ子になってる。
let ciImage = CIImage(contentsOf: fileURL)
let metadata:[String:Any] = ciImage!.properties

辞書の中身は以下のよう。
{ }が辞書、( )が配列。
ISOSpeedRatingsのように要素1個の配列に入ってる場合もあるので注意。
辞書のKeyはColorModelとかの文字列でもいいが、kCGImagePropertyColorModelなどの定数があるのでそれを使うといい。ただし、CFStringなのでStringにキャストして使う。
“ ”で囲まれた数値は最初にNSNumberにキャストすると取り出せる場合が多い。
値がなくてnilの場合もあるので、Optionalとして、nilチェックもする。

辞書のトップレベル

辞書のトップレベルならそのままkeyで取れる。
let colorModel = metadata[kCGImagePropertyColorModel as String] as! String

辞書の2ndレベル

2ndレベル以降だと、一度入れ子になった辞書や配列を読み出さないといけない。
以下ではExifの中のExposureTime(シャッター速度)を取り出す。
let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? [String:Any]
if let m = exifDict?[kCGImagePropertyExifExposureTime as String] as? NSNumber {
    let exposureTime = m.floatValue
}

つづいてISOSpeedRatings(ISO感度)を取り出す。
辞書の中の要素一個の配列に入ってる。
if let ISORateArr = exifDict?[kCGImagePropertyExifISOSpeedRatings as String] as? [Any] {
    if let rate = ISORateArr[0] as? NSNumber {
        let ISORate = exifDict[kCGImagePropertyExifISOSpeedRatings as String] = rate.stringValue
    }
}

メタデータの中身の例

{
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 1;
    PixelHeight = 2448;
    PixelWidth = 3264;
    ProfileName = "sRGB IEC61966-2.1";
    "{Exif}" =     {
        ApertureValue = "2.52606882168926";
        BrightnessValue = "2.661020629750272";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            1,
            2,
            3,
            0
        );
        CustomRendered = 2;
        DateTimeDigitized = "2016:03:09 15:28:25";
        DateTimeOriginal = "2016:03:09 15:28:25";
        ExifVersion =         (
            2,
            2,
            1
        );
        ExposureBiasValue = 0;
        ExposureMode = 0;
        ExposureProgram = 2;
        ExposureTime = "0.0303030303030303";
        FNumber = "2.4";
        Flash = 32;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 31;
        FocalLength = "3.3";
        ISOSpeedRatings =         (
            160
        );
        LensMake = Apple;
        LensModel = "iPad Air 2 back camera 3.3mm f/2.4";
        LensSpecification =         (
            "3.3",
            "3.3",
            "2.4",
            "2.4"
        );
        MeteringMode = 5;
        PixelXDimension = 3264;
        PixelYDimension = 2448;
        SceneCaptureType = 0;
        SceneType = 1;
        SensingMethod = 2;
        ShutterSpeedValue = "5.058989898989899";
        SubjectArea =         (
            1631,
            1223,
            1795,
            1077
        );
        SubsecTimeDigitized = 996;
        SubsecTimeOriginal = 996;
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = "143.7687861271676";
        AltitudeRef = 0;
        DateStamp = "2016:03:09";
        DestBearing = "127.0993788819876";
        DestBearingRef = T;
        HPositioningError = 5;
        ImgDirection = "12x.xxx3788819876";
        ImgDirectionRef = T;
        Latitude = "36.588055";
        LatitudeRef = N;
        Longitude = "139.xxx3666666667";
        LongitudeRef = E;
        Speed = 0;
        SpeedRef = K;
        TimeStamp = "06:28:25";
    };
    "{MakerApple}" =     {
        1 = 4;
        10 = 2;
        14 = 1;
        2 = <37007700 73003500 34004c00 5c006c00 67007c00 75008500 a300b100 b000e400 6e006500 68003600 38005f00 62006c00 66007700 70007b00 8200b600 e200cd00 5800b900 9d009400 5f004300 52005e00 5c008100 8300b100 d000cf00 de000701 56006800 a8009700 7e005300 4c006600 7c008e00 9300ea00 1601f100 e900e300 53006100 83007c00 7e00c300 ca00b800 86008c00 9200f200 ff000101 d500b100 5e006200 6b00be00 4a016d01 7d018d01 5001cd00 a200c900 f800df00 e000af00 71009600 fd004d01 4b014401 4f014f01 6a01a201 44013a01 16012501 37019000 6f015801 0f01fc00 f800f000 f7002101 3f016a01 9001bb01 9b018101 4401f700 d500a200 e700d700 dc00e500 f500e800 18013b01 60019001 6a013001 12010b01 5f008000 9d00b200 9d009b00 b900c800 e7002a01 87017201 3401fa00 c800cb00 5c008000 91009400 85009900 9600af00 be005601 59012b01 e300d800 18012f01 6e008a00 7c007500 72008200 9100a500 e7006701 3a01c400 e9007601 8c018e01 41007100 63005c00 5f006c00 83009100 3a014f01 dc00fb00 9e019d01 aa01ad01 59005300 4f005500 52005d00 72000c01 53010b01 1701b001 a701a201 9b019c01 44005100 51005000 5200a400 0a014701 23010c01 af01b801 9f019c01 9701a601 36003800 48006700 d000f300 08014301 19018701 ad01b101 9c019901 93018a01>;
        20 = 3;
        3 =         {
            epoch = 0;
            flags = 1;
            timescale = 1000000000;
            value = 817736307596166;
        };
        4 = 1;
        5 = 214;
        6 = 220;
        7 = 1;
        8 =         (
            "-0.9269282",
            "0.0151294",
            "-0.3894437"
        );
    };
    "{TIFF}" =     {
        DateTime = "2016:03:09 15:28:25";
        Make = Apple;
        Model = "iPad Air 2";
        Orientation = 1;
        ResolutionUnit = 2;
        Software = "9.2.1";
        XResolution = 72;
        YResolution = 72;
    };

}

2017年11月3日金曜日

ファイルPathとファイルURL

ファイルを保存したり、ファイルの有無を確認したりするのに、ファイルPathを用いる方法と、ファイルURLを用いる方法があり、結構混乱するのでまとめ。

ファイルPathとファイルURLの違い

ファイルPath

/var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents/ファイル名

ファイルURL

file:///var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents/ファイル名

以上のように、頭に「file://」が付いてる方がURL。
WebのURLの頭に「http://」が付いてるのと一緒だな。

Documentディレクトリを得る

Path(String型)で得る

let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)last!
【結果】
/var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents

URL型で得る

let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!

【結果】
file:///var/mobile/Containers/Data/Application/97C2495D-1A6B-4E5D-BCAE-39C64563328A/Documents

Documentsディレクトリにファイル名を追加

URLの場合はそのまま追加できる

let fileURL = documentsURL.appendingPathComponent(fileName)

Pathの場合は一度URLに変換しないといけない

let fileURL = URL(fileURLWithPath: documentsPath).appendingPathComponent(fileName)

SwiftにはPathにファイル名などを追加するメソッドがない。(たしかObjective-Cにはあった)
そのため一度URL化する必要がある。めんどくせえ。

URLからPathに変換

let filePath = fileURL.path

URLのpathプロパティで得られる。

PathからURLに変換

let url = URL(fileURLWithPath: filePath)


引数のfilePathがPathと判断されない場合(例えばファイル名の文字列とか、空白文字とか)、
123456.jpg -- file:///
という、末尾に -- file:///が付いた変な形式に変換されてエラーの原因になるので注意が必要。
その場合は
let urlStr = URL(string: "123456.jpg")
とやるといい。

2017年11月1日水曜日

enumをエンコード/デコードしようとするとエラーになる

地図のタイプ(MKMapType)をファイルに保存すべく、

func encode(with aCoder: NSCoder) {
    aCoder.encode(mapType, forKey: "mapType")
}

などと書いたのだが、以下のようなエラーが出て落ちてしまった。

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x1c4242e80'

有効じゃないselectorがインスタンスに送られた…ってことらしいが、さっぱりわからん。
調べたところ、要はMKMapTypeがenum(列挙型)のため、そのままじゃencode、decodeできんのだと。
中身(RawValue)はUIntなので、
aCoder.encode(mapType.rawValue, forKey: "mapType")
でうまくいった。

decode側も、以下のように対処。
mapType = MKMapType(rawValue: UInt(aDecoder.decodeInteger(forKey: "mapType")))!
decodeInteger(forKey:)でIntとしてデコードして、それをUIntにキャストしてる。

mapType = MKMapType(rawValue: aDecoder.decodeInteger(forKey: "mapType")) as! UInt)!
としたかったけど、このやり方はできなかったので、UInt()で囲んだ。

グチ

カスタムクラスでも構造体でも列挙型でも、そのまんまファイルやUserdefaultsに読み書きできるように作らないのかねえ? めんどうだよね。

Xcode 9.1


2017/10/31にリリースされたバージョンの新機能。
  • OpenGL ESとマップのパフォーマンスに影響を与えたSimulatorの問題を修正しました。
  • iPhone Xのサポートが強化されました。
  • 追加のバグ修正と安定性の向上。

そういえば「‪iPhone X‬に対応させよう!」「‪iPhone X‬のスクリーンキャプチャを付けよう!」みたいなお知らせメールがアップルデベロッパーから来てたな。
きちんと対応させないと審査通らなくなるんだろうか?
スクリーンキャプチャも今までは一番高解像度の機種用のスクリーンを用意すれば、他の解像度のはそれから作ってくれたけど、‪iPhone X‬のは例の部分が特殊だから、別に用意しなけりゃダメとかになった?(´・ω・`)

2017年10月31日火曜日

Selectorの書き方

Timerとかで処理先のメソッドを指定するSelectorだけど、書式が変わったし、引数を渡したい場合などがよくわかってないのでまとめ。

基本書式

override func viewDidLoad() {
    super.viewDidLoad()

    let t = Timer.init(timeInterval: 0.5,
                       target: self,
                       selector: #selector(ViewController.hoge(_:)),
                       userInfo: userInfo,
                       repeats: false)
    t.fire()

}

@objc func hoge(_ sender:Timer) -> Void {
    print("タイマー実行")
}

上のコードは0.5秒後にhoge()を一度だけ実行する。
selectorで指定するメソッドは、Swift4から頭に@objcを付けてやらなければいけない。
これはどうやらObjective-C由来の仕様だからで、今までは推定して実行してくれてたけど、もうやってくれないんだそうな。ケチだな。

selfがViewControllerの場合、Selectorは以下のとおり。

hoge()に引数がある場合

⭕️ #selector(ViewController.hoge(_:))
⭕️ #selector(ViewController.hoge)
⭕️ #selector(hoge(_:))
⭕️ #selector(hoge)

普通に後ろに()を付けたり、以前の書き方のように" "で囲むのはダメ。
❌ #selector(ViewController.hoge())
❌ #selector("ViewController.hoge(_:)")
❌ #selector("ViewController.hoge")

hoge()に引数がない場合

⭕️ #selector(ViewController.hoge)
⭕️ #selector(hoge)

 #selector(hoge(_:))
 #selector(hoge())

メソッドの引数は使えない?

引数を渡したくて、普通に書いてやっても、エラーになってしまう。
❌ selector: #selector(hoge(str: "文字"))

@objc func hoge(str:String) -> Void {
    print("タイマー実行 \(str)")
}

それでも引数を渡すのだ!

引数の渡し方は以下のようにuserInfo経由のものとなる。
複数の引数を渡したいなら、配列や辞書に入れて渡すのかな?
hogeの方ではsender.userInfoで取り出せる。

let userInfo = "引数がわり"
let t = Timer.init(timeInterval: 0.5,
                   target: self,
                   selector: #selector(hoge(_:)),
                   userInfo: userInfo,
                   repeats: false)


@objc func hoge(_ sender:Timer) -> Void {
    let str = sender.userInfo
    print("タイマー実行 \(String(describing: str))")
}

senderのかわりに別の名前でもいい。型名は送り手側のクラス名になるのだな。
@objc func hoge(_ s:Timer) -> Void {
    let str = s.userInfo
    print("タイマー実行 \(String(describing: str))")
}

めんどくさいね

書き方がちょくちょく変わるし、引数の渡し方も特殊だし、総じて面倒臭い。
もっと簡単な書き方にしやがれと思うのだが。