Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【SwiftUI】"Fatal error: No ObservableObject of type ~ found. A View.environmentObject(_:) for ~ may be missing as an ancestor of this view."の解決方法

 

はじめに

表題のエラーに関して、発生時の状況と解決方法を記事に残します。

 

開発環境

 

本題

エラー発生時の状況

sheetでEnvironmentObjectのデータをListに表示した状態で、行を編集しようとするとアプリがクラッシュし、表題のエラーが発生。

文章だと伝わりにくいため、以下表題のエラーが発生する雑なサンプルを作りました笑

 

SceneDelegate

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {        
        let contentView = ContentView()
            .environmentObject(Region())
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }    
}

 

ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var isModalActive = false
    
    var body: some View {
        Button(action: {
            self.isModalActive.toggle()
        }) {
            Text("モーダルビューを表示する")
        }
        .sheet(isPresented: $isModalActive) {
            ModalView()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

ModalView.swift

import SwiftUI

struct ModalView: View {
    @EnvironmentObject private var region: Region
    
    var body: some View {
        NavigationView {
            List {
                ForEach(region.names, id: \.self) { region in
                    Text(region)
                }
                .onDelete { indexSet in
                    region.names.remove(atOffsets: indexSet)
                }
                .onMove { indexSet, toOffset in
                    region.names.move(fromOffsets: indexSet, toOffset: toOffset)
                }
            }
            .listStyle(InsetGroupedListStyle())
            .navigationTitle("地方")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar { EditButton() }
        }
    }
}

struct ModalView_Previews: PreviewProvider {
    static var previews: some View {
        ModalView()
    }
}

 

Region.swift

import Foundation

class Region: ObservableObject {
    @Published var names = ["北海道", "東北", "関東", "中部", "近畿", "中国", "四国", "九州"]
}

 

エラーの解決方法

ContentView.swiftに実装されたsheetの実装を以下に変更する。

変更前

.sheet(isPresented: $isModalActive) {
    ModalView()
}

変更後

.sheet(isPresented: $isModalActive) {
    ModalView()
        .environmentObject(Region())
}

 

まず、前提としてEnvironmentObjectは、親ビューまたは祖先のビューによって提供される監視可能なオブジェクトのプロパティラッパータイプです。

※参考:https://developer.apple.com/documentation/swiftui/environmentobject

 

今回のサンプルでは、ContentViewを親ビューと想定してEnvironmentObjectを利用していました。しかし、以下の記事を参考にすると、sheetはContentViewの子ビューにならない新たな階層になるとのことでした。確かに言われればそうですね・・・。

※参考:

ios - SwiftUI -> Thread 1: Fatal error: No observable object of type MyObject.Type found (EnvironmentObject in sheet) - Stack Overflow

 

おわりに

基本的な実装だけでなく、今回のようなエラーの解決方法もなるべく記事に残していこうと思います。

 

参考