- 投稿日:2021-03-25T22:31:23+09:00
【2021年版】XcodeへのCrashlyticsの設定方法
実施時期:2021月03月25日
XcodeへのCrashlyticsの設定方法について、Firebaseの公式ドキュメントのみでは分かりづらい箇所があったためこちらに記録。
まずは公式の設定方法
https://firebase.google.com/docs/crashlytics/get-started?platform=ios&authuser=1#add-sdk特に
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
や
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
をどこに設定したら良いのか?や
どのようにするとクラッシュレポートが上がるのか?
更にいくつかエラーが出たのでその内容も。誰かの助けとなれば幸いです。
ステップ 1: Firebase コンソールで Crashlytics を設定する
これは分かりやすくドキュメントも多いので省略
ステップ 2: アプリに Firebase Crashlytics を追加する
1.Firebase プロジェクト用に作成した podfile を開き、FirebaseCrashlytics ポッドを追加します。Firebase Crashlytics でのエクスペリエンスを最適化するために、Google アナリティクス用の Firebase ポッドもアプリに追加して、プロジェクトで Google アナリティクスを有効にすることをおすすめします。クラッシュの影響を受けていないユーザーとパンくずリストをリアルタイムで取得するには、バージョン 6.3.1 以降の Google アナリティクスを追加してください。
特にイレギュラーはなかったので省略
2.ポッドをインストールし、.xcworkspace ファイルを再度開いて Xcode でプロジェクトを表示します。
特にイレギュラーはなかったので省略3.Firebase モジュールを UIApplicationDelegate にインポートします。
AppDelegate.swiftの中の上部に
AppDelegate.swiftimport Firebaseを追記した。
4.FirebaseApp 共有インスタンスを構成します。通常はアプリの application:didFinishLaunchingWithOptions: メソッドで行います。
AppDelegate.swiftの中で
AppDelegate.swiftclass AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() // Override point for customization after application launch. return true }5.アプリを再コンパイルします。
こちらを実施。
ステップ 3: Crashlytics を初期化する
1,2,3,4,5を一気にまとめる。ここがどこに何を入れるかが分かりづらい。
Xcode でプロジェクトを開き、左側のナビゲータでプロジェクト ファイルを選択します。
[Select a project or target] プルダウンから、メインのビルド ターゲットを選択します。
[Build Phases] タブを選択し、add > [New Run Script Phase] の順にクリックします。
表示される [Run Script] セクションを展開します。スクリプト フィールド([Shell] フィールドの下)で、新しい実行スクリプトを追加します。
${PODS_ROOT}/FirebaseCrashlytics/run
アプリの dSYM の場所を、大規模なアプリに対する Crashlytics の自動 dSYM 生成を高速化する入力ファイルとして追加します。次に例を示します。
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
アプリのビルド済み Info.plist の場所を、ビルドフェーズの [Input Files] フィールドに指定することもできます。
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
以下具体的な方法:
Build Settingsの中の左上の+アイコンからNew Run Script Phaseを選択すると、リストの最下部にRun Scriptという項目生成される。
Shellの/bin/shの下のボックスには
${PODS_ROOT}/FirebaseCrashlytics/run
のみを入れ、
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
および
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
は、下の方にあるInput Filesの中に1つずつ入力する。※ここが公式の説明だとよく分からなかった。DWARF with dSYM Fileの設定
公式の同じページには記載がなかったが、調べるとクラッシュログを上げるためにはこの設定も必要かもしれなさそうだったので、こちらも行った。
Build Settingsの中のDebug Information Formatの中をこのようにDebug, Release共にDWARF with dSYM Fileにする。ビルド時のエラー
ここまでの手順を行っても、このようなエラーが出た。
for architecture arm64 linking in object file built for iOS
解決策を以下に示す:
Excluded ArchitecturesのDebug、Releaseそれぞれにarm64
を追加する
-> こちらを行うと実機ビルドでarchitecture周りのエラーが出たため、削除した。
削除すると実機ビルドもシミュレータービルドもうまくいったため、不要かもしれない。上記のarm64を消した後のSchemaの状態は以下の通り。
この状態でシミュレーターと実機でのクラッシュ両方のデータが上がっていることが確認できた。その他のエラー
自分の場合は、iOSのプロジェクトファイルを入れているディレクトリまでのパスの中にGoogle Driveというようにスペースが入ってしまっている箇所があったため、そこでもNo such dir〜という旨のエラーが出た。
解決策としては単にディレクトリの名前からスペースを削除すれば良い。以上で設定ができました。
クラッシュさせる方法
任意の場所で以下のようにエラーを起こしてクラッシュさせる
.swift
fatalError()
ここで注意点としては、シミュレーターでアプリを起動させている状態だとクラッシュログが上がらないらしく、
シミュレータ上で一度アプリをスワイプして消して、再度アプリアイコンを押して単独でアプリを起動させ、クラッシュさせることでログが上がった。以上でクラッシュのレポートが取得できるはず。
少しでも誰かの助けになればと思います。
Twitterの方でも仲良くしてくれる人が増えると嬉しいです!
My Twitter Account
- 投稿日:2021-03-25T17:23:27+09:00
【iOS】Core Audioでシンセサイザーを作る
iOSのCore Audioを使って、波形を生成するシンセサイザーを作ってみました。あわせてエフェクターもいくつか実装しています。
その作り方を解説します。
できたもの
音のサンプルをSoundCloudにアップしています。アナログシンセのような感じが出ていると思います。
画面はこんな感じです。鍵盤ではなく、スライダーで音程を調整します。
ソースコードはこちらです。複数のサンプルがありますが、今回解説するのは「Synthesizer」になります。
https://github.com/TokyoYoshida/CoreAudioExamples作り方の概要
Core Audioの構成要素であるAVAudioEngineを使います。波形を生成するレンダー関数を作り、これをソースノードとしてミキサーノードにつなぎ、出力ノードから出力することで、任意の波形を音声出力することができます。
作り方
本稿は「Synthesizer」の解説記事ですが、波形の作り方は「AudioEngeneGenerateWave」の方がシンプルで分かりやすいので、まずはこれをベースに解説していきます。
1. AVAudioEngineを生成する
AVAudioEngineを生成して各種の情報を取得します。
AudioEngeneWaveGenerator.swiftclass AudioEngeneWaveGenerator { var audioEngine: AVAudioEngine = AVAudioEngine() var sampleRate: Float = 44100.0 var deltaTime: Float = 0 var mainMixer: AVAudioMixerNode? var outputNode: AVAudioOutputNode? var format: AVAudioFormat? func initAudioEngene() { mainMixer = audioEngine.mainMixerNode outputNode = audioEngine.outputNode format = outputNode!.inputFormat(forBus: 0) sampleRate = Float(format!.sampleRate) deltaTime = 1 / Float(sampleRate) } // 〜略〜2. レンダー関数を作る
波形をレンダーする関数を作ります。ここではサイン波を生成しています。
sin関数に周波数(currentTone)と、2π(単位時間で1周する)、時間を与えて生成します。currentToneに、440.0を与えればラの音(A)になります。
AudioEngeneWaveGenerator.swift# class AudioEngeneWaveGeneratorの一部 static let toneA: Float = 440.0 var time: Float = 0 lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in let abl = UnsafeMutableAudioBufferListPointer(audioBufferList) for frame in 0..<Int(frameCount) { let sampleVal: Float = sin(AudioEngeneWaveGenerator.toneA * 2.0 * Float(Double.pi) * self.time) self.time += self.deltaTime for buffer in abl { let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer) buf[frame] = sampleVal } } return noErr }3. 音を鳴らす
実際に音を鳴らします。
AVAudioEngineに、AVAudioSourceNodeやAVAudioOutputNodeをコネクトしていくことで、音の生成から出力までをルーティングすることができます。合わせて、AVAudioSourceNodeのフォーマットを指定したり、AVAudioOutputNodeのボリュームを設定します。
準備ができたら、AVAudioEngineのstartメソッドで再生開始です。
AudioEngeneWaveGenerator.swift# class AudioEngeneWaveGeneratorの一部 func start() { refData.frame = 0 let inputFormat = AVAudioFormat(commonFormat: format!.commonFormat, sampleRate: Double(sampleRate), channels: 1, interleaved: format!.isInterleaved) audioEngine.attach(sourceNode) audioEngine.connect(sourceNode, to: mainMixer!, format: inputFormat!) audioEngine.connect(mainMixer!, to: outputNode!, format: nil) mainMixer?.outputVolume = 0 do { try audioEngine.start() } catch { fatalError("Coud not start engine: \(error.localizedDescription)") } }これで音が鳴るようになりました。
4. 音の再生を止める
音の再生を止めるメソッドも作っておきます。
AudioEngeneWaveGenerator.swift# class AudioEngeneWaveGeneratorの一部 func stop() { audioEngine.stop() }5. 音を差し替えられるようにする
ここからは、サンプル「Synthesizer」の説明になります。
ここまではAudioEngeneWaveGeneratorクラスを実装していましたが、ここからはSynthesizerクラスになります。実装はほぼ同じなので、追加したところだけを説明します。
まず、音を差し替えたり、エフェクターをつなげたりできるようにします。
これは、波形の生成をする関数をprotocolで抽象化し、それぞれの部品をクラスにすることで実現できます。
今回は次のような構成で作ってみました。protocol AudioSourceは、音程(tone)と、時間を与えるとその時点の波形を出力するメソッド(signal)を持ちます。
Synthesizer.swiftprotocol AudioSource { var tone: Float {get set} func signal(time: Float) -> Float }そして先程説明したレンダー関数の波形生成部分は、AudioSourceに委譲するようにします。
Synthesizer.swift// class Synthesizerの一部 private var audioSource: AudioSource? lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in let abl = UnsafeMutableAudioBufferListPointer(audioBufferList) guard let oscillator = self.audioSource else {fatalError("Oscillator is nil")} for frame in 0..<Int(frameCount) { let sampleVal: Float = oscillator.signal(time: self.time) // この部分 self.time += self.deltaTime for buffer in abl { let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer) buf[frame] = sampleVal } } return noErr }6. オシレーターを作る
サイン波を発生させるオシレーターを作ります。先ほど紹介したサイン波の数式をclassでくるんでいるだけです。
updateToneメソッドの詳細は割愛しますが、音色を急に変化させてもゆっくりなめらかにcurrentToneを変化させるための処理です。これがなくても動作しますが、音色を変化させたときにプチプチとノイズが入ります。
Synthesizer.swiftclass SinOscillator: ToneAdjuster { // ToneAdjusterはupdateToneの実体を定義するためのもので、それ以外はAudioSourceプロトコルと同じ override func signal(time: Float) -> Float { updateTone() return sin(currentTone * 2.0 * Float(Double.pi) * time) } }7. ミキサーを作る
ミキサーは、AudioSouceを継承しつつ、エフェクターを加えることができるプロトコルとして宣言します。
Synthesizer.swiftprotocol Mixer: AudioSource { func addEffector(index: Int, effector: Effector) func removeEffector(at index: Int) }ミキサーの実装であるAudioMixerは、オシレータを1つ、エフェクターを複数保持しています。
Synthesizer.swiftclass AudioMixer: Mixer { private var oscillator: Oscillator private var effectors: [Int:Effector] = [:] // 〜略〜ミキサーのsignalメソッドが、実際にレンダー関数から呼ばれるところです。
オシレーターに時間を渡して波形を生成させ、エフェクターに波形を渡し、その出力をさらに別のエフェクターに渡すということを繰り返しています。
セマフォによる排他制御をしているのは、波形の処理中に画面側でオシレーターやエフェクターを変更したときの競合を避けるためです。
Synthesizer.swift// class AudioMixerの一部 let semaphore = DispatchSemaphore(value: 1) func signal(time: Float) -> Float { semaphore.wait() var waveValue = oscillator.signal(time: time) for (_,effector) in effectors { waveValue = effector.signal(waveValue: waveValue, time: time) } semaphore.signal() return waveValue }8. エフェクターを作る
エフェクターを作ります。これが一番楽しい作業でした。
エフェクターは、入力した波形に変化を加えて、出力します。
例えばディレイエフェクターは、入力した波形に、バッファにためた過去の波形を少し音量を下げて加えます。そしてその結果をバッファにためます。このようにすると残響効果が現れて、面白い音が作り出せます。
Synthesizer.swiftclass DelayEffector: Effector { var delayCount = 22_100 lazy var buffer = RingBuffer<Float>(delayCount + 1) var index: Int = 0 func signal(waveValue: Float, time: Float) -> Float { func enqueue(_ value: Float) { if !buffer.enqueue(value) { fatalError("Cannot enqueue buffer.") } } if delayCount > 0 { delayCount -= 1 enqueue(waveValue) return waveValue } if let delayValue = buffer.dequeue() { let ret = waveValue + delayValue*0.4 enqueue(ret) return ret } fatalError("Cannot dequeue buffer.") } }なお、各エフェクターの厳密な言葉の定義は違っているかもしれないのでご了承下さい。(ある程度のイメージは合っていると思います)
9. 画面を作る
最後に画面を作ります。鍵盤を付けるのもいいですが、アナログシンセっぽく使えるといいかなと思い、音程はスライダーにしてみました。
参考資料
Building a Synthesizer in Swift
https://betterprogramming.pub/building-a-synthesizer-in-swift-866cd15b731最後に
iOSを使ったARやML、音声処理などの作品やサンプル、技術情報を発信しています。
作品ができたらTwitterで発信していきますのでフォローをお願いします?
https://twitter.com/jugemjugemjugemQiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。
https://qiita.com/TokyoYoshidaNoteでは、連載記事を書いています。
https://note.com/tokyoyoshidaZennは機械学習が多めです。
https://zenn.dev/tokyoyoshida
- 投稿日:2021-03-25T17:03:01+09:00
Xcodeの「Please reconnect the device.」の解決方法
アプリをビルドしているXcodeが、端末のOSバージョンに対応していないことを表示しています。
下記を参考にどうぞ。
- 投稿日:2021-03-25T17:01:49+09:00
SwiftUIでAdMob対応(バナー編)
要約
UIViewControllerRepresentableを使ってSwiftUIでいい感じにバナーを含んだ画面を表示します。
はじめに
SwiftUIでAdMobのバナーが表示したかったので、実装してみました。
AdMobの導入が済んでいる事が前提となります。
https://developers.google.com/admob/ios/quick-start?hl=ja結果
こんな感じでプレビューでバナーが表示できました。
Repository
概要
SwiftUIでAdMob対応をします。
手始めにバナーを表示します。Life Cycle SwiftUI App対応
リポジトリではプロジェクトはSwiftUI Appでのプロジェクトになります。
AdMobはAppDelegateでGADMobileAds.sharedInstance().start(completionHandler: nil)
の初期化を行う必要があるので、ひと手間必要です。AppDelegateを作成し、プロジェクトのAppに
UIApplicationDelegateAdaptor
を追加して、AppDelegate内で初期化するようにします。@main struct SwiftUIAdMobApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } }class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { GADMobileAds.sharedInstance().start(completionHandler: nil) return true } }UIViewControllerRepresentableで広告表示用UIViewControllerを作成
AdMobのバナーには、広告の処理を行うためのフルスクリーンなrootViewControllerが必要です。
SwiftUI単体ではUIViewControllerを用意することができないため、これをUIViewControllerRepresentable経由で用意します。ContainedAdViewControllerでUIViewControllerRepresentableに適合する処理を書きます。
makeUIViewController
で提供するUIViewControllerはStoryboardで用意しました。struct ContainedAdViewController<Content: View>: UIViewControllerRepresentable { let rootView: Content func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIViewController { let adViewController: AdViewController = UIStoryboard(name: "AdViewController", bundle: nil).instantiateInitialViewController()! adViewController.rootView = AnyView(rootView) return adViewController } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } typealias UIViewControllerType = UIViewController class Coordinator: NSObject { var parent: ContainedAdViewController init(_ containedAdViewController: ContainedAdViewController) { parent = containedAdViewController } } }Storyboardの初期表示はAdViewControllerというUIViewController継承クラスで、その内部ではSwiftUIを表示するUIHostingControllerコンテナとその下で表示を行うバナーを格納しています。
広告バナーに対するrootViewControllerはAdViewControllerになります。SwiftUI側の表示
SwiftUI側はContainedAdViewControllerのrootViewに表示したいViewを実装すればOKです。
struct ContentView: View { var body: some View { ContainedAdViewController(rootView: Text("Hello, world!").padding() ) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }動作
上記のRepositoryのコードはSceneもサポートしているので、複数画面でもばっちりアダプティブ広告が表示できます。
- 投稿日:2021-03-25T17:00:41+09:00
iOSで5Gかどうか判断する(罠付き)
前提
Xcode Version 12.4 (12D4e)で動作確認しています。
概要
iOSでCoreTelephonyを使って、今使っているキャリアが5Gかどうかを判別します。
また、その際の罠について紹介します。結論
iOS 14.1以上で、5Gかどうかの判断をするようにしましょう。
今使っているキャリアを取得
まず、
CTTelephonyNetworkInfo
を生成します。
CTTelephonyNetworkInfoからserviceSubscriberCellularProviders
を使って端末のキャリアを取得します。
keyとともにCTCarrier
を取得できます。let info = CTTelephonyNetworkInfo() info.serviceSubscriberCellularProviders?.forEach({ (key, value) in print(value.carrierName ?? "") })無線テクノロジーを取得
先ほど得たキーで、
serviceCurrentRadioAccessTechnology
より無線のテクノロジーが何であるかを取得可能です。
CoreTelephonyのドキュメントにRadio Access Technology Constants
があるので、switchを使って目的の無線テクノロジーを検出できます。
CTRadioAccessTechnologyNR
,CTRadioAccessTechnologyNRNSA
は5Gになります。
二つの違いはスタンドアローン型か非スタンドアローン型かの違いです。詳しくはWikipediaを貼っておきます。
https://ja.wikipedia.org/wiki/5G_NR#展開方式let info = CTTelephonyNetworkInfo() info.serviceSubscriberCellularProviders?.forEach({ (key, value) in if let radioAccessTechnology = info.serviceCurrentRadioAccessTechnology?[key] { switch radioAccessTechnology { case CTRadioAccessTechnologyLTE: print("これはLTEです") case CTRadioAccessTechnologyNRNSA: print("これは非スタンドアローンモード 5Gです") case CTRadioAccessTechnologyNR: print("スタンドアローンモード 5Gです") default: print(radioAccessTechnology) } } })iOS 13以下への対応
しかしながら、開発中のアプリがiOS13以降にも対応するとなると、このコードだとエラーが起きます。
'CTRadioAccessTechnologyNRNSA' is only available in iOS 14.0 or newer
ということなので、version checkのコードを追加します。let info = CTTelephonyNetworkInfo() info.serviceSubscriberCellularProviders?.forEach({ (key, value) in if let radioAccessTechnology = info.serviceCurrentRadioAccessTechnology?[key] { switch radioAccessTechnology { case CTRadioAccessTechnologyLTE: print("これはLTEです") default: if #available(iOS 14.0, *) { switch radioAccessTechnology { case CTRadioAccessTechnologyNRNSA: print("これは非スタンドアローンモード 5Gです") case CTRadioAccessTechnologyNR: print("スタンドアローンモード 5Gです") default: print(radioAccessTechnology) } } else { print(radioAccessTechnology) } } } })ここから罠
実はこのコード、iOS 14.0系のOSで実行するとクラッシュします。
理由はCTRadioAccessTechnologyNRNSAとCTRadioAccessTechnologyNRの部分で、
実はこの二つの定数はiOS 14.1からのAPIであるとの記載がWebのドキュメントにあります。
しかしながら、Xcodeで定義にジャンプするとこれらのAPIは
@available(iOS 14.0, *)
でマークされています。
このため、対応OSと定義で不整合が起きて、iOS 14.0に本来無いAPIに触ろうとしてクラッシュするわけです。対策
#available
の部分を iOS 14.1に変更しましょう。
これで本来APIが使えるOSだけがAPIに触ることになります。let info = CTTelephonyNetworkInfo() info.serviceSubscriberCellularProviders?.forEach({ (key, value) in if let radioAccessTechnology = info.serviceCurrentRadioAccessTechnology?[key] { switch radioAccessTechnology { case CTRadioAccessTechnologyLTE: print("これはLTEです") default: if #available(iOS 14.1, *) { switch radioAccessTechnology { case CTRadioAccessTechnologyNRNSA: print("これは非スタンドアローンモード 5Gです") case CTRadioAccessTechnologyNR: print("スタンドアローンモード 5Gです") default: print(radioAccessTechnology) } } else { print(radioAccessTechnology) } } } })ちなみに
Xcode Version 12.5 beta 3 (12E5244e)だと、この問題は解決されていて、CTRadioAccessTechnologyNRNSAとCTRadioAccessTechnologyNRは
@available(iOS 14.1, *)
でマークされています。
- 投稿日:2021-03-25T07:38:29+09:00
【Swift】モーダル遷移かつNavigationBarも表示させたいかつフルスクリーンで表示させたい
どういうことか
- 下からヌッと出てくるモーダルで遷移させたい
- そいつにNavigationBarもつけたい
- さらにフルスクリーンで表示させたい
コードで書きます。
実装
たとえばボタンを押したときのアクションなんかに書く。
ViewController.swiftlet storyboard: UIStoryboard = self.storyboard! let vc = storyboard.instantiateViewController(withIdentifier: "nextView") let nav = UINavigationController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen self.present(nav,animated: true)閉じるボタンがないのでNavigationBarとかに作るといいと思う。
おわり(´・ω・`)
- 投稿日:2021-03-25T05:12:32+09:00
Swift メモリリークのつまずきポイント
「メモリリークが発生していることはわかるが、その原因がわからない。」
このような質問を、聞くことが多々あります。
行き詰まる方の助けになれればと思い、
今回、そのポイントをまとめてみました。
知ってる方は「あるある!!」と思っていただければ幸いです。メモリリークって何?
何かが邪魔をして、画面を閉じてもインスタンスが残ったまま消えないという現象。
これでは、メモリが解放されません。つまり、これを繰り返していると どんどん動作が重くなり、最悪の場合 アプリが落ちます。
それだけは避けたいですね。
ということで…
早速、見落としやすいポイントを見てみましょう。メモリリークのパターン・解消
メモリリークの発生箇所は、決まって2つ。
- クロージャー (
closure
)- デリゲート(
delegate
)では、何が原因?
全て 強参照 が原因です。
1. クロージャー (closure)
クロージャー とは、処理(関数)そのものを「値」として 変数に格納できる もの。
引数として使えば 、特定の処理を設定して…呼び出した後に実行したり…ムフフ…
とにかく便利なんです。彼。ただ、そのままにしておくとメモリリークを引き起こします。
ご安心ください。弱参照
[weak self]
で全て解決します。class TestClass { var text = "test" var testClosure: () -> () = { [weak self] in // <- ここ print(self?.text) } }クロージャー を生成した際、クラス内の変数・関数を呼び出していないか(
self
を普通に、または暗黙的に使っていないか)をチェックしてください。もし使っていたら、
[weak self]
を使いましょう。…ああ、あともう一つ。
DispatchQueue
DispatchQueue.main.async
を使う際も注意してください。DispatchQueue.main.async { [weak self] in // <- ココ self?.image = image }
DispatchQueue
もまた、クロージャーみたいなものです。
初見の方は、ここを見落としやすいです。2. デリゲート (delegate)
外側にあるクラスから処理を呼び出したい時に使う、アレですね。
その点は、もうご存知かと思います。こちらも強参照によるもの。
デリゲートを自作する場合、そのままだとメモリリークが発生します。ここでの ポイントは2つ です。
- delegate を格納する場合、弱参照
weak
にする。- delegate の
protocol
は、AnyObject
を継承しておく。class TestClass { weak var delegate: SampleDelegate? ... }protocol SampleDelegate: AnyObject { ... }これだけ。
URLSession
URLSession
を使用する際にも注意。元々、デリゲート自体が強参照されています。
流石に、ここを弱参照にすることはできませんね。ここでは、対応策が2つ ほどあります。
URLSession.shared
を使う(こちらを使っているなら、そもそも発生しない。)invalidateAndCancel()
等の 専用メソッド で、セッションを明示的に無効化する。前者は
URLSession.shared
が使える場合ですね。
細かい設定は使えませんが、大抵はこちらで良いかと。後者の専用メソッドについては、こちらの記事が参考になります。
[weak self]
は必須ではない。要は、「違った書き方もある」という意味ですね。
[weak self]
が必須ではないだけ です。代わりになるような 処理は必ず必要 です。その他、参考になりそうな記事
【メモリリークの仕組み】
本記事は例の紹介です。
極力、別の話題は省いています。そのため、強参照が原因である理由は説明していません。(循環参照)
メモリリークの仕組みに関しては、以下の記事が参考になります。ARC(Automatic Reference Counting) と呼ばれる機能を、swiftは有しています。
今回の問題も、全てはこれに基づいていますね。【メモリリークが発生しているか、チェックしたい】
また、
メモリリークが発生しているか、チェックしたい 場合はこちら。
- iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。 //qiita
- [iOS] メモリリークをXcodeでチェックして、リークしないようにしたい! //DevelopersIO
ということでね
メモリリークのパターン(例)をまとめました!!
(パターン紹介以外はほぼ記事のリンク紹介ですが)原因に関する詳しい事柄は、多くの方々が記事にまとめてくれていると思います。
そのため、本記事は「メモリリークから原因につなげるための記事」として書きました。原因さえわかれば、それらの記事にたどり着けると思います。
思い当たる内容はあげましたが、その他にもある場合は教えていただけると助かります!