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.
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.
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
.