I have a SwiftUI View that shows a list of items. The view is pretty large so I won't paste the whole thing here. However, the List uses a ForEach:
ForEach(model.items, id: \.id) { item in
myCell(item: item)
}
Everything works fine. What I am trying to do is have a simple slide animation when an item is deleted in the model.
I looked at this answer on stack:
Insert, update and delete animations with ForEach in SwiftUI
However, my setup is different and I am not sure how to try to apply that to my case. In that link the items are stored as a #State property in the view.
My setup is the view uses as #StateObject:
#StateObject var model = MyViewModel()
The model has this:
#Published var items = [MyStruct]()
The List's cells allow the swipe action, the user taps on delete, and the view tells the model to delete that specific item.
The model then has async logic that kicks in and eventually (pretty much immediately) removes the related item from its items. When that happens the List updates, the cell goes away and all works.
I would like to add a cell delete animation. How can I do that given my setup?
EDIT
Trying to add more code to help with context.
The deletion is triggered from an alert like so:
return Alert(
title: Text(title),
message: Text("You cannot undo this action"),
primaryButton: .destructive(Text("Delete")) {
withAnimation {
model.deleteItem(id)
}
},
secondaryButton: .cancel()
)
I added that withAnimation but nothing has changed.
Ok,
so I found what was making things not work even withAnimation.
My model has a call:
model.deleteItem(id)
Internally, as part of the work needed to delete that item, deleteItem(id) uses a Task.init { ... } task. I was then jumping back on main to then update items #Published var items = [MyStruct]() in the model.
The async nature of it broke the withAnimation.
I pulled out the removal of the item from items and left it sync within the withAnimation call and now the default delete animation works.
So, as long as the removal is synchronous within the withAnimation call, the animation kicks in.
Related
See the following app screens:
Content View Screen:
Content View with hierarchical list children rows disclosed:
Parent Row Detail View:
Child Row Detail View:
Referencing the above views, here are the steps I do and the resulting problem I’m trying to solve:
Launch the app.
From the Functions (Content View) presented at launch, see that there is one item listed in a list view (1.0 Move Vessel)
Click the yellow (my app accent color) disclosure arrow at the right of the list item.
Two subordinate child list rows appear under the parent list item, 1.1 Move Position and 1.2 Hold Position.
When I tap the parent item (1.0 Move Vessel) in the hierarchy list, I'm successfully able to navigate to a detail view for that tapped item.
Edit the description of the 1.0 Move Vessel item (defaults to test) of the tapped item properties in the detail view using a TextEditor view.
Click yellow Save button at top left of detail view. The app navigates back to the parent Functions (Content View).
Click on the parent 1.0 Move Vessel row again.
See that description was successfully saved and now displayed from the change made in Step 5 and 6.
Repeat steps 5 through 8 again for 1.1 Move Position list row.
See that the edit/change made to the description was not saved and the default test1 description is displayed instead (not what is wanted).
Repeat steps 5 through 8 again for 1.2 Hold Position list row.
See that the edit/change made to the description was not saved and the default test2 description is displayed instead (not what is wanted).
I think I may have a problem in my save code logic and I'm trying to investigate.
Here are the swift files for the Detail View, the View Model, and the Model (I’ve not included the content view code because that code is working ok with the detail view. Again, I think the problem is in my save button and function call code for updating the view model.
NOTE: sorry that I can’t seem to figure out how to get all the code for a file contiguous in the code view. I seem to have some closing braces that don’t appear in the code view. I think you can still follow the code.
struct FunctionDetailView: View {
#State var vesselFunction: VesselFunction
#State var vesselFunctionDescription: String
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var functionViewModel : FunctionViewModel
var body: some View {
NavigationView {
Form {
Text("Enter description below")
TextEditor(text: $vesselFunctionDescription)
.frame(height: 200)
.toolbar {
Button {
//print(vesselFunction)
vesselFunction.funcDescription = vesselFunctionDescription
//print(vesselFunction)
functionViewModel.updateVesselFunction(vesselFunction: vesselFunction)
//print(vesselFunction)
presentationMode.wrappedValue.dismiss()
} label: {
Text("Save")
}
}
}
.padding()
.navigationTitle(vesselFunction.name)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct FunctionDetailView_Previews: PreviewProvider {
static var previews: some View {
FunctionDetailView(vesselFunction: VesselFunction(id: UUID(), name: "x.x Verb Noun", funcDescription: "Description", children: nil), vesselFunctionDescription: "placeholder")
.environmentObject(FunctionViewModel())
.preferredColorScheme(.dark)
}
}
FunctionViewModel.swift
#MainActor class FunctionViewModel: ObservableObject {
#Published private(set) var decomp : [VesselFunction] = [
VesselFunction(id: UUID(), name: "1.0 Move Vessel", funcDescription: "test", children: [
VesselFunction(id: UUID(), name: "1.1 Move Position", funcDescription: "test1", children: nil),
VesselFunction(id: UUID(), name: "1.2 Hold Position", funcDescription: "test2", children: nil)
])
]
func updateVesselFunction(vesselFunction: VesselFunction) {
/*
if let index = decomp.firstIndex(where: { (existingVesselFunction) -> Bool in
return existingVesselFunction.id == vesselFunction.id
}) {
//run this code
}
*/
// cleaner version of above
if let index = decomp.firstIndex(where: { $0.id == vesselFunction.id }) {
decomp[index] = vesselFunction.updateCompletion()
}
/*
else {
for item in decomp {
if item.children != nil {
if let index = item.children?.firstIndex(where: { $0.id == vesselFunction.id }) {
item.children![index] = vesselFunction.updateCompletion()
}
}
}
} */
}
}
FunctionModel.swift
struct VesselFunction: Identifiable {
let id : UUID
let name : String
var funcDescription : String
var children : [VesselFunction]?
init(id: UUID, name: String, funcDescription: String, children: [VesselFunction]?) {
self.id = id
self.name = name
self.funcDescription = funcDescription
self.children = children
}
func updateCompletion() -> VesselFunction {
return VesselFunction(id: id, name: name, funcDescription: funcDescription, children: children)
}
}
As you can see from the else and for-in loop code commented out at the bottom of the FunctionViewModel code, I was trying to see if I needed to do something like this code to access the children VesselFunction array entries of the decomp published property. With the if let index code that is not commented out, the save function works but only for the top-level decomp array VesselFunction elements, not the nested children arrays elements.
Any help would be appreciated so all decomp array elements, both parent and nested children, can be updated when the TextEditor field is changed and the Save button is pressed in the FunctionDetailView.
NOTE: I am only showing a 1 level deep nested array of children for the decomp property. I actually want to have multiple (at least 3) level of children arrays, so if you have any ideas how to make an updateVesselFunction function work for multiple children array elements, I would appreciate it.
In the main View use ForEach($model.items) { $item in so you get a write access to the model item. In the detail View change the #State to #Binding.
The issue isn't so much your code right now, as it is the architecture of the program. You really need to reorganize the app with MVVM concepts in mind. If you are not sure of them, study Apple’s SwiftUI Tutorials & Stanford’s CS193P. Without a proper architecture, you have gotten lost down a rabbit hole to the extent that I gave up trying to fix the code.
Also, given the structure of your data, I would give serious consideration to using Core Data to model it. Your VesselFunction struct contains an array of VesselFunction, and that it much better modeled as a relationship, rather than having a struct hold an array of the same struct which can hold an array of the same struct. It is a nightmare to deal with as a struct, instead of as a Core Data class.
I would also consider make your FunctionDetailView just display data, and have a separate editing view. This will keep your view separate and easier to manage.
Lastly, you have a lot of redundancy in your naming conventions. Theoretically, you could be trying to access a piece of data at functionViewModel.funcDescription (Not to mention: functionViewModel.children[index].children[subIndex].children[subSubIndex].funcDescription); this can get a bit unwieldy. The further you go down, the worse it will get.
What is the correct way to implement a Picker component with specific logic within a Section element?
I would like to have each type displayed in a separate row.
var types = ["Books", "Films", "Music"]
var body: some View {
Form {
Section(header: Text("Type")) {
TextField("Type", text: $newCategoryType)
// Picker
}
}
}
First you must have a #State property that can be updated based on what selection the user makes, say in this case we have this
#State private var selectedType = "Books"
Then you will implement a Picker SwiftUI struct as follows
Picker("Please choose a type", selection: $selectedType) {
ForEach(types, id: \.self) {
Text($0)
}
}
Note that the \.self is really important for ForEach to distinguish between each element inside the list, without which the Picker won't perform the selection action correctly.
The above is enough for doing the job of displaying each option as a row since that is the default behaviour of ForEach
Additionally if you want to customise the look and feel of the picker
you would like to see .pickerStyle() view modifier, for which the docs and examples are mentioned
Also
Tip: Because pickers in forms have this navigation behavior, it’s important you present them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work. This might be one you create directly around the form, or you could present the form from another view that itself was wrapped in a NavigationView.
I am using Core data and Swiftui and everything have been working fine but in this one view I have a List, ForEach that is not working.
So for testing purpose my code currently look like this:
#ObservedObject var viewModel = NewLearningWhyViewModel()
VStack {
ForEach(viewModel.whys, id: \.self) { why in
Text(why.why)
}
List {
ForEach(viewModel.whys, id: \.self) { why in
Text(why.why)
}
}
Button(action: {
viewModel.createWhy(why: "Test", count: viewModel.whys.count, learning: learn)
viewModel.fetchWhy(predicate: NSPredicate(format: "parentLearning == %#", learn))
}){
Text("Add")
.font(.title)
.foregroundColor(.white)
}
}
The problem is my List { ForEach, first time I press add button it shows the new why, second time i press the button the whole list goes away, the third time I press the button the list shows again with all 3 items.
To test the problem I added that first ForEach part and that shows the correct item at all times, so there is not a problem with the viewmodel or adding the items, the items are added and it is published from the viewmodel since that part is updated.
Does anyone have any clue why my List { ForEach only show every other time?
I have gotten this problem. I figure it out by adding objectWillChange in ViewModel, and send() it manually when your why is changed. Actually I don't know your NewLearningWhyViewModel clearly, so this is just an example, you should try it out.
class NewLearningWhyViewModel: ObservableObject {
let objectWillChange: ObservableObjectPublisher = ObservableObjectPublisher()
#Published var whys: Why = Why() {
didSet {
objectWillChange.send()
}
}
}
Ok the post from Becky Hansmeyer solved it, adding .id(UUID()) to the list solved it and it started working correctly...
because of "viewModel.whys" is set of classes.
SwiftUI does not work with classes directly.
There is 2 solutions:
make it struct instead of class + add #Published modifier inside of view
leave it as is + do it observable object and in init of your view assign into observed object.
More details here:
https://stackoverflow.com/a/62919526/4423545
I have list of structs that I display in the view using a ForEach, I am trying to add to the list and when the user taps they see information for that item. Using two approaches I get two different results.
First Approach
The view updates the view when items are added perfectly, but changing the note in the Details changes values of all notes.
#Binding var person: Person
ForEach(self.person.notes) {
note in
DetailsCard(person: self.$person, note: notes)
}
Second Approach
The view does not update when notes are added, only when the view reappears does it show the new items. But I when the items are shown, the details view works as expected.
#Binding var person: Person
ForEach(self.person.notes.indices) { index in
VStack {
DetailsCard(person: self.$person, note: self.person.notes[index])
}
}
DetailView
#Binding var person: Person
#State var note: Note
This should be a fairly simple task but working with SwiftUI I am confused by the two different results for similar approaches. Does anyone have any ideas on why this is occurring and how to correctly dynamically update the view and pass the values.
Try using dynamic ForEach loops, ie. with an explicit id parameter:
ForEach(self.person.notes, id:\.self)
ForEach(self.person.notes.indices, id:\.self)
Note: id must conform to Hashable. If you decide to use self as an id, it will only work if your item conforms to Hashable. Otherwise you may want to use some property (or just add the conformance).
I have reduced my problem to the following views which are embedded in two WKHostingControllers paging between each other.
struct FirstContentView: View {
var body: some View {
Text("FirstContentView")
.contextMenu {
Text("FirstContextMenu")
}
}
}
struct SecondContentView: View {
var body: some View {
List {
Text("SecondContentView")
}.contextMenu {
Text("SecondContextMenu")
}
}
}
When I run it this happens. As soon as page 2 is loaded the context menu of page 1 remains replaced by the one of page 2. This issue only happens if a List view is anywhere in the body of SecondContentView, even if the .contextMenu is not attached to it or the List is encapsulated into another View. Replacing List for example with Group works, but I need the list behaviour for my items.
Has anybody also run into this issue? How can I fix it so the context menu works as intended (?) on both pages?