Search code examples
swiftswiftuiobservedobject

How does $ work in SwiftUI and why can I cast a @StateObject to an @ObservedObject


I'm a newbie developing an app which basically has a fridge class, which contains food. I'm trying to have one single fridge with a list of food. So I set foodList to be a published object and instantiate a @StateObject fridge when the program starts. Basically, from my perspective, whenever the foodList changes, the fridge object will save its state and give me a new view.

In addition, I want to pass this @StateObject fridge to another view so that my another view could modify my foodList in fridge

Below is my code

struct ContentView: View {
    @StateObject private var fridge=Fridge();
    private var dbStartWith=0;
    
    @State private var selection = 1;
    @State private var addFood = false;
    
    var body: some View {
        TabView(selection: $selection) {
            NavigationView {
                List(fridge.container!){
                    food in NavigationLink(destination: FoodView()) {
                        Text("HI")
                    }
                }.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
                .navigationBarItems(trailing:
                                        NavigationLink(destination: AddFoodView(fridgeView: fridge)) {
                                            Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
                                        } )
            }
            .tabItem {
                Image(systemName: "house.fill")
                Text("Home")
            }
            .tag(1)
            
            
            Text("random tab")
                .font(.system(size: 30, weight: .bold, design: .rounded))
                .tabItem {
                    Image(systemName: "bookmark.circle.fill")
                    Text("profile")
                }
                .tag(0)
        }
        
    }
}
    

struct FoodView: View{
    var body: some View{
        NavigationView{
            Text("food destination view ");
        }
    }
}

struct AddFoodView: View{
    @ObservedObject var fridgeView: Fridge;
    @State private var name = ""
    @State private var count : String = "1"
    @State private var category : String = "肉类";
    @State var showCategory = false
    @State var showCount = false
    

    var body: some View{
        ZStack{
            NavigationView{
                Form{
                    HStack{
                        Text("食品")
                        TextField("请输入材料名", text: $name);
                    }.padding(.bottom,10).padding(.top,10).padding(.leading,10).padding(.trailing,10)
                    HStack{
                        Text("数量")
                        TextField("请选择数量",text:$count,onEditingChanged:{(changed) in
                            self.hideKeyboard();
                            self.showCount=changed;
                        }){}
                    }.padding(.bottom,10).padding(.top,10).padding(.leading,10).padding(.trailing,10)
                    HStack{
                        Text("种类")
                        TextField("请选择种类",text: $category,onEditingChanged:{(changed) in
                            self.hideKeyboard();
                            self.showCategory=changed;
                        }){}
                    }.padding(.bottom,10).padding(.top,10).padding(.leading,10).padding(.trailing,10)
                }
            }.navigationBarItems(trailing: NavigationLink(destination: FoodView()){
                Text("保存").foregroundColor(Color.blue).font(.system(size: 18,design: .default))
            }).simultaneousGesture(TapGesture().onEnded{
                var tempFood=Food(id: fridgeView);//not completed yet
            })
            ZStack{
                    if self.showCount{
                        Rectangle().fill(Color.gray)
                            .opacity(0.5)
                        VStack(){
                            Spacer(minLength: 0);
                            HStack{
                                Spacer()
                                Button(action: {
                                    self.showCount=false;
                                }, label: {
                                    Text("Done")
                                }).frame(alignment: .trailing).offset(x:-15,y:15)
                            }
                            Picker(selection: $count,label: EmptyView()) {
                                ForEach(1..<100){ number in
                                    Text("\(number)").tag("\(number)")
                                }
                            }.labelsHidden()
                        }            .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
                        .background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
                        .overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
                        .offset(x:10,y:-10)
                        Spacer()
                    }
                    if self.showCategory{
                        let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
                        ZStack{
                            Rectangle().fill(Color.gray)
                                .opacity(0.5)
                            VStack(){
                                Spacer(minLength: 0);
                                HStack{
                                    Spacer()
                                    Button(action: {
                                        self.showCategory=false;
                                    }, label: {
                                        Text("Done")
                                    }).frame(alignment: .trailing).offset(x:-15,y:15)
                                }
                                Picker(selection: $category,label: EmptyView()) {
                                    ForEach(0..<categoryArr.count){ number in
                                        Text(categoryArr[number]).tag(categoryArr[number])
                                    }
                                }.labelsHidden()
                            }            .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
                            .background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
                            .overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
                            Spacer()
                        }.offset(x:10,y:20)
                    }
            }
        }.animation(.easeInOut)
    }
        
    
}

Basically I want to pass the @StateObject private var fridge to the struct AddFoodView: View so that my AddFoodView can use the variable. From what I learned online, I think I need to inject @ObservedObjects like this NavigationLink(destination: AddFoodView(fridgeView: fridge)). This doesn't give me error ( at least in debugger phase) which confuses me. Isn't fridge a Food variable and fridgeView a @ObservedObject Food variable ?

If I do something like this AddFoodView(fridgeView: $fridge)). There is an error saying Cannot convert value '$fridge' of type 'ObservedObject<Fridge>.Wrapper' to expected type 'Fridge', use wrapped value instead


Solution

  • @StateObject and @ObservedObject are sources of truth, they are very similar to each other and almost interchangeable.

    To share these with other Views you should pass it to the AddFoodView using .environmentObject(fridge) and change the @ObservedObject var fridgeView: Fridge; in AddFoodView to @EnvironmentObject var fridgeView: Fridge.

    Look up the Apple Documentation on "Managing Model Data in Your App".

    struct BookReader: App {
        @StateObject var library = Library()
    
        var body: some Scene {
            WindowGroup {
            LibraryView()
                    .environmentObject(library)
            }
        }
    }
    
    struct LibraryView: View {
        @EnvironmentObject var library: Library
    
        // ...
    }
    

    Another good source are the Apple SwiftUI Tutorials, especially "Handling User Input"