SwiftUI - Issues with Setting Data in EnvironmentObject for Preview

165 views Asked by At

I have this overlay view and when a button is pressed it's supposed to refresh the view by calling a function in the view model. The code does work on the app itself but doesn't work within the preview and I'm guessing it's because the environment variables don't contain any data as they're below the level where data is entered. In the app, the data comes from the FeedView and is passed to the children.

The view structure is FeedView -> ReaderView -> ReaderOverlay. When the button is pressed on the preview for FeedView it works properly but it doesn't work properly in ReaderView or ReaderOverlay.

Here's the code for ReaderView:

struct ReaderView: View {

@State var chapter:  ChapterInfo

@StateObject private var reader = ReaderViewModel()
@EnvironmentObject var mangaFeed: FeedViewModel
@EnvironmentObject var ch: ChapterIndex

@State private var isTapped = false
@State private var selected: Int = 0 //used in tabview
@State var currentPage = 1
@State var totalPages = 0


var body: some View {
    
    TabView(selection: $selected){
            ForEach(Array(reader.pages.enumerated()), id: \.element) { index, element in
                Page(page: element)
                    .tag(index)
                    .onChange(of: selected){ val in
                        currentPage = val + 1 //tracks changes in selected tab to display page numbers
                    }
                    
            }
        }
        .tabViewStyle(.page(indexDisplayMode: PageTabViewStyle.IndexDisplayMode.never))
        .onTapGesture(){
            isTapped.toggle() // when this is tapped the overlay for control will be toggled
        }
        .overlay(alignment: .top){
            if isTapped{
                readerOverlay(chapter: chapter, currentPage: $currentPage, totalPages: reader.pages.count)
                    .environmentObject(reader)
                
            }
        }
        .task{
            await reader.populate(chapterID: chapter.id)
        }
    
}

    
}

And the code for ReaderOverlay with its preview is as follows:

struct readerOverlay: View {


@State var nextChapter: ChapterInfo?

@State var chapter: ChapterInfo
@EnvironmentObject var mangaFeed: FeedViewModel
@EnvironmentObject var reader: ReaderViewModel
@EnvironmentObject var ch: ChapterIndex



@Binding var currentPage: Int
@State var totalPages: Int

@Environment(\.dismiss) var dismiss

var body: some View {
    
    
    VStack{
        
        //the proxy inherits the size of the parent view which in this case is just the screen
        GeometryReader{ proxy in
            RoundedRectangle(cornerRadius: 15)
                .overlay(
                    HStack{
                        VStack(alignment: .leading){
                            
                            //Displays name of the manga
                            Text(chapter.relationships.first{
                                $0.type == "manga"
                            }?.attributes?.title["en"] ?? "")
                            .font(.headline)
                            .fontWeight(.medium)
                            .foregroundColor(.white)
                                .lineLimit(1)
                                
                            //Displays name of the chapter
                            Text("Ch \(chapter.attributes.chapter ?? "") - \(chapter.attributes.title ?? "")" ).font(.caption2)
                                .fontWeight(.light)
                                .foregroundColor(.white)
                                .lineLimit(1)
                            
                        }
                        
                        Spacer()
                        
                        //Dismisses panel window
                        Button(action: {
                            dismiss()
                        }){
                            Image(systemName: "xmark")
                                .foregroundColor(.red)
                        }
                        
                        
                       
                    }.frame(width: proxy.size.width - 100)
                        
                        
                        
                ).padding(.horizontal, 30.0)
                .frame(width: proxy.size.width , height: 60)
                .foregroundColor(.black)
                
        }
        
        HStack{
            Text("\(currentPage) / \(totalPages)")
            
            //The button that refreshes the reader view
            Button(action: {
                
                nextChapter = mangaFeed.items[ch.chIndex + 1]
                ch.chIndex += 1
                Task {await reader.populate(chapterID: nextChapter!.id)}
                
            }){
                Text("next chapter")
            }
            
        }
        
    }
    
    
}
}

