20210325のSwiftに関する記事は7件です。

【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.swift
import Firebase

を追記した。

4.FirebaseApp 共有インスタンスを構成します。通常はアプリの application:didFinishLaunchingWithOptions: メソッドで行います。

AppDelegate.swiftの中で

AppDelegate.swift
class 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という項目生成される。
image.png

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つずつ入力する。※ここが公式の説明だとよく分からなかった。

image.png

DWARF with dSYM Fileの設定

公式の同じページには記載がなかったが、調べるとクラッシュログを上げるためにはこの設定も必要かもしれなさそうだったので、こちらも行った。
Build Settingsの中のDebug Information Formatの中をこのようにDebug, Release共にDWARF with dSYM Fileにする。

image.png

ビルド時のエラー

ここまでの手順を行っても、このようなエラーが出た。

for architecture arm64 linking in object file built for iOS

解決策を以下に示す:
Excluded ArchitecturesのDebug、Releaseそれぞれにarm64を追加する

-> こちらを行うと実機ビルドでarchitecture周りのエラーが出たため、削除した。
削除すると実機ビルドもシミュレータービルドもうまくいったため、不要かもしれない。

image.png

上記のarm64を消した後のSchemaの状態は以下の通り。
image.png
この状態でシミュレーターと実機でのクラッシュ両方のデータが上がっていることが確認できた。

その他のエラー

自分の場合は、iOSのプロジェクトファイルを入れているディレクトリまでのパスの中にGoogle Driveというようにスペースが入ってしまっている箇所があったため、そこでもNo such dir〜という旨のエラーが出た。
解決策としては単にディレクトリの名前からスペースを削除すれば良い。

以上で設定ができました。

クラッシュさせる方法

任意の場所で以下のようにエラーを起こしてクラッシュさせる
.swift
fatalError()

ここで注意点としては、シミュレーターでアプリを起動させている状態だとクラッシュログが上がらないらしく、
シミュレータ上で一度アプリをスワイプして消して、再度アプリアイコンを押して単独でアプリを起動させ、クラッシュさせることでログが上がった。

以上でクラッシュのレポートが取得できるはず。
少しでも誰かの助けになればと思います。


Twitterの方でも仲良くしてくれる人が増えると嬉しいです!
My Twitter Account

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Core Audioでシンセサイザーを作る

iOSのCore Audioを使って、波形を生成するシンセサイザーを作ってみました。あわせてエフェクターもいくつか実装しています。

その作り方を解説します。

できたもの

音のサンプルをSoundCloudにアップしています。アナログシンセのような感じが出ていると思います。

画面はこんな感じです。鍵盤ではなく、スライダーで音程を調整します。

ソースコードはこちらです。複数のサンプルがありますが、今回解説するのは「Synthesizer」になります。
https://github.com/TokyoYoshida/CoreAudioExamples

作り方の概要

Core Audioの構成要素であるAVAudioEngineを使います。波形を生成するレンダー関数を作り、これをソースノードとしてミキサーノードにつなぎ、出力ノードから出力することで、任意の波形を音声出力することができます。

<処理の流れ>
image.png

作り方

本稿は「Synthesizer」の解説記事ですが、波形の作り方は「AudioEngeneGenerateWave」の方がシンプルで分かりやすいので、まずはこれをベースに解説していきます。

1. AVAudioEngineを生成する

AVAudioEngineを生成して各種の情報を取得します。

AudioEngeneWaveGenerator.swift
class 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で抽象化し、それぞれの部品をクラスにすることで実現できます。
今回は次のような構成で作ってみました。

<波形生成関連クラスの関係図>
image.png

protocol AudioSourceは、音程(tone)と、時間を与えるとその時点の波形を出力するメソッド(signal)を持ちます。

Synthesizer.swift
protocol 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.swift
class SinOscillator: ToneAdjuster { // ToneAdjusterはupdateToneの実体を定義するためのもので、それ以外はAudioSourceプロトコルと同じ
    override func signal(time: Float) -> Float {
        updateTone()
        return sin(currentTone * 2.0 * Float(Double.pi) * time)
    }

}

7. ミキサーを作る

ミキサーは、AudioSouceを継承しつつ、エフェクターを加えることができるプロトコルとして宣言します。

Synthesizer.swift
protocol Mixer: AudioSource {
    func addEffector(index: Int, effector: Effector)
    func removeEffector(at index: Int)
}

ミキサーの実装であるAudioMixerは、オシレータを1つ、エフェクターを複数保持しています。

Synthesizer.swift
class 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.swift
class 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で発信していきますのでフォローをお願いします?

Twitter
https://twitter.com/jugemjugemjugem

Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。
https://qiita.com/TokyoYoshida

Noteでは、連載記事を書いています。
https://note.com/tokyoyoshida

Zennは機械学習が多めです。
https://zenn.dev/tokyoyoshida

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeの「Please reconnect the device.」の解決方法

アプリをビルドしているXcodeが、端末のOSバージョンに対応していないことを表示しています。

下記を参考にどうぞ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでAdMob対応(バナー編)

要約

UIViewControllerRepresentableを使ってSwiftUIでいい感じにバナーを含んだ画面を表示します。

はじめに

SwiftUIでAdMobのバナーが表示したかったので、実装してみました。
AdMobの導入が済んでいる事が前提となります。
https://developers.google.com/admob/ios/quick-start?hl=ja

結果

こんな感じでプレビューでバナーが表示できました。

スクリーンショット 2021-03-25 16.35.32.png

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になります。

スクリーンショット 2021-03-25 16.53.26.png

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

動作

表示できました。
Simulator Screen Shot - iPod touch (7th generation) - 2021-03-25 at 16.56.10.png

上記のRepositoryのコードはSceneもサポートしているので、複数画面でもばっちりアダプティブ広告が表示できます。
Simulator Screen Shot - iPad Pro (12.9-inch) (4th generation) - 2021-03-25 at 16.40.12.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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以降にも対応するとなると、このコードだとエラーが起きます。

スクリーンショット 2021-03-25 15.10.00.png

'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, *) でマークされています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】モーダル遷移かつNavigationBarも表示させたいかつフルスクリーンで表示させたい

どういうことか

  • 下からヌッと出てくるモーダルで遷移させたい
  • そいつにNavigationBarもつけたい
  • さらにフルスクリーンで表示させたい

コードで書きます。

実装

たとえばボタンを押したときのアクションなんかに書く。

ViewController.swift
let storyboard: UIStoryboard = self.storyboard!
let vc = storyboard.instantiateViewController(withIdentifier: "nextView")

let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen

self.present(nav,animated: true)

閉じるボタンがないのでNavigationBarとかに作るといいと思う。
おわり(´・ω・`)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift メモリリークのつまずきポイント

メモリリークが発生していることはわかるが、その原因がわからない。

このような質問を、聞くことが多々あります。

行き詰まる方の助けになれればと思い、
今回、そのポイントをまとめてみました。

知ってる方は「あるある!!」と思っていただければ幸いです。

メモリリークって何?

tumatta.png
何かが邪魔をして、画面を閉じてもインスタンスが残ったまま消えないという現象。
これでは、メモリが解放されません。

つまり、これを繰り返していると どんどん動作が重くなり、最悪の場合 アプリが落ちます

memoryLeak.png
アプリが落ちる…

それだけは避けたいですね。
ということで…
早速、見落としやすいポイントを見てみましょう。

メモリリークのパターン・解消

メモリリークの発生箇所は、決まって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は有しています。
今回の問題も、全てはこれに基づいていますね。

【メモリリークが発生しているか、チェックしたい】

cheakMemory.png

また、
メモリリークが発生しているか、チェックしたい 場合はこちら。

ということでね

メモリリークのパターン(例)をまとめました!!(パターン紹介以外はほぼ記事のリンク紹介ですが)

原因に関する詳しい事柄は、多くの方々が記事にまとめてくれていると思います。
そのため、本記事は「メモリリークから原因につなげるための記事」として書きました。

原因さえわかれば、それらの記事にたどり着けると思います。

思い当たる内容はあげましたが、その他にもある場合は教えていただけると助かります!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む