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")
}
}
}
}
Related
I have application that has 2 Views. One just keeps an empty array and shows it, and the other one view should add data in this array. In the end I want added info to show on the view that keeps this array.
I've already tried to create an variable of another view inside of the first one, and try to change array, but as i experienced, it doesn't work, because it just created a new instance of view, that has nothing in common with the one that I want to update.
`
//View that keeps array "savedArticles"
struct SavedNewsView: View {
#State var savedArticles: [Article] = []
func removeArticle(at offsets:IndexSet) {
savedArticles.remove(atOffsets: offsets)
}
var body: some View {
NavigationView {
List {
ForEach(savedArticles, id: \.self) {article in
NewsComponentView(title: article.title, description: article.description, urlToImage: article.urlToImage, url: article.url)
}
.onDelete(perform: removeArticle)
}
.listStyle(.plain)
}
.navigationTitle("Saved News")
}
}
`
And this is the view that updates this array:
`
struct NewsView: View {
#StateObject var viewModel = ViewModel()
#State var savedArray = SavedNewsView()
var body: some View {
List {
ForEach(viewModel.news, id: \.self) {article in
if article.urlToImage != nil {
NewsComponentView(title: article.title, description: article.description, urlToImage: article.urlToImage, url: article.url)
.swipeActions() {
Button {
savedArray.savedArticles.append(Article(title: article.title, description: article.description, url: article.url, urlToImage: article.urlToImage))
} label: {
Label("Save", systemImage: "bookmark")
}
.tint(.yellow)
}
}
}
}
.listStyle(.plain)
.onAppear {
viewModel.fetch()
}
}
`
You can pass write access to an #State to a subview using #Binding, e.g.
struct NewsView: View {
#Binding var savedArticles: [Article]
NewsView(savedArticles: $savedArticles)
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)
}
}
}
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
}
I have an array of objects from Core Data that I would like to sort in a function in a child view. I know how to sort using .sorted(by:{}), but the problem is assigning the sorted array back to the ObservedObject variable so the UI updates with the sorted data.
In the code below, when I try to do habits = habits.sorted(by:{}), I get the following errors:
Cannot assign to property: 'self' is immutable
Mark method 'mutating' to make 'self' mutable
Cannot assign value of type '[FetchedResults.Element]' (aka 'Array') to type 'FetchedResults'
Does anyone know a better way to do this or how to resolve the error?
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Habit.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Habit.id, ascending: true)]) var habits: FetchedResults<Habit>
var body: some View {
NavigationView {
VStack {
List {
ForEach(habits, id: \.self) { habit in
VStack(alignment: .leading) {
Text(habit.title)
HStack {
ForEach(habit.dayArray.count-7..<habit.dayArray.count, id: \.self) { index in
DayView(day: habit.dayArray[index], habits: self.habits)
}
}
}
}
}
}
}
}
struct DayView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var day: Day
var habits: FetchedResults<Habit>
var body: some View {
Text(“\(day.wrappedShort)“).onTapGesture {
self.sortHabits(day: self.day)
}
}
func sortHabits() {
self.habits = habits.sorted{ $0.title.uppercased() < $1.title.uppercased() }
}
}
}
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