2017年12月28日木曜日

後からNavigationを追加する場合は

StoryboardでNavigationを使う場合、最初からならUINavigationControllerをドロップしてやればいいけど、ViewControllerに後から追加する場合、追加するのはUINavigationBarじゃなく、UINavigationItemの方だ。

2017年12月22日金曜日

MapViewの縮尺が思いどおりにならない

やりたいこと

地図をタップもしくはピンチして拡大/縮小した縮尺を覚えさせ、次回表示時に同じ縮尺で表示したい。
複数の元データがあり、地図を開くごとにそれぞれ個別の縮尺で表示したい。

やったこと

mapViewの.region.span.latitudeDelta、.region.span.longitudeDeltaの値が地図の縮尺の値(正確には表示範囲の緯度、経度の角度)なので、地図を閉じる際にそれを保存し、開く際に同じ値を設定してやる。

うまくいかないこと

表示されたものがだいぶズームアウトしたものになってしまう。

たとえば地図に以下の値を設定したのに、
latitudeDelta: 0.047710965458634291, longitudeDelta: 0.10232551591900574
地図表示後は以下の値になってしまう🤔
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

再度地図を開閉するとその値を保存するので、開閉するたびにどんどんズームアウトしてしまう。
最初これはiPad、iPhoneシミュレータ(iPadの擬似iPhone表示含む)で起こった。
そのうち、コードやらConstraintsやらをいじっていたところ、iPadで現象が起こらなくなった。そのかわりiPhoneで今度はズームインするようになった。

原因を推測

アップルのドキュメントによると、設定した縮尺はそのまま利用されるのでなく、指定された領域全体が画面に収まるように、勝手に少しずつズームアウトするらしい。
regionプロパティに割り当てる値(またはsetRegion:animated:メソッドで設定する値)は通常、このプロパティによって最終的に保存される値と同じではありません。領域のスパンを設定すると、表示したい矩形が名目的に定義されるだけでなく、Map View自体の拡大縮小レベルも暗黙的に設定されます。Map Viewは、任意の拡大縮小レベルを表示することができないため、指定された領域を、Map Viewがサポートする拡大縮小レベルに合うように調節しなければなりません。Map Viewは、できる限り画面いっぱいに表示しながら、指定した可視全体を表示できる拡大縮小レベルを選択します。その後、それに応じてregionプロパティを調節します。regionプロパティの値を実際には変更せずに結果の領域を確認するには、Map ViewのregionThatFits:メソッドを使用できます。
毎回値が固定されてればいいけど、ズームアウトされた値を保存して次回使うようにすると、使うたびにどんどんズームアウトされちゃうんだな。

どのタイミングで値が変わる?

どのタイミングで値が変わるのかを調べるため、実行時に順に呼ばれるメソッドを監視したところ、以下の順で呼ばれていることがわかった。
ちなみにviewWillAppearの中から地図の設定のメソッドを呼び出して緯度経度および縮尺を設定している。
  1. viewDidLoad
  2. viewWillAppear←この中で縮尺等設定
  3. mapViewWillStartRenderingMap
  4. mapViewWillStartLoadingMap
  5. mapViewDidFinishLoadingMap
  6. viewDidAppear
  7. mapViewDidFinishRenderingMap
  8. mapViewWillStartLoadingMap
  9. mapViewDidFinishLoadingMap

それぞれの時点での縮尺の値の変化


viewDidLoad 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

viewWillAppearから呼び出すメソッド内でmapViewに値を設定。
設定直前 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163

(ここでmapViewに前回保存したregionごと設定
mapView.region = prev.region!)

設定直後 
latitudeDelta: 0.33684171745417757, longitudeDelta: 0.42023935281952163 
↑なぜか片方だけ値が勝手に変わっている

viewWillAppearでマップに関係ない他の処理をした最後 
latitudeDelta: 0.19593365632808712, longitudeDelta: 0.42023935281952163 
↑なぜか値が正常値に戻る

mapViewWillStartRenderingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402
↑ここでとうとう値が大きく変わり、以下そのまま

mapViewWillStartLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

viewDidAppear 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishRenderingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewWillStartLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

mapViewDidFinishLoadingMap 
latitudeDelta: 0.80467494493102976, longitudeDelta: 1.7258762895046402

対処方法

  • 縮尺を固定する
    • 前回の縮尺を利用っていう希望は実現されないけど、いちばん楽。
  • 表示時か保存時に値をちょっとずつズームインさせて、調整分を吸収する
    • どれくらいズームインするのかわからないので、結局使っているうちに少しずつズームアウトされたり、ズームインしすぎたりしてしまう恐れがある。
    • 必要なズームインの比率がデバイスの違いや、マップサイズの変更、iOSの仕様変更などで細かく変わるかもしれない。
やっぱり縮尺をある程度固定しておくのがいいかねえ?

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))")
}

めんどくさいね

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

2017年10月29日日曜日

Objective-CからSwiftへの移行でNSKeyedunarchiverの不具合

Objective-Cで書いてたアプリをSwift4に書き直してる。
カスタムクラスをファイルに読み書きしているので、NSCodingを使ってNSKeyedUnarchiverを使っているんだけど、Objective-Cそのままのやり方でSwiftに書き直したら、カスタムクラスの
dataArray = NSKeyedUnarchiver.unarchiveObject(with:d) as! [incidentDatas]
のところでSIGABRTになって止まってしまい、その先のrequired init?(coder aDecoder: NSCoder) {〜
が実行されなかった。

エラーメッセージは以下。
2017-10-29 23:53:04.817840+0900 SkyReporter[6679:5156962] *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (incidentDatas) for key (NS.objects); the class may be defined in source code or a library that is not linked'

調べると、Objective-CでNSObjectを継承しているクラスを継承してないSwiftのクラスに持ってくるとダメだとかなんとか出てきたけど、よくわからない。

結局はこちらのページ(ObjCからSwiftに書きなおす際にNSCodingがうまく動かない(解決済))のとおり、
@objc (incidentDatas)
class incidentDatas:NSObject, NSCoding {〜
というように、カスタムクラス名の前に@objcの一文付け加えてやったところ、
required init?(coder aDecoder: NSCoder) {〜
のところを実行してくれたよ。

その先で引っかかってるけど、とりあえず覚書として。

2017年10月24日火曜日

MacのTime Machineボリュームのフォーマット

Time Machineボリュームは、Mac OS拡張(ジャーナリング)じゃなきゃダメ?

Time Machineのボリュームが手狭になったので、新しい外付けHDを買ってきた。
デフォルトでWindows用フォーマットがされてるドライブなので、High Sierraから導入されたAPFSに変更した。
(APFSに変更する際にも面倒なトラブルがあったのだで、最後に書く)

Time Machineのバックアップ内容は最近は単純コピーで移せるというのでドラッグ&ドロップしたのだが、「ボリュームのフォーマットが正しくないためバックアップを作成できません。」と表示されてコピーができない。
フォーマット形式で「大文字/小文字を区別する」にしているとダメらしいが、そんな設定にはしていない。

試しにTime Machineの環境設定から新しいボリュームを選択したところ、「互換性のないファイルシステムだから、ディスク消去せんとあかんよ」とのメッセージ。
どうせ新品なので消去してみたら、フォーマットが「Mac OS拡張(ジャーナリング)」に変更された。どうやらTime MachineはAPFSに対応していないようだ。
この状態でようやくTime Machineのデータをコピーすることができた。

APFS以外が出てこない
ちなみに一度APFSにすると、フォーマットの選択肢にAPFS以外が出てこなくなるので、Time Machine環境設定から変更するしかないみたい。めんどくさいし、おっかないね😩

ディスクユーティリティでHDが扱えなくなった

最初にも書いたとおり、買ってきたIO DATA製の3TB HDはデフォルトでWindowsのフォーマットがされていた。そのままでもMacからアクセスできるんだけど、やっぱりちゃんとMac用のフォーマットに直して使いたい。新しいAPFSはコピーとかが速いって聞くし。

ディスクユーティリティからボリュームを選び、フォーマットを「APFS」にして消去してみたのだが、「消去プロセスを完了できませんでした。続けるには、“完了”をクリックします。」と出てアンマウントしてしまい、再マウントもできなくなってしまった。

調べたところ似たようなケースがあるようで、ターミナルを使ってアンマウント、フォーマットをするしかないようだ。
以下のサイトを参考にさせてもらい、無事にAPFSにすることができた。感謝である。
詳しい手順はリンク先を見て欲しいが、ターミナルから以下の操作を行う。
  1. Macにつながってるボリュームの一覧を表示し、問題のボリューム名を確認。
  2. 問題のボリュームをアンマウント。
  3. 問題のボリュームをHFS+でフォーマット。
  4. 無事マウント!
その後は普通にディスクユーティリティから操作できる。
いくらOSが新しくなっても、こんなめんどくさいことしなきゃいけない局面があるのはいただけないね。

2017年10月20日金曜日

構造体をファイルやUserDefaultsに保存したい

ファイルやUserDefaultsには構造体を直接保存することができない。
そのため、構造体で使われてるメモリ領域を一度バイナリーデータにして(シリアライズ)、それを保存することで実現する。
(構造体の要素を辞書に変換して保存するやり方もあるんだけど、要素一個一個変換しなきゃいけなさそうなのでやったことはない)

構造体 -> Data

たとえばCGRectをDataにしてみる。

 let data = Data(bytes: &self.rect, count: MemoryLayout<CGRect>.size)

bytes:はUnsafeRawPointer。そこに変換したい変数のアドレスを渡す。
count:はその変数が使用してるメモリの大きさ。CGRectが使用するメモリの大きさはMemoryLayout<CGRect>.sizeで求められる。

要は、{アドレス}から始まる{大きさ}分のメモリをDataに直してるわけだ。

Data -> 構造体

逆にDataをCGRectに戻す。

 data.getBytes(&self.rect, length: MemoryLayout<CGRect>.size)

これも同様な引数を渡す。
dataを{アドレス}から始まる{大きさ}分のメモリに書き込んでるような感じ。
実際は書き込むというよりdataのポインタとself.rectのポインタを同じにしてんのかな? よくわからんですが。
注意なのは、このdataはSwiftのData型じゃなく、Objective-Cで使われてたNSDataということ。Dataで同じようなメソッドがあればいいんだけど、Swift4でどう書いたらいいかわからなかったので、暫定的にNSDataで書いてる。
中身がほぼ同じなんで値の扱いは困らないみたいからいいんだけど。

おそらくは
data.copyBytes(to: &self.rect, count: MemoryLayout<CGRect>.size)
のメソッドを書くんじゃないかと思うんだけど、to:に書くUnsafeRawPointerがUInt8とかで、CGRectのアドレスをどう変換したらUInt8にできるのかわかんない。
まあいずれまたわかったら追記する。

2017年9月30日土曜日

シミュレータだとメールが送れない

iOS8、Xcode6当時からずっと直しやがらないバグ。

メールを送信させるために
MFMailComposeViewController *mailPicker = [[MFMailComposeViewController alloc] init];
を書くわけだけど、これがiPhoneシミュレータ上だとnilのままになってしまい、
[self presentViewController:mailPicker animated:YES completion:nil];
で送信用ビューを表示しようとしても落ちる。
iPhone4だと一瞬表示されるけど引っ込んじゃってエラーを吐く。

高い給料と年会費取ってんだからさっさと直せや、ゴルァ!
Appleゥァア゛!
と強く思うけど、実機だと問題ないのでしょうがないけど我慢する。

2017年9月25日月曜日

Xcode 9でArchive時に変なチェックが

Xcode9でアプリを審査提出すべくArchiveしていたら、これまでと手続きが少し変わり、ValidateおよびUploadの段階でこのような同じメッセージが出たので覚書。

App Store distribution options

Include bitcode for iOS content
Allows the App Store to build your app to take advantage of hardware, software or compiler changes.
iOSコンテンツのビットコードを含める
App Storeがあなたのアプリケーションを構築して、ハードウェア、ソフトウェア、またはコンパイラの変更を利用できるようにします。
Upload your app's symbols to receive symbolicated reports from Apple
Crash logs and other diagnostic information from your customers will be symbolicated and viewable within Xcode.
Appleのシンボル化されたレポートを受け取るために、あなたのアプリのシンボルをアップロードしてください。
お客様のクラッシュログやその他の診断情報は、Xcode内で象徴的に表示されます。
最初のチェックのビットコードとは、アプリをコンパイルする時に、最終的に近藤マシン語臣に変換する前段階で、中間コードのようなものに変換するんだそうな。それのことのようで、含めておくといいことあるなんて読んだ。
なんのこっちゃよくわからんけどね。
最初bitcoinって書いてあるのかと思って、驚いた(笑)

次のチェックはおそらくクラッシュ情報なんかをアップルに送信していいかどうかだろう。今までは勝手に送信されてたんじゃないかって思うけど、コンプライアンス上明示し、選択できるようにしたんじゃないかと。

両方ともチェックされたままでいいと思う。

Re-sign "App name"

"App name" needs to be re-signed for App Store distribution.
Select one of the following signing options to continue.
あなたのAppは、App Storeの配布のために再署名する必要があります。
続行するには、次の署名オプションのいずれかを選択します。
App Storeで配布するための署名を自動で管理するか手動でするかの選択。これも上の自動のままでいいだろう。

Distoribution completed with warnings

その後のいつもの手順でUploadは終わったみたいなんだけど、また変なメッセージが出た。
iTunes Storeのオペレーションに失敗したと。
1024x1024のPNGフォーマットのマーケティングアイコンが含まれてないからApp Reviewかベータ App Reviewに提出できないと。
プロジェクトのImages.xcassetsのアイコンのところに1024ptのアイコンを登録する欄が増えとる…orz
他にもiPhoneとiPadでそれぞれ1x〜3xの20ptのアイコンの欄が…。めんどくせえ。
ScreenShot同様に、アイコンもでっかいの一個アップロードして、小さいのはそれを縮小して対応するようにできないのかね? 21世紀なのに…グチグチ…。

アイコン追加してアップロードし直して無事審査提出できたわい。
そのかわりiOS11やiPhone Xへの対応なんかが超テキトーだから、リジェクトされるかもね?(笑)
そんなもんにいちいち対応してられるか! 毎度毎度仕様を変えやがって!

2017年9月23日土曜日

Web関係のViewはどんどん新しく

Webを表示するためのViewは、もともとUIWebViewだったけど、いつの間にかこれdeprecate(使用不可)になってるのな。

かわりに、いつ登場したんだか知らんけど、WKWebView(WebKit View)がオススメになってる。
さらに、SFSafariViewControllerってのもあるようで。

SFSafariViewControllerはXcodeでSafariServices.frameworkをLinkeすると使える。
Xcode9現在、まだStoryboardからは直接扱えないみたいだけど、Safariとほぼ同等の機能が使えるようだ。
デフォルトでタブブラウジングができないものの、cookieとかのデータが共有できるんだと。

カスタマイズして使いたい人はWKWebView、できるだけカスタマイズせずに楽に使いたい人はSFSafariViewみたいな住み分けかしら? 最終的に一本化されるかもしれないけど。

2017年9月21日木曜日

TwitterKit3でツイートできない -> できた

Twitterは公式SDK使用になった

iOS11にアップデートして、自分のアプリからツイッターに投稿しようとしたら、

if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter) { }
のところで値が false になっちまいまして…orz
むろん今まではうまくいってた。

どうやら、他社のサービスは他社のやり方でやれ、Appleはしらんわ…ということらしい。まあセキュリティの問題とかいろいろあるせいとは思うけど、めんどくさくなるのは勘弁してほしい。
ツイッター公式のTwitter kitというSDKがあって、今後はそっちを使わねばいけなくなるようだ。

詳しくは上のサイトのやり方に従って欲しいが、
  1. ツイッターにサインインしてアプリ情報などを入力
    1. 【重要】PermissionがRead & Writeになってないので、Update Settingsボタンを押してやって直す。(詳しくは後述)
  2. Twitter Kit SDKをダウンロードしてXcodeのプロジェクトにインストール(たいていはLinked Frameworks and Librariesに勝手に入ってくれると思う)
  3. Info.plistに項目追加
  4. ソースコードを編集
という手順になるようだ。

SafariServices.framework、AVFoundation.framework、CoreMedia.framework の3つも必要という情報もあったのだが、少なくともTwitterKit3、Xcode9、iOS11においては不要だった。

TwitterKitをゲット

さっそくツイッターのアプリ登録サイトに情報を入力してみたが…

ツイッターアカウントに電話番号情報が必要

…無情にもはじかれる。
You must add your mobile phone to your Twitter profile before creating an application. 
アプリケーションを作成する前に、携帯電話をTwitterプロファイルに追加する必要がありマース。
自分のツイッターアカウントに、セキュリティ用の携帯電話番号を登録しないといけないようだ。
普通にツイッター使うだけならいらないから無視してたんだけど、とうとうこの日がやってきてしまったか。

携帯電話番号登録のメリットとは!?

  • アカウントのセキュリティ
  • アカウント回復にかかる時間が短縮
  • ショートメールを介してTwitterを使用
  • 友だちがあなたのアカウントを見つけやすくなる
    • やめてくれ! むしろ見つけにくくしてくれ!
ツイッターがハッキングされて番号情報が漏れたら嫌だなあと思いつつ、やむをえずTwitter for iOSから携帯電話番号の登録をした。
登録した携帯にショートメールで認証番号が届くので、それを入力してやったらすぐにさっきのページが通ったよ。
これのConsumer Key (API Key)ってのを、後で自分のプロジェクトのInfo.plistに書き込む。
また、このConsumer Keyと、Keys And Access TokensタブにあるConsumer Secret (API Secret)をプロジェクトのAppDelegate内に書き込むことになる。

TwitterKit ダウンロード

現時点のバージョンは
CocoaPodsというiOSライブラリ管理ツールを使うと、手作業でいちいちダウンロードしてプロジェクトに追加して…ってやらずに済むらしいんだけど、最初にコマンドライン使ったりするようで、俺にとってはかえって敷居が高いので、今回は手作業でやってみる。

このページ中ほどの Install Twitter Kit Manuallyにある
1. Download and unzip Twitter Kit.
からダウンロード。

プロジェクトにインストール

この中の左3つを自分のプロジェクトにインストールしてやる。
右2つはTwitterKit.frameworkに内包されてるエイリアスなんだけど、別物と考えてそのままインストールする。

Info.plistの設定

Info.plistに以下の項目を追加する。
プロパティリストのままでもいいし、右クリックでOpen AsからSource Code表示にしてもいい。
// Info.plist
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>twitterkit-ここに取得したconsumerKeyを書く</string>
    </array>
  </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>twitter</string>
    <string>twitterauth</string>
</array>

コードを書きませう

AppDelegate

TwitterKitのimportは言わずもがな。
import TwitterKit

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        //TwitterKit初期設定
        Twitter.sharedInstance().start(withConsumerKey: "ここにConsumerKeyを書く", consumerSecret: "ここにConsumer Secretを書く")
        
        return true
    }

//Twitterにログイン時に、認証トークンをディスクにリダイレクト(転送)し保存
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        
    return Twitter.sharedInstance().application(app, open: url, options: options)
    }

