How to set up a model for a SwiftUI news app - arrays

I am attempting to build a basic app that returns a list of news items from a newsapi.org API data source. I set up my viewModel to fetch news from the API and convert the data to the model. I then set up my model to be structured around specific items from the api response (see URL for API in the viewModel below). Finally, I set up my ContentView to return the news items in a list. The app builds fine, but the news items (source name, article title) do not populate on the screen, and in the console I get the printed "failed" message. Is my model not set up correctly? Should Article instead be an array of Article (ex. [Article])? Any idea how to set up the model (or viewModel) in order to populate news items on the screen? I appreciate the feedback.
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = NewsViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.articles, id: \.articles.source.id) { news in
VStack(alignment: .leading) {
Text(news.articles.source.name)
Text(news.articles.title)
Text(news.articles.description)
}
}
}
.navigationTitle("News List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Model
import Foundation
struct APIResponse: Codable {
let articles: Article
}
struct Article: Codable {
let source: Source
let title: String
let description: String
}
struct Source: Codable, Identifiable {
let id: String
let name: String
}
ViewModel
import Foundation
class NewsViewModel: ObservableObject {
#Published var articles = [APIResponse]()
init() {
fetchNews()
}
func fetchNews() {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=4b6cfa9b54c74b4db7d7d8a2120718d3") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let model = try JSONDecoder().decode([APIResponse].self, from: data)
//update properties on the main thread
DispatchQueue.main.async {
self.articles = model
}
}
catch {
print("failed")
}
}
task.resume()
}
}

Always print error in case you aren't sure why its failing:
print("\(error)") // Instead of print("failed")
Make it optional if any value has null in it:
struct APIResponse: Codable {
let articles: [Article]
}
struct Article: Codable {
let source: Source
let title: String
let description: String?
}
struct Source: Codable, Identifiable {
let id: String?
let name: String?
}
Update the view:
struct ContentView: View {
#ObservedObject var viewModel = NewsViewModel()
var body: some View {
NavigationView {
List(viewModel.articles, id: \.source.id) { news in
VStack {
VStack(alignment: .leading) {
Text(news.source.name ?? "")
Text(news.title)
Text(news.description ?? "")
}
}
}
.navigationTitle("Landmarks")
}
}
}
View Model Updates:
#Published var articles = [Article]()
let model = try JSONDecoder().decode(APIResponse.self, from: data)
//update properties on the main thread
DispatchQueue.main.async {
self.articles = model.articles
}

Related

How to bind a list of textfields that edit a variable within an a core data array?

Swift ui requires a Binding<String> to link to the value you are updating in a text field. Much like the native iPhone Reminders app, I am looking to permit inline editing a list that will persist.
The attached code works only but gives the same name for each item due to them all being bound to the same variable. How can I bind this to the [FruitEntity] array?
class CoreDataViewModel: ObservableObject {
//static let instance = CoreDataViewModel()
let container: NSPersistentContainer
let context: NSManagedObjectContext
#Published var savedEntities: [FruitEntity] = []
}
struct Screen: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: entity.name) { text in
entity.name = questionVariable
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}
}
}
You can just move the TextField to a separate view, with its own #State var for the field and another var for the entity.
Create a view like the following one:
struct ChangeName: View {
// Will change the entity
let entity: FruitEntity
// Will update the field
#State private var questionVariable = ""
var body: some View {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: questionVariable) { text in
entity.name = text
// Remember to save the persistent container/ managed-object-context
}
}
}
Call it in your main view:
struct Screen: View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
ChangeName(entity: entity)
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}

MacOS - Using a JSON array to add entries to a SwiftUI list

Some time ago, I asked a question involving decoding JSON. I am using this JSON to create an entry in a list and decide whether or not to have a checkbox ticked.
My previous question asked how to decode the JSON, and I have this code which looks useful (below), but to be honest, I have no idea what to do with.
Where would I put this within a SwiftUI file where I could loop through each piece of JSON in this array and add an entry into a SwiftUI?
I have experience with Python, and I understand how it should be done, but am quite the beginner in swift...
My code:
struct Mod: Decodable {
let id: String
let display: String
let description: String
let url: String?
let config: Bool?
let enabled: Bool?
let hidden: Bool?
let icon: String?
let categories: [String]?
// let actions: Array<OptionAction>?
// let warning: ActionWarning?
}
let modsURL = URL(string: "https://raw.githubusercontent.com/nacrt/SkyblockClient-REPO/main/files/mods.json")!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([Mod].self, from: data!)
print(result)
} catch {print(error)}
}
task.resume()
let mods_test: () = importJSON(url: modsURL)
let enableds = mods_test.map {$0.enabled}
If there is anything wrong with this question, please tell me! Thanks in advance for the help.
Here, how you can do that,
you can use State to declare variable
struct ContentView: View {
//Declare variable
#State var jsonDataList = [jsonData]()
var body: some View {
VStack {
List(jsonDataList, id: \.id) { jsonDataList in
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text(jsonDataList.id)
.font(.title3)
.fontWeight(.bold)
Text(jsonDataList.display)
.font(.subheadline)
.fontWeight(.bold)
}
Spacer()
Image(systemName: jsonDataList.enabled ?? false ? "checkmark.square": "square")
}
}
}
.onAppear(perform: loadData)
}
}
//MARK: - Web Service
func loadData() {
guard let modsURL = URL(string: "https://raw.githubusercontent.com/nacrt/SkyblockClient-REPO/main/files/mods.json") else {
print("Invalid URL")
return
}
let task = URLSession.shared.dataTask(with: modsURL) { (data, _, error) in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([jsonData].self, from: data!)
jsonDataList = result
print("Response:",jsonDataList)
} catch {
print(error)
}
}
task.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - Data Model
struct jsonData: Codable, Identifiable {
let id: String
let display: String
let description: String
let url: String?
let config: Bool?
let enabled: Bool?
let hidden: Bool?
let icon: String?
let categories: [String]?
}
Response Screenshot :

SwiftUI MVVM how to loop through data and store them inside array

I am using MVVM in my SwiftUI project and after I request to fetch data from the API, I want to loop through the data and store some of them inside an array, however it returns error, what is the correct method to do this?
Here is my code and data struct
MenuDetailView
struct MenuDetailView: View {
#ObservedObject var viewModel = MenuDetailViewModel()
var body: some View {
ForEach(self.viewModel.variantGroup, id: \.self) { vg in
/*** I Need to loop and store vg.variantGroupName into array viewModel.variantChosen, how? ***/
self.viewModel.variantChosen.append(vg.VariantGroupName)
// This always return error:
// Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
VStack {
HStack {
Text(vg.variantGroupName)
Text(String(self.viewModel.arrVariantChoosen[0]))
}
VStack {
ForEach(vg.variant, id: \.self) { v in
Text(v.variantName)
}
}
}
}
}
}
}
MenuDetailViewModel.swift
class MenuDetailViewModel: ObservableObject, MenuDetailService {
var apiSession: APIService
#Published var detaildata: MenuDetailData?
#Published var variantGroup = [MenuDetailVariantGroup]()
#Published var variantChosen: Array<String> = []
var cancellables = Set<AnyCancellable>()
init(apiSession: APIService = APISession()) {
self.apiSession = apiSession
}
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
}
cancellables.insert(cancellable)
}
}
MenuDetailData.swift
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let idMenu: String
let menuName: String
let variantGroup: [MenuDetailVariantGroup]
}
MenuDetailVariantGroup.swift
struct MenuDetailVariantGroup: Codable, Identifiable, Hashable {
let id = UUID()
let variantGroupName: String
let variant: [MenuDetailVariant]
let limit: Int
}
MenuDetailVariant.swift
struct MenuDetailVariant: Codable, Identifiable, Hashable {
let id = UUID()
let variantName: String
}
Thank you all in advance
You can not add this inside the ForEach. In SwiftUI, ForEach is a view. It accepts view data, it's not the same as Array.forEach.
You need to do it inside the view model. Like this
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
self.variantChosen = self.variantGroup.map{$0.variantGroupName} //<--here
}
cancellables.insert(cancellable)
}
Remove this from ForEach
self.viewModel.variantChosen.append(vg.VariantGroupName)

Multidimensional lists in SwiftUI that pass data

I have this model:
struct Training: Identifiable {
let id = UUID()
let name: String
let workout: [Workout]?
}
and:
struct Workout: Identifiable {
let id = UUID()
let name: String
let exercices: [Exercice]?
}
and:
struct Exercice: Identifiable {
let id = UUID()
let name: String
}
The data for the models is coming from an environment object.
The app will launch with an empty list of trainings and you can add trainings within the UI. Each training has a navigtaionlink to a view to add workouts to each training and in the next step you can add exercices to each workout.
In my logic I create multidimensional arrays with the structs shown above.
The trainings view is easy:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
#State var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.trainings) { training in
NavigationLink(destination: WorkoutsView(training: training).environmentObject(self.appState)) {
Text(training.name)
}
}
.onDelete(perform: appState.removeTraining)
}
// Button to add trainings....
.navigationBarTitle(Text("Trainings").foregroundColor(Color.white))
}
}
}
}
The WorkoutsView is looking the same but I have an issue with listing the items of the parent training:
struct WorkoutsView: View {
// ...
var training: Training
var body: some View {
VStack {
List {
ForEach (appState.trainings(training).workouts) { workout in // I know the appState call is incorrect, but I don't know how to access is correctly.
NavigationLink(destination: ExercicesView(workout)) {
Text(workout.name)
}
}
}
// ...
}
}
}
I already tried:
List {
ForEach (0 ..< appState.trainings.count) {
NavigationLink(destination: WorkoutsView(training: $0).environmentObject(self.appState)) {
Text(appState.trainings[$0].name)
}
}
}
I could use appState.trainings[training].workouts in the WorkoutsView but I'm getting the error Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body on the NavigationLink line and don't know what to do.
Additional question: If this is close to the solution, I don't need the struct to conform to Identifiable?
You have 2 broad approaches here, depending on how you want to design your system.
1. Your child views know about the app state and can modify it directly. So, the parent needs to pass the indices/keys for the child to locate which data to modify:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
List(0..<appState.trainings.count) { i in
NavigationLink(destination: WorkoutsView(trainingIndex: i)) {
Text(self.appState.trainings[i].name)
}
}
}
}
}
struct WorkoutsView: View {
var trainingIdx: Int
#EnvironmentObject var appState: AppState
var body: some View {
VStack() {
TextField("training name: ", text: $appState.trainings[trainingIdx].name)
Button(action: {self.appState.trainings[trainingIdx].workouts.append(Workout(...))}) {
Text("Add workout")
}
}
}
}
2. Alternatively, you might say that you don't want your child views to know about the app's state - you just want them to modify some static struct that they don't own (but rather owned by their parent), then you should use use a #Binding.
The example below is conceptual to illustrate a point:
struct TrainingsView: View {
#State var trainingA = Training(...)
#State var trainingB = Training(...)
var body: some Body {
NavigationView {
List {
WorkoutsView(training: $trainingA)
WorkoutsView(training: $trainingB)
}
}
}
}
struct WorkoutsView: View {
#Binding var training: Training
var body: some View {
VStack() {
TextField("training name: ", text: $training.name)
Button(action: { self.training.workouts.append(Workout(...)) }) {
Text("Add workout")
}
}
}
}

How do I make the Observable Object update the list?

So I know my items are being added to the 'vitallist'(through printing the list in the terminal), but I am not seeing them appear on list view. I think it has something to do with the 'ObservedObject' not being linked correctly. Any suggestions?
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
I also had same problem.
I am not sure why, but it works that creating a new element in the array, not changing the element itself. I confirmed directly updating works only in data, but not for binding UI.
In my code, element change in TobuyData class.
class Tobuy: Identifiable {
let id = UUID()
var thing: String
var isDone = false
init(_ thing: String, isDone: Bool = false) {
self.thing = thing
self.isDone = isDone
}
}
class TobuyData: ObservableObject {
#Published var tobuys: [Tobuy]
init() {
self.tobuys = [
Tobuy("banana"),
Tobuy("bread"),
Tobuy("pencil"),
]
}
func toggleDone(_ tobuy: Tobuy) {
if let j = self.tobuys.firstIndex(where: { $0.id == tobuy.id }) {
self.tobuys[j] = Tobuy(self.tobuys[j].thing, isDone: !self.tobuys[j].isDone)
// self.tobuys[j].isDone.toggle() // this works only in data, but not for binding UI
}
}
}
In View
struct ContentView: View {
#EnvironmentObject var tobuyData: TobuyData
var body: some View {
List {
ForEach(tobuyData.tobuys) { tobuy in
Text(tobuy.thing)
.strikethrough(tobuy.isDone)
.onTapGesture { self.tobuyData.toggleDone(tobuy) }
...
p.s.
Changing Tobuy Class to Struct made direct element updating work, the comment out part above. This referenced to Apple's official tutorial: "Handling User Input"
change
#ObservedObject var vitallist = VitalList()
to
#EnvironmentObject var vitallist = VitalList()
The code seems fine. I added a simple add method to VitalList
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
func addVital(){
self.vitallist.append(Vital(name: UUID().description))
}
}
And a Button to the body
var body: some View {
NavigationView{
VStack{
Button(action: {self.vitallist.addVital()}, label: {Text("add-vital")})
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
The list updates as expected. check your code that adds your items to
#Published var vitallist = [Vital]()
Are you using the same instance of VitalList? A singleton might help.
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton

Resources