struct readerOverlay_Previews: PreviewProvider {

@State static var currPage = 0
static var previews: some View {
    
    let dummy =  ChapterInfo(id: "5df4596c-febd-492e-bf0d-d98f59fd3f2b", type: "Chapter", attributes: chInfo_Attributes(volume: "1", chapter: "1", title: "Test", publishAt: "2020-05-23", externalUrl: "" ), relationships: [chapter_Relationships(id: "s", type: "manga", attributes: attributes(title: ["en":"20TH Century Boys"]))])
    
    
    readerOverlay(chapter: dummy, currentPage: $currPage, totalPages: 10)
        .environmentObject(FeedViewModel())
        .environmentObject(ChapterIndex())
        .environmentObject(ReaderViewModel())
    
}
}

I was following another post on here that suggested to put environment objects under the preview but that doesn't work.

2

There are 2 answers

0
Yoast On

Okay, so the solution was to manually set the environment object fully in the provider. Very simple solution and I just wasn't thinking clearly but I understand it now.

ReaderView Preview Code:

struct Reader_Previews: PreviewProvider {
    
    
    static var previews: some View {
        
        
        let dummy =  ChapterInfo(id: "5df4596c-febd-492e-bf0d-d98f59fd3f2b", type: "chapter", attributes: chInfo_Attributes(volume: "1", chapter: "1", title: "Friend", publishAt: "2020-05-23", externalUrl: "" ), relationships: [chapter_Relationships(id: "s", type: "manga", attributes: attributes(title: ["en":"20th Century Boys"]))])
                
        let mangaID = "ad06790a-01e3-400c-a449-0ec152d6756a"
        
        //providing the preview with mock environment objects for FVM and CI class
        ReaderView(chapterID: dummy.id)
            .environmentObject({ () -> FeedViewModel in
                let envObj = FeedViewModel()
                Task{await envObj.populate(mangaID:mangaID)}
                return envObj
            }() )
            .environmentObject({ () -> ChapterIndex in
                let envObj = ChapterIndex()
                envObj.chIndex = 0
                return envObj
            }() )
        
    }
}

and ReaderOverlay

struct readerOverlay_Previews: PreviewProvider {
    
    @State static var currPage = 0
    @State static var selected = 0
    
    
    
    static var previews: some View {
        
      
        let dummy =  ChapterInfo(id: "5df4596c-febd-492e-bf0d-d98f59fd3f2b", type: "chapter", attributes: chInfo_Attributes(volume: "1", chapter: "1", title: "Friend", publishAt: "2020-05-23", externalUrl: "" ), relationships: [chapter_Relationships(id: "s", type: "manga", attributes: attributes(title: ["en":"20th Century Boys"]))])
        
        let mangaID = "ad06790a-01e3-400c-a449-0ec152d6756a"
        
        //providing the preview provider mock environment objects
        readerOverlay(currentPage: $currPage, selected: $selected, totalPages: 10)
            .environmentObject({ () -> FeedViewModel in
                let envObj = FeedViewModel()
                Task{await envObj.populate(mangaID:mangaID)}
                return envObj
            }() )
            .environmentObject({ () -> ReaderViewModel in
                let envObj = ReaderViewModel()
                Task{await envObj.populate(chapterID: dummy.id)}
                return envObj
            }() )
            .environmentObject({ () -> ChapterIndex in
                let envObj = ChapterIndex()
                envObj.chIndex = 0
                return envObj
            }() )
        
    }
}
2
malhal On

You've got too many store classes there should just be one. And usually there are 2 singletons of it, one for the app and another for previews filled with test data, e.g.

struct Reader_Previews: PreviewProvider {
    
    
    static var previews: some View {
        ReaderView()
        .environmentObject(ReaderStore.preview)
    }
}

class ReaderStore: ObservableObject {
    static shared = ReaderStore()
    static preview = ReaderStore(preview: true)
...

ReaderStore should load and save your book model data. Your view data should be in View structs, not in view model objects, that won't fly with SwiftUI.

Also I noticed you are using the ForEach View like it's a for loop on an array, that will crash. Best read the docs on how to use that View.