ツイートする処理

    //ツイッターへツイート
    @IBAction func actionBtn(_ sender: AnyObject) {
        //Twitterログイン認証トークンの有無を確認
        if Twitter.sharedInstance().sessionStore.hasLoggedInUsers() {
            //ツイート
            let composer = TWTRComposer()
            composer.setText("イニシャルテキスト") //初期テキスト
            composer.setURL(URL(string: "リンクのURL")) //リンク
            composer.setImage(UIImage(named: "リンクのURL")) //画像
            
            composer.show(from: self) { (result) in
                if result == TWTRComposerResult.done {
                    print("ツイートされた")
                } else {
                    print("ツイートできんかった")
                }
            }
        } else {
            //Twitterログイン処理
            Twitter.sharedInstance().logIn(completion: { (session, error) in
                if session != nil {
                    print("signed in as \(String(describing: session?.userName))")
                } else {
                    print("error: \(String(describing: error?.localizedDescription))")
                }
            })
        }
    }

うまくいかん!

以上でツイートできるはずなのだが、結果は失敗。

今までのSLComposeViewControllerのちっちゃいウィンドウと違い、もっとちゃんと大きなツイートのウィンドウが開き、画像もURLも貼られてるのに、ツイートボタンを押してもツイートされない。

エラーメッセージとしては以下のものが出るが、さっぱりわからん。
CredStore - performQuery - Error copying matching creds.  Error=-25300, query={
    class = inet;
    "m_Limit" = "m_LimitAll";
    ptcl = htps;
    "r_Attributes" = 1;
    sdmn = "https://api.twitter.com";
    srvr = "api.twitter.com";
    sync = syna;
}

