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