Changes to my model/viewmodel are not charging what's on my view

70 Views Asked by At

I'm making a simple news app with swiftUI. the problem is that I want to add a view model to my project but now when I run the app nothing is shown. When I first added the viewmodel everything was perfect, but now the viewmodel can't see the changes I make or it doesn't communicate them. I tried everything that I could think of, so I need some help.

What shows when I run the app

App Entry

@main
struct GameNewsApp: App {
    
    @State var newsModel = NewsViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(newsModel)
        }
    }
}

Model

struct ArticleSearch: Decodable {
        var results = [Articles]()
}
struct Articles: Decodable, Identifiable {
    let id = UUID()
    var publish_date: String?
    var authors: String?
    var title: String?
    var image: Images?
    var deck: String?
    var body: String?
    var categories: [Category]
}

struct Images: Decodable {
    var square_tiny: String?
    var screen_tiny: String?
    var square_small: String?
    var original: String?
}

struct Category: Decodable, Identifiable {
    var id: Int?
    var name: String?
}

Views

struct ContentView: View {
    var body: some View {
        
        TabView {
            NewsView()
                .tabItem {
                    Label("Game News", systemImage: "gamecontroller")
                }
            
            VideoView()
                .tabItem {
                    Label("Game Videos", systemImage: "airplayvideo")
                }
        }
        .onAppear {
            UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance()
        }
        .preferredColorScheme(.dark)
    }
}

#Preview {
    ContentView()
}
struct NewsView: View {
    
    @Environment (NewsViewModel.self) var newsModel
    
    var body: some View {
        NavigationStack {
            ScrollView(showsIndicators: false) {
                VStack {
                    ForEach(newsModel.articles) { a in
                        NavigationLink {
                            DetailNews()
                        } label: {
                            NewsCard()
                        }
                        .onTapGesture {
                            newsModel.selectedNews = a
                        }
                    }
                }
            }
            .navigationTitle("Game News")
        }
        .onAppear {
            newsModel.getNewsData()
        }
        .refreshable {
            newsModel.getNewsData()
        }
    }
}

#Preview {
    NewsView()
}
import SwiftUI
import CachedAsyncImage

struct NewsCard: View {
    
    @Environment(NewsViewModel.self) var newsModel
    
    var body: some View {
        
        let articles = newsModel.selectedNews
        
        VStack(alignment: .leading, spacing: 0) {
            CachedAsyncImage(url: URL(string: articles?.image?.original ?? "")) { image in
                switch image {
                case .empty:
                    HStack {
                        Spacer()
                        ProgressView()
                        Spacer()
                    }
                case .success(let image):
                    image
                        .resizable()
                        .clipShape(.rect(topLeadingRadius: 10, topTrailingRadius: 10))
                        .frame(height: 150)
                        .padding(.bottom, 10)
                        .overlay {
                            LinearGradient(stops: [
                                Gradient.Stop(color: .clear, location: 0.6),
                                Gradient.Stop(color: .black, location: 1)
                            ], startPoint: .top, endPoint: .bottom)
                        }
                case .failure:
                    HStack {
                        Spacer()
                        Image(systemName: "photo")
                            .imageScale(.large)
                        Spacer()
                    }
                @unknown default:
                    fatalError()
                }
            }
            .frame(maxHeight: 150)
            .background(Color.gray.opacity(0.3))
            .clipped()
            
            
                Spacer()

                Text(articles?.title ?? "")
                    .font(.title3)
                    .fontWeight(.bold)
                    .lineLimit(3)
                    .padding(.bottom, 10)
                    .padding(.horizontal)
                    .multilineTextAlignment(.leading)
            
                Text(articles?.deck ?? "")
                    .font(.subheadline)
                    .lineLimit(4)
                    .padding(.bottom, 10)
                    .padding(.horizontal)
                    .multilineTextAlignment(.leading)
            
            Spacer()
            
            HStack {
                ForEach(articles?.categories ?? []) { category in
                    Text(category.name ?? "")
                }
            }
            .padding(.horizontal)
            .padding(.bottom, 5)
            
                Spacer()
            
            }
            .frame(height: 350)
            .overlay {
                RoundedRectangle(cornerRadius: 10)
                    .stroke(.white)
            }
            .padding(.all, 10)
            .foregroundStyle(.white)
    }
}


#Preview {
    NewsCard()
}

Viewmodel

@Observable
class NewsViewModel {
    
    var articles = [Articles]()
    var dataService = DataService()
    var selectedNews: Articles?
    
    func getNewsData() {
        Task {
            articles = await dataService.articleSearch()
        }
    }
}

When changes happen I want to show them in the view of the app.

1

There are 1 best solutions below

7
malhal On

The structure is not quite right, async funcs must return something and it's .task not Task{} in SwiftUI for the correct lifetime, try something like this:

struct DataService {
    // background thread
    func articleSearch() async -> [News] { // usually this would also be throws
        return ...
    }

    func articleContent(for articleID: String) async -> String {
        return 
    }
}

struct NewsView: View {
    
    @Environment(\.dataService) private var dataService // have to learn EnvironmentKey
    @State private var articles: [Articles] = []

    var body: some View {
        if articles.empty {
             ContentUnavailableView(...)
             .task {
                 await load()
             } 
        }
        else {
            List(articles) { article in
                ArticleContent(articleID: article.id)
            }
            .refreshable { 
                await load()
            }
        }
    }

    func load() -> async {
       articles = await dataService.articleSearch() // usually you would catch an error and set it on a state to show it
    }
}

struct ArticleContent: View {
    let articleID: String

    @Environment(\.dataService) private var dataService
    @State private var content = ""

    var body: Some View {
        Text(content)
        .task(id: articleID) {
            content = await dataService.articleContent(for articleID: articleID)
        }
    }
}

For selection use another @State.