Did encounter error sending Tweet: Error Domain=TWTRNetworkingErrorDomain Code=-1011 "Request failed: unauthorized (401)" UserInfo={NSLocalizedFailureReason=, TWTRNetworkingStatusCode=401, NSErrorFailingURLKey=https://api.twitter.com/1.1/statuses/update.json, NSLocalizedDescription=Request failed: unauthorized (401)}

[TwitterKit] Composer did fail: Error Domain=TWTRNetworkingErrorDomain Code=-1011 "Request failed: unauthorized (401)" UserInfo={NSLocalizedFailureReason=, TWTRNetworkingStatusCode=401, NSErrorFailingURLKey=https://api.twitter.com/1.1/statuses/update.json, NSLocalizedDescription=Request failed: unauthorized (401)}

調べると他の人も同じことで困ってる人がいるみたい。ここにも。ここにも。
ツイート内に「^ < > \ | ` または \r」の文字が使われていると401エラーになるそうだが、使ってないしなあ。

Fabricってのが必要?

2014年10月にTwitter社が発表したモバイルアプリ開発プラットフォーム。
安定性、利用者増、収益化、ユーザー認証の側面を網羅した開発キットだそうだ。
どうやらTwitterアプリを開発するにあたってこいつを使ってる人が多いらしいのだが、ツイートするだけとかならいらないのかもしれず、Twitter開発者のサイトでも「Upgrade from Fabric」として少ししか書かれてない様子。
もしどうしても必要なら調べて使うけど…??

うまくいった!

原因はTwitterKitのWebにある管理画面のパーミッションの設定で、表示と実際が違っていたためだ。
Read and Writeが嘘っぱちだった!
デフォルトの設定は表面上Read and Writeになってるんだけど、これが実際はRead onlyになっているものだから、ツイートしようとしても失敗してしまうのだ。
そりゃよっぽど注意しないと気づかねえってば。
責任は完全にTwitter社にある。

アカウント連携時の注意書きを見ると、ツイートできませんって!
それじゃツイートできるわきゃないよ。

直し方

管理画面でPermissionsタブの下の方にあるUpdate Settingsを押してやると、選ばれたとおりのPermissionに直ってくれる。
Fabricとかは全然いらない。
ツイッターゥァア゛ーーー
こんなもので何日間も悩ませやがって!