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にできるのかわかんない。
まあいずれまたわかったら追記する。