LoginSignup
5
4

More than 1 year has passed since last update.

SwiftUIのハマりポイント集

Last updated at Posted at 2022-05-26

前段

上記リンクはUnity as a Libraryを使っていた時のハマりポイントだったが、Unity関係なくてもSwiftUIだけでもハマるポイントがあった。
今回のこちらは純粋にSwiftUIだけの内容にしてある。

Q. 「Cannot find type 'className' in scope」 と表示されてコンパイルできなくなる

何かの拍子で、「Cannot find type 'className' in scope」とエラー表示されて、クラスを見つけてくれなくなる。
ちゃんとプロジェクトには該当のクラスのファイルがあるのに、だ。

いくらBridgingファイルを消そうが、Cleanビルドしようが、DeriveDataを消そうが、一向に解決されない。

A. 該当ファイルを一時的にDelete(Remove Reference)して、再度付け直す

ファイルを「Delete」→「Remove Reference」して(Move Trashしない!)、いったんプロジェクトから切り離す。

スクリーンショット 2022-05-26 21.12.22.png

そして再度プロジェクトに加えてあげると、何事もなかったようにビルドしてくれるようになる。

Q. onTapGestureでモーダルビューが出る1回目の瞬間は値が渡らない

よくあるパターンだと思うが、以下のようにList形式からセルをタップすると、モーダルビューが表示されるケース。

SongListView.swift
struct PlayingView: View {
    let selectedItem: MPMediaItem?
    
    var body: some View {
        Text("\(selectedItem?.title ?? "*nil*")")
    }
}

struct SongListView: View {
    let playlist: MPMediaPlaylist
    @State private var isShow: Bool = false
    @State var selectedMediaItem : MPMediaItem?

    var body: some View {
        NavigationView {
            List {
                ForEach(self.playlist.items, id: \.persistentID) { mediaItem in
                    SongCell(mediaItem)
                        .onTapGesture {
                            self.selectedMediaItem = mediaItem
                            self.isShow = true
                        }
                }
            }
        }
        .sheet(isPresented: self.$isShow, content:  {
            if let item = selectedMediaItem {
                PlayingView(selectedItem:item)
            }
        })
    }
}

曲リストからセルをタップすれば、選んだ曲が表示される、というごく当たり前の処理が、実はできない。

モーダルビューが表示されるタイミングの

SongList.swift
        .sheet(isPresented: self.$isShow, content:  {
            if let item = selectedMediaItem {
                PlayingView(selectedItem:item)
            }
        })

ここで、最初の一回だけがなぜかselectedMediaItemがnil になってしまう。(2回目以降はちゃんと入っている)
self.isShowが「true」だからこの部分が呼ばれたはずが、なぜか selectedMediaItem が「nil」になっている。意味がわからない。
さらに言えば、Breakpointを仕掛けてself.isShowを見てみたら「false」になっている。なんでやねん。

A. 値渡しではなく、参照渡しにする

海外に同じ悩みを抱えている同士がいた。
要は、パラメーター受け渡す側を

SongList.swift
PlayingView(selectedItem:item)

とやるのではなく、

SongList.swift
PlayingView(selectedItem:self.$selectedMediaItem)

と参照渡しにする、ということだった。それにともなって、受け渡される側のPlayingViewの方も

SongList.swift
struct PlayingView: View {
    let selectedItem: MPMediaItem?
      :

から

SongList.swift
struct PlayingView: View {
    @Binding var selectedItem: MPMediaItem?
      :

という感じになる。
最終的に全体は以下のようになる。

SongList.swift
struct PlayingView: View {
    @Binding var selectedItem: MPMediaItem?
        
    var body: some View {
        Text("\(selectedItem?.title ?? "*nil*")")
    }
}

struct SongListView: View {
    let playlist: MPMediaPlaylist
    @State private var isShow: Bool = false
    @State var selectedMediaItem : MPMediaItem?

    var body: some View {
        NavigationView {
            List {
                ForEach(self.playlist.items, id: \.persistentID) { mediaItem in
                    SongCell(mediaItem)
                        .onTapGesture {
                            self.selectedMediaItem = mediaItem
                            self.isShow = true
                        }
                }
            }
        }
        .sheet(isPresented: self.$isShow, content:  {
            PlayingView(selectedItem:self.$selectedMediaItem)
        })
    }
}

限りなくバグに近いが、直される気配がないのでこれでいくしかないようだ。

Q. 値の変更で通知してくれるというBindingがうまく通知してくれない

親のViewで定義した変数を、子側で変更することはよくある。
そういう場合は親View側で

ParentView.swift
    @State private var isShow: Bool

と@ State定義しておいて、

ParentView.swift
    ChildView(isShow: $isShow)

と「$」を前に付けて参照として、子View側に受け渡す。
子側は、

ChildView.swift
    @Binding var isShow: Bool

と用意してやれば、子側の変更がされた場合、親のViewの再描画が走ってくれる。

はずなのだが、配列丸ごと同じことを以下のようにしても、うまくビルドできない。
(ビルドできても、通知してくれない)

ChildView.swift
    @Binding var parameters:[Int]

A. 配列の場合は中身自体をBindingしないとうまく通知してくれない

これに関しては「それはその通り」で、よくわかっていないのに使っているのが悪い
てっきり配列をBindingすれば全体が通知対象になると勘違いしていた。

Q. AppStorage がうまく保存してくれない

SettingView.swift
struct SettingView: View {
    @AppStorage("Debug:RecordMotion") var isRecording = false
      :

と設定画面で設定させて、

AccelSensor.swift
class AccelSensor : ObservableObject {
    @AppStorage("Debug:RecordMotion") var isRecording = false

    init() {
        if isRecording {
            :
        }
    }

と実装部分で使おうかと思ったのだが、どうもうまく保存してくれない。

A. AppStorage はView以外だと正常に入らないことがある

上記の例だと、設定画面はViewだが、利用部分はclassでありViewではない。
そういう場合正常に最新の値が入らないようだ。

なので、普通にUserDefaultsを利用することにする。
先程の例だと、以下のようになる。

AccelSensor.swift
class AccelSensor : ObservableObject {
    var isRecording = false

    init() {
        isRecording = UserDefaults.standard.bool(forKey: "Debug:RecordMotion")
        if isRecording {
            :
        }
    }

(以下2022/9/8 追記)

Q. @ Stateで渡しているのだがうまく通知してくれない(最初は通知しなくて、2回目から通知するとか)

先程のモーダルビューのケース

SongListView.swift
struct SongListView: View {
       :
    @State var selectedMediaItem : MPMediaItem?
       :
         self.selectedMediaItem = mediaItem
       :
        .sheet(isPresented: self.$isShow, content:  {
            if let item = selectedMediaItem {
            }
        })
    }
}

という時はうまく行っていたのだが、MPMediaItemを別の自分のオリジナルクラスに当てはめてやったらうまくいかなくなった。
最初だけ通知してくれなく、2回目から通知してくれるという謎挙動になってしまった。

A. @ Stateはクラスには適応できない。

以下のブログによると、

プロパティにデータクラス(classのインスタンス)は使えません。この場合は、後述する@ObservedObjectか@EnvironmentObjectを使用します。参照型であるclassは内部のプロパティが変化してもインスタンス自体が変化しない為、Viewの再描画は発生しません。

ということで、データクラスには使えない。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4