Swift で循環参照するケース
循環参照という言葉を最近よく耳にしていて、なんとなく相互参照の結果メモリリークしてしまうというのは分かっていたのですが、じゃあどうしたら起きてしまうのかというところがあやふやだったので調べてみました。
循環参照
インスタンスは参照カウンタという値を持っており、変数に代入されたりして強参照されることで参照カウンタが 1 増えます。この参照カウンタが 0 になるとインスタンスは解放されます。
class Sample {} // Sample クラスのインスタンスが生成されるが参照カウンタが 0 のままなのですぐに解放される Sample() // Sample クラスのインスタンスが生成されて sample に代入されているので参照カウンタが 1 となり解放されない var sample: Sample? = Sample() // nil を代入すると参照がなくなるのでインスタンスの参照カウンタが 0 になり解放される sample = nil
循環参照とは、参照カウンタが 0 になり得ない状態をつくってしまう参照のことです。
これにより、インスタンスは解放されることなく残り続けてしまい、メモリリークとなってしまいます。
具体的なケースをいくつか書いてみます。 ( Swift 3.0 )
図では、赤枠が変数、青枠がインスタンス、緑枠がクロージャを表しています。
ケース1
class Computer { var printer: Printer? } class Printer { var computer: Computer? } var computer: Computer? = Computer() // Computer の参照カウンタ1 var printer: Printer? = Printer() // Printer の参照カウンタ1 computer.printer = printer // Printer の参照カウンタ2 printer.computer = computer // Computer の参照カウンタ2 computer = nil // Computer の参照カウンタ1 printer = nil // Printer の参照カウンタ1 // 各インスタンスの参照カウンタは1なので解放されていない // しかしプログラムから参照することはできない
各インスタンスのプロパティで一方のインスタンスを強参照しているので、変数からの参照がなくなった後も各インスタンスは解放されません。
変数からインスンタンスへの参照がなくなったことによりプログラムからインスタンスを参照することができなくなってしまい、無駄なインスタンスが生じている状態になっています。
対処例
このケースはプロパティに対して弱参照を用いることで解決できます。
class Computer { var printer: Printer? } class Printer { // weak をつけることで弱参照となる weak var computer: Computer? } var computer: Computer? = Computer() // Computer の参照カウンタ 1 var printer: Printer? = Printer() // Printer の参照カウンタ 1 computer.printer = printer // Printer の参照カウンタ 2 printer.computer = computer // Computer の参照カウンタ 1 // 弱参照は参照カウンタを増やさない computer = nil // Computer の参照カウンタ 0 , Printer の参照カウンタ 1 // computer が解放されることで computer.printer からの Printer への参照もなくなる printer = nil // Printer の参照カウンタ 0
weak
をつけてプロパティ定義をすると、そのプロパティからインスタンスへの参照は弱参照となります。
弱参照とは、参照カウンタを増やさずに参照する方法で、これにより循環参照を防ぐことができます。
delegate を実装するときに delegate プロパティに対して weak
をよくつけるかと思いますが、それはこのケースのためです。
ケース2
class Printer { var printClosure: (() -> Void) init() { self.printClosure = { print("Printer name is", self) } } func run() { printClosure?() } } var printer: Printer? = Printer() // Printer の参照カウンタ 2 , クロージャの参照カウンタ 1 printer = nil // Printer の参照カウンタ 1
クロージャは、生成されたタイミングでクロージャ内で扱われているインスタンスへの参照をキャプチャします。つまり、クロージャが解放されるまで参照が保持されるので、強参照をしている場合はインスタンスの参照カウンタが 1 増えたままということになります。
そのため、上記のケースでは、 Printer のインスタンスが生成されたときに self ( = Printer ) を強参照するクロージャが生成されているので、 Printer のインスタンスの参照カウントが 1 になります。
そして、このクロージャをプロパティへ代入しているため、 Printer のインスタンスがクロージャを強参照し、クロージャもまた Printer のインスタンスを強参照しているので循環参照となっています。
対処例
ケース1と同様にプロパティを弱参照とすることもできますが、クロージャがキャプチャする参照を弱参照へと変更する方法もあります。
その場合は以下のように [weak self]
と記述します。
class Printer { var printClosure: (() -> Void) init() { // self の参照を弱参照にする self.printClosure = { [weak self] in print("Printer name is", self ?? "") } } func run() { printClosure?() } } var printer: Printer? = Printer() // Printer の参照カウンタ 1 , クロージャの参照カウンタ 1 printer = nil // Printer の参照カウンタ 0, クロージャの参照カウンタ 0
これにより循環参照を防ぐことができます。
しかし、弱参照とすることで参照先のインスタンスが解放されることを許容することになるので、クロージャ内ではオプショナル型として扱われます。上記の場合では、 self はオプショナル型となっています。そのため、オプショナルバインディング等でアンラップする必要があります。
unowned
ちなみに unowned
というのもあり、これをつけることで参照カウンタを増やさずに参照することができます。
weak
との違いは、 オプショナル型と暗黙的オプショナル型の違いのように、 参照先が解放されてしまっている ( nil
になっている ) ときに実行時エラーとなるかどうかということのようです。
weak
は実行時エラーになりませんが、 unowned
は実行時エラーとなります。
ですが、 unowned
の場合はオプショナルバインディング等でアンラップする必要がありません。
ケース3
class Computer { var printer: Printer? func printName() { printer?.run { print("Computer name is", self) } } } class Printer { var printClosure: (() -> Void)? func run(printClosure: @escaping () -> Void) { self.printClosure = printClosure run() } func run() { printClosure?() } } var computer: Computer? = Computer() // Computer の参照カウンタ 1 var printer: Printer? = Printer() // Printer の参照カウンタ 1 computer?.printer = printer // Printer の参照カウンタ 2 computer?.printName() // Computer の参照カウンタ 2 , クロージャの参照カウンタ 1 computer = nil // Computer の参照カウンタ 1 printer = nil // Printer の参照カウンタ 1
このケースでは、 func printName()
実行した時点で循環参照となってしまっています。
このメソッド内では クロージャを受け取るメソッド func run(printClosure: @escaping () -> Void)
を実行しているのですが、渡したクロージャが Printer のプロパティへと代入されてしまっているのでクロージャへの強参照が生じています。
これにより、クロージャが Computer のインスタンスを self
で強参照する、 Computer のインスタンスがプロパティで Printer のインスタンスを強参照する、 Printer のインスタンスがプロパティでクロージャを強参照するという循環参照となってしまっています。
対処例
class Computer { var printer: Printer? // self の参照を弱参照にする func printName() { [weak self] in printer?.run { print("Computer name is", self ?? "") } } } class Printer { var printClosure: (() -> Void)? func run(printClosure: @escaping () -> Void) { self.printClosure = printClosure run() } func run() { printClosure?() } } var computer: Computer? = Computer() // Computer の参照カウンタ 1 var printer: Printer? = Printer() // Printer の参照カウンタ 1 computer?.printer = printer // Printer の参照カウンタ 2 computer?.printName() // Computer の参照カウンタ 1 , クロージャの参照カウンタ 1 computer = nil // Computer の参照カウンタ 0 , Printer の参照カウンタ 1 printer = nil // Printer の参照カウンタ 0 , クロージャの参照カウンタ 0
他と同様に弱参照を用いて防ぎます。
このケースの厄介な部分は、 func printName()
の処理を見ただけだとクロージャがどう扱われているのかわからないという点です。 今回は func run(printClosure: @escaping () -> Void)
の処理を見たときに、クロージャをプロパティへ代入していることがすぐにわかりますが、メソッド内の処理が複雑になってくると、クロージャがどう扱われるのかというのが把握しづらくなってしまいます。
@escaping
そこで @escaping
というものがあります。
func run(printClosure: @escaping () -> Void)
という部分で使われていますが、 これが付いていると、そのクロージャがどこかで強参照されるということを保証してくれます。そのため、このメソッドにクロージャを渡す際は、クロージャ内で扱うインスタンスを [weak self]
のように弱参照としておく必要があるということがすぐわかります。
まとめ
なんとなくわかった気がしたのですが、普段コードを書いていてもシュッと「これは循環参照するぞ」なんてわからなそうなので、迷ったらとりあえず weak
をつけておけばいいのかなあという感じがしました。
追記
上記のエントリで改めて @escaping 属性について書きましたが、 @escaping 属性がついていれば循環参照の可能性があるので弱参照にする、ついていなければ強参照にするで良さそうです。
UIPickerView の使い方
UIPickerView とはこういうやつです。
これを使うための最低限の部分です。
UITableView 等と同様に DataSource と Delegate を設定して、表示する列数、行数、要素を各メソッドで return するといった感じです。
import UIKit class ViewController: UIViewController { @IBOutlet weak var pickerView: UIPickerView! { didSet { pickerView.dataSource = self pickerView.delegate = self } } let components = (1...100).map { "\($0)" } } extension ViewController: UIPickerViewDataSource { // 列数 // 1列であってもこのメソッドを省略できない func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } // 行数 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return components.count } } extension ViewController: UIPickerViewDelegate { // 要素 func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return components[row] } }
各要素の表示領域の backgroundColor を変更したりとか色々手を加えたい場合は、上記の Delegate メソッドではなく
func pickerView(UIPickerView, viewForRow: Int, forComponent: Int, reusing: UIView?)
UIPickerViewDelegate - UIKit | Apple Developer Documentation
を使って、色々手を加えた View を返すようにすることでできそうです。
ちなみに、UIDatePicker の countDownTimer の hour や min のように固定ラベルをつけたいとなっても、それようのプロパティやメソッドはなさそうでした。 やるなら UIPickerView とは別途、固定の UILabel を置く等しなければならなそうです。
UIAlertController 使ってみた
UIAlertController を使うことで Alert や Action Sheet を画面に表示することができます。 Alert や Action Sheet とはこういうやつ↓です。
Alert | Action Sheet |
---|---|
使う際の流れとしては、以下のような感じです。 Alert も Action Sheet もやることは同じなので Alert について書きます。
- Alert となる UIAlertController のインスタンスを生成する
- 表示する各項目 (キャンセルボタンとか) を作る
- 各項目をインスタンスに設定する
- 表示する ViewController で present する
Alert となる UIAlertController のインスタンスを生成する
インスタンス生成時にタイトルとメッセージと Action か Action Sheet かを設定します。
let alert = UIAlertController(title: "Alert", message: "Alert Message", preferredStyle: .alert // ここを .actionSheet に変えると Action Sheet になります )
表示する各項目 (キャンセルボタンとか) を作る
項目の style (UIAlertActionStyle) は .default
, .destructive
, cancel
の3つがあります。
.cancel
以外は複数指定できます。
let defaultAction = UIAlertAction(title: "Default", style: .default, handler: { action in print("Default") }) let destructiveAction = UIAlertAction(title: "Destructive", style: .destructive, handler: { action in print("Destructive") }) let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: { action in print("Cancel") })
各項目をインスタンスに設定する
インスタンスに対して addAction(action: UIAlertAction)
で設定できます。
alert.addAction(defaultAction) alert.addAction(destructiveAction) alert.addAction(cancelAction)
表示する ViewController で present する
現在表示されている ViewController で Alert を表示する場合は以下のように書きます。
present(alert, animated: true, completion: nil)
iPad 対応
iPad の場合、 Action Sheet はポップオーバーで表示されます。
Action Sheet for iPad |
---|
この時、以下の設定がされていないとクラッシュするので忘れないよう気をつけてください。
// 表示先の View actionSheet.popoverPresentationController?.sourceView = view // 表示場所 actionSheet.popoverPresentationController?.sourceRect = rect
サンプル
こちらにサンプルを置いたので良かったら試してみてください。
shibuya.swift #5 で初めての LT をしてきた
shibuya.siwft で生まれて初めての LT をしてきました。
shibuya.swift にはお手伝いとして何度か参加させてもらっていたのですが、今回は自分が話す側だったのでとても緊張しました。頭が真っ白になることなく無事終わったので良かったです。
話すことになった経緯
会社の先輩に「なんか話してよ」と言われたので、「あ、はい、わかりました!(参加ボタンぽち)」という感じで LT をすることになりました。
自分は、今年配属されてからの6ヶ月間は Ruby on Rails でのAPI開発をやってきました。iOS開発をやるようになったのは7月からで、まだ3ヶ月しか経っていません。そのため、話す内容なんてない...とか思っていたのですが、勢いで参加ボタンを押してしまったので、何かネタを見つけなければと思いひたすら考えていました。
ペーペーの自分が技術的な話をするのはちょっと厳しいなと思っていたので、なんか経験談みたいのを話せればなあと思っていました。
だったら新卒の数ヶ月間で API開発とiOS開発両方やってきた人はあんまりいなそうだからそういうこと話せばいいのかあ、でも需要あるのかなあとか思っていたところ、「新卒でAPI開発からiOS開発やってきてどうだったかみたいな話が面白そう」という助言をいただいたので、やっぱりこれしかないと思い内容を練り始めました。
資料づくり
LT は見たことあるけどどうやって資料作れば良いかわからん!という感じだったので、まずは他人の発表資料を漁って、 LT の資料とはどんな感じかをつかむところから始めました。 資料だけだとちょっとよくわからんという感じもあったので、動画も見たりしました。
他にもLT 資料 作り方
みたいな感じでググったりしてみたりもしました。
まあ雰囲気は掴めた気がしたのですが、結局よくわからんとなったので、内容を練ることにしました。
まずは頭の中で、どんなことをやってきたか、どんなことを学んできたかを振り返ってみました。 ざっと、こういうことがあったなあという感じには振り返ることができ、なんとなくの話の流れをイメージすることができてので、次に、各話をもっと深堀りするためにマインドマップを書いてみました。
まあ書いてみたのですが、頭でざっと振り返った以上のことは書けずに特に進展がなかったので 、 実際にどんなことがあったのか具体的に思い出すために、 GitHub の PR や issue を眺めることにしました。
ネタ探しにはこれが一番効果がありました。この時に自分はどう考えて、どういうアドバイスをもらって、どういうことを学んだのかということを結構具体的に思い出すことができました。
そして、項目ごとに文章に書き出してみました。これを幾つか書いていると、ここの話を書こうとか、ここはやめておこうとか結構まとまってきたので、先輩に見てもらって、内容とか流れをブラッシュアップしていきました。
これを何回か繰り返して文章ではまとまったので、スライドを作り始めました。
まずスライドにはひたすら箇条書きで書いていきました。最初から最後までずらっと適当にスライドに書き起こしてから、一通りの流れを見て、それからスライドを分割したり、まとめたりを繰り返していきました。
最後にレイアウトやデザインを少しだけ修正して完成させました。
練習
あまり緊張せずとも人前で話すことができる人もいるかと思いますが、自分はとても緊張してしまいます。
今回緊張してしまう理由は、大人数の・初対面の人に対して・LT をする ことに慣れていないことだと思います。常日頃当たり前にやっていることは緊張しないのと同様に、これにも慣れてしまえば緊張もしなくなると思います。
ただ、大人数という部分にすぐ慣れるなかなか難しいかと思います。初対面の人に対してという部分も同様です。ですが、LT をするという部分は、練習を重ねることで慣れることができます。そのために、何度も練習を繰り返しました。
練習は、以下の4つのフェーズに分けて行いました。
- 原稿を読みながら練習する
- 原稿をなるべく見ないようにして練習する
- スライドだけを見ながら練習する
- 何も見ずに練習する
これらをタイマーで時間をはかったり、音声を録音したりしながら行いました。
本番
とにかくやるだけ!!
やってみて
慣れていないことをするにはとても抵抗があって、最初の一歩がなかなか踏み出せないのですが、なんだかんだやってしまえばやれるもだなあと思いました。クオリティはどうであれ。
今回の LT も準備すればできるということが分かり、とても自信となったので、また機会があればやってみたいなあという気持ちになりました。
今後は、技術的な話にもチャレンジしていきたいです。
最後に、会場準備をしてくださった Fabric さん、ありがとうございました!
Swift の Class, Protocol を Objective-C で使う
Objective-C 側で ProjectName-Swift.h を import すれば使えるでしょとか思っていたのですが、すんなり使えなかったので書いておきます。
Class を使う
SampleObjcClass.h と SampleObjcClass.m からなる Objective-C のクラス内で、 Swift で書かれた SampleSwiftClass を使いたいとします。 この方法は、ヘッダファイル (.h) と インプリメンテーションファイル (.m) で違います。
インプリメンテーションファイルでは、ProjectName-Swift.h を import するだけで大丈夫です。
// SampleObjcClass.m #import "ProjectName-Swift.h" @implementation SampleClass @end
ヘッダファイルの場合は、対象のクラスを class で宣言することで呼び出せるようになります。
// SampleObjcClass.h @class SampleSwiftClass @interface SampleObjectiveCClass @end
継承する
Objective-C のクラスで Swift のクラスを継承することはできないようです。
そのため、 Swift のクラスを継承したいときは Objective-C のクラスを Swift に書き換える必要があります。何か別の方法あったら教えていただきたいです、、、
Protocol を使う
先ほど同様、 SampleObjcClass.h と SampleObjcClass.m からなる Objective-C のクラス内で、 Swift で書かれた SampleSwiftProtocol を使いたいとします。
この場合、 Swift 側で SampleSwiftProtocol の定義に @objc を付ける必要があります。
// SampleSwiftProtocol.swift @objc protocol SampleSwiftProtocol { }
Objective-C 側は、クラスの場合と同様にインプリメンテーションファイルでは、ProjectName-Swift.h を import するだけで大丈夫です。
// SampleObjcClass.m #import "ProjectName-Swift.h" @implementation SampleClass () @end
ヘッダファイルの場合は、対象のプロトコルを @protocol で宣言することで呼び出せるようになります。
// SampleObjcClass.h @protocol SampleSwiftProtocol @interface SampleObjectiveCClass @end
適用する
適用する定義は、インプリメンテーションファイルにて記述する必要があるようです。ヘッダファイルではエラーとなってしまいます。
// SampleObjcClass.m #import "ProjectName-Swift.h" @implementation SampleClass () <SampleSwiftProtocol> @end
// SampleObjcClass.h @protocol SampleSwiftProtocol // エラーになる @interface SampleObjectiveCClass <SampleSwiftProtocol> @end
参照
iOSDC Japan 2016 に参加してきた!
8/19, 20 にかけて開催された iOSDC Japan 2016 に参加してきました!
記念すべき第一回でした!
次回開催についてですが、 SNS やブログ等で拡散されていけばあるかもということでした。是非来年も開催されてほしいです!
iOSDC Japan 2016 の詳細はこちら↓
自分は iOS 開発を始めて期間が浅く、話を聞くのでいっぱいいっぱいで全然メモを取れませんでしたが、わずかに残したのでそれをちょろっと書きます。発表資料を公開されている方もいるので、そちらのリンクも貼っておきます。
AB Tests in Mobile App
kazunori kikuchi (@kichikuchi) さん
iOS アプリ開発の補助ツールのベストプラクティス
宇佐見 公輔 (@usamik26) さん
ライブラリ管理
Carthage とか
- アプリとライブラリが分離される
- バージョンが分かる
リソース取り込み
SwiftGen とか
コードチェック
SwiftLint とか
- Warning を見逃さない
- 量が多すぎて辛いならば、チェックをゆるくして、チェックを継続したほうが良い
- そうすれば大事な Warning を見落とさない
- SwiftLint の autoformat とかのコードフォーマットで自動変換できる
デザイナーにStoryboardをお任せする技術
Hiroki Kato (@cockscomb) さん
iOSアプリのリモートサポートツール「ミレタ」の作り方 #WebRTC #Swift #PrivatePod
Yuichiro Masui (@masuidrive) さん
Swift で JavaScript 始めませんか?
熊谷 友宏 (@es_kumagai) さん
www.slideshare.net
Xcode で快適なデバッグライフを追い求める
Toshihiro Morimoto @dealforest さん speakerdeck.com
クラッシュしたら AppDelegate だった
ExceptionBreakpoint を追加
exception が発生したタイミングで breakpoint
意図とは違うタイミングで break してしまうことがDiagonostics を設定
不正なメモリ操作を検知
どの ViewController かわからない
Symbolic Breakpoint with action を使って viewDidLoad と viewWillApear が呼ばれた時に break されるようにする
起動時に ViewController を指定したい
EnvironmentVariables で設定する
端末のログやファイルを取得したい
LLDB を使って slack に送信
全然メモ取っていなくてすみません。。。
constraint を動的に追加 / 削除する
AutoLayout でレイアウトに必要な constraint を追加する場合、 Storyboard 上で指定する方法と、コードで指定する方法があります。
Storyboard では静的な constraint であれば簡単に追加できますが、動的に追加、そして削除したい場合には、コードで指定しなければならないと思います。
以下では、対象の constraint はつくられているとして、その constraint を動的に追加 / 削除する方法を記述します ( iOS8 以上) 。
方法は、メソッドによる指定とプロパティによる指定の 2 通りがあります。
メソッドによる指定
これらのメソッドは、引数に [NSLayoutConstraint]
をとるため、複数の constraint を 1 行で指定することができます。
let constraints = [hogeConstraint, fugaConstraint] NSLayoutConstraint.activateConstraints(constraints) // constraint の追加 NSLayoutConstraint.deactivateConstraints(constraints) // constraint の削除
プロパティによる指定
単一の constraint を指定する場合にはこちらの方法で良いかと思います。
個人的にはこちらの方が読みやすくて好きです。
hogeConstraint.active = true // constraint の追加 hogeConstraint.active = false // constraint の削除
注意点
注意したいのは、 削除した constraint を再び追加する場合です。
具体的には、 Stroyboard 上で指定した constraint を IBOutlet で接続し、それを状況に応じて削除、そして再び追加するということをしたい場合です。
このとき、constraint の参照を weak として IBOutlet で接続してしまうと、削除したのちに再び追加しようとするとエラーになってしまいます。
@IBOutlet internal var hogeConstraint: NSLayoutConstraint! // weak 参照 hogeConstraint.active = false hogeConstraint.active = true // 実行時エラーになる
このエラーを解消するためには、 参照を strong にする必要があるようでした。
@IBOutlet internal var hogeConstraint: NSLayoutConstraint! // strong 参照 hogeConstraint.active = false hogeConstraint.active = true // エラーにならない
追記
strong にする必要があると書きましたが、強参照にすると循環参照の恐れがあるので、weak のままで true にする直前にフォースアンラップ等で nil チェックをする方が良さそうかなという気がしています。