How to use array of data from different Views - Swift - arrays

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)

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)
}
}
}

How to update variables in ContentView with SheetView function call?

I have a code with a sheet view and a normal view. When I press a button within my sheet view I make an API call. This API call then updates some variables which I'm trying to display in my regular view using ´ForEach´. However, when I make the call in the sheet view and close it down, the array does not seem to update in my normal view. My view just remains blank (except for displaying the button that says "Show sheet". How do I make the array update so that it isn't blank?
Here is my regular view:
// MARK: - Schedule View
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
var body: some View {
GeometryReader { geo in
VStack {
ForEach(SheetView().vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!) // I want this varible to update, but I doesn't
Text(dict["Destination"]!) // It instead remains blank
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
And here is my sheet view:
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var vm: PlanTripViewModel = PlanTripViewModel()
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
Right now, you're making a new SheetView instance on every single ForEach call -- it's not the same one that you're using in your sheet call.
To solve this, you'll want to store the state in your parent view and give the sheet view a reference to it.
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: PlanTripViewModel //<-- Here
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
#StateObject var vm: PlanTripViewModel //<-- Here
var body: some View {
GeometryReader { geo in
VStack {
ForEach(vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!)
Text(dict["Destination"]!)
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView(vm: vm) //<-- Here
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
(Note: you may know this already, but force unwrapping your dictionary values with ! will cause a crash if the keys don't exist. You may want to use optional binding (if let) or another safe check to make sure they exist.)

Swift: Sort Core Data array in a function

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() }
}
}
}

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