I have this code:
struct ContentView: View {
#State private var items = [Int]()
#State private var value = 1
var body: some View {
VStack {
List {
ForEach(items, id: \.self) {
Text("\($0)")
}
.onDelete(perform: DeleteRow)
}
Button("Add Number") {
self.items.append(self.value)
self.value += 1
}
}
}
func DeleteRow(at offsets: IndexSet) {
DispatchQueue.main.asyncAfter(deadline: .now()) {
items.remove(atOffsets: offsets)
}
}
}
Want i want to do is to remove the stretchy top animation line when deleting an item . I could not find any solution online. I attached a pic . The red line attached to the animation is what i want to remove.I delete item 9 in this example.
Related
I want to make a recursive view like this:
But what I have done is like this:
It's a tvOS application, the sample code is:
struct MainView: View {
#State private var selectedItem: ListItem?
var body: some View {
VStack {
RecursiveFolderListView(fileId: "root", selectedItem: $selectedItem)
}
}
}
struct RecursiveFolderListView: View {
#EnvironmentObject var api: API
var fileId: String
#Binding var selectedItem: ListItem?
#State private var currentPageSelectedItem: ListItem?
#State private var list: [ListItem]?
#State private var theId = 0
var body: some View {
HStack {
if let list = list, list.count > 0 {
ScrollView(.vertical) {
ForEach(list, id: \.self) { item in
Button {
selectedItem = item
currentPageSelectedItem = item
} label: {
HStack {
Text(item.name)
.font(.callout)
.multilineTextAlignment(.center)
.lineLimit(1)
Spacer()
if item.fileId == selectedItem?.fileId {
Image(systemName: "checkmark.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
.foregroundColor(.green)
}
}
.frame(height: 60)
}
}
}
.focusSection()
.onChange(of: currentPageSelectedItem) { newValue in
if list.contains(where: { $0 == newValue }) {
theId += 1
}
}
} else {
HStack {
Spacer()
Text("Empty")
Spacer()
}
}
if let item = currentPageSelectedItem, item.fileId != fileId {
RecursiveFolderListView(fileId: item.fileId, selectedItem: $selectedItem)
.id(theId)
}
}
.task {
list = try? await api.getFiles(parentId: fileId)
}
}
}
It's a list view, and when the user clicks one item in the list, it will expand the next folder list to the right. The expanded lists and the left one will have the same width.
I think it needs Geometryreader to get the full width, and pass down to the recursive hierarchy, but how to get how many views in the recursive logic?
I know why my code have this behavior, but I don't know how to adjust my code, to make the recursive views the same width.
Since you didn't include definitions of ListItem or API in your post, here are some simple definitions:
struct ListItem: Hashable {
let fileId: String
var name: String
}
class API: ObservableObject {
func getFiles(parentId: String) async throws -> [ListItem]? {
return try FileManager.default
.contentsOfDirectory(atPath: parentId)
.sorted()
.map { name in
ListItem(
fileId: (parentId as NSString).appendingPathComponent(name),
name: name
)
}
}
}
With those definitions (and changing the root fileId from "root" to "/"), we have a simple filesystem browser.
Now on to your question. Since you want each column to be the same width, you should put all the columns into a single HStack. Since you use recursion to visit the columns, you might think that's not possible, but I will demonstrate that it is possible. In fact, it requires just three simple changes:
Change VStack in MainView to HStack.
Change the outer HStack in RecursiveFolderListView to Group.
Move the .task modifier to the inner HStack around the "Empty" text, in the else branch.
The resulting code (with unchanged chunks omitted):
struct MainView: View {
#State private var selectedItem: ListItem? = nil
var body: some View {
HStack { // ⬅️ changed
RecursiveFolderListView(fileId: "/", selectedItem: $selectedItem)
}
}
}
struct RecursiveFolderListView: View {
...
var body: some View {
Group { // ⬅️ changed
if let list = list, list.count > 0 {
...
} else {
HStack {
Spacer()
Text("Empty")
Spacer()
}
.task { // ⬅️ moved to here
list = try? await api.getFiles(parentId: fileId)
}
}
}
// ⬅️ .task moved from here
}
}
I don't have the tvOS SDK installed, so I tested by commenting out the use of .focusSection() and running in an iPhone simulator:
This works because the subviews of a Group are “flattened” into the Group's parent container. So when SwiftUI sees a hierarchy like this:
HStack
Group
ScrollView (first column)
Group
ScrollView (second column)
Group
ScrollView (third column)
HStack (fourth column, "Empty")
SwiftUI flattens it into this:
HStack
ScrollView (first column)
ScrollView (second column)
ScrollView (third column)
HStack (fourth column, "Empty")
I moved the .task modifier because otherwise it would be attached to the Group, which would pass it on to all of its child views, but we only need the task applied to one child view.
Although rob's answer is perfect, I want to share another approach.
class SaveToPageViewModel: ObservableObject {
#Published var fileIds = [String]()
func tryInsert(fileId: String, parentFileId: String?) {
if parentFileId == nil {
fileIds.append(fileId)
} else if fileIds.last == parentFileId {
fileIds.append(fileId)
} else if fileIds.last == fileId {
// do noting, because this was caused by navigation bug, onAppear called twice
} else {
var copy = fileIds
copy.removeLast()
while copy.last != parentFileId {
copy.removeLast()
}
copy.append(fileId)
fileIds = copy
}
}
}
And wrap the container a GeometryReader and using the SaveToPageViewModel to follow the recursive view's length:
#State var itemWidth: CGFloat = 0
...
GeometryReader { proxy in
...
RecursiveFolderListView(fileId: "root", selectedItem: $selectedItem, parentFileId: nil, itemWidth: itemWidth)
.environmentObject(viewModel)
...
}
.onReceive(viewModel.$fileIds) { fileIds in
itemWidth = proxy.size.width / CGFloat(fileIds.count)
}
And in the RecursiveFolderListView, change the model data:
RecursiveFolderListView(fileId: item.fileId, selectedItem: $selectedItem, parentFileId: fileId, itemWidth: itemWidth)
.id(theId)
...
}
.onAppear {
model.tryInsert(fileId: fileId, parentFileId: parentFileId)
}
When using a NavigationView and a ScrollView with searchable, as soon as you focus a item in the LazyVGrid the search bar collapses the keyboard, and it's no longer possible to re-focus the search bar to change the query.
It doesn't matter if the .searchable modifier is applied to the ScrollView or the NavigationView.
The more I look at it, the more it appears to be a SwiftUI bug on tvOS, but I would still like to find a workaround, if possible.
Sample code which reproduces the problem:
import SwiftUI
struct ContentView: View {
private var fruits = ["Apples", "Pears", "Oranges", "Plums", "Pineapples", "Bananas"]
#State private var items: [String]
#State private var searchText: String = ""
init() {
self.items = fruits
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300))], spacing: 40) {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(text: item)) {
Text(item)
}
}
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { query in
if query.isEmpty {
items = fruits
} else {
items = fruits.filter { $0.contains(query) }
}
}
}
}
}
struct DetailView: View {
let text: String
var body: some View {
Text(text)
}
}
Gif illustrating the problem:
It seems that there is a problem in SwiftUI with List and deleting items. The items in the list and data get out of sync.
This is the code sample that reproduces the problem:
import SwiftUI
struct ContentView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
}
}
.animation(.easeInOut, value: popupShown)
}
}
struct MainListView: View {
#State var texts = (0...10).map(String.init)
func delete(at positions: IndexSet) {
positions.forEach { texts.remove(at: $0) }
}
var body: some View {
List {
ForEach(texts, id: \.self) { Text($0) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
If you perform a delete action on the first row and scroll to the last row, the data and list contents are not in sync anymore.
This is only happening when animation is attached to it. Removing .animation(.easeInOut, value: popupShown) workarounds the issue.
This code sample works as expected on iOS 14 and doesn't work on iOS 15.
Is there a workaround for this problem other then removing animation?
It isn't the animation(). The clue was seeing It appears that having the .animation outside of the conditional causes the problem. Moving it to the view itself corrected it to some extent. However, there is a problem with this ForEach construct: ForEach(texts, id: \.self). As soon as you start deleting elements of your array, the UI gets confused as to what to show where. You should ALWAYS use an Identifiable element in a ForEach. See the example code below:
struct ListDeleteView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
.animation(.easeInOut, value: popupShown)
}
}
}
}
struct MainListView: View {
#State var texts = (0...10).map({ TextMessage(message: $0.description) })
func delete(at positions: IndexSet) {
texts.remove(atOffsets: positions)
}
var body: some View {
List {
ForEach(texts) { Text($0.message) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
struct TextMessage: Identifiable {
let id = UUID()
let message: String
}
This is the code :
struct ContentView: View {
#State var names = ["A" , "B", "C", "D"]
var body: some View {
List {
ForEach(names, id: \.self ) { name in
Group {
testStruct(name: name)
}
}.onDelete(perform: removeItems)
}
}
private func removeItems (indexSet: IndexSet) {
names.remove(atOffsets: indexSet)
}
}
struct testStruct: View , Identifiable {
#State var name: String
let id = UUID()
var body: some View {
HStack {
Text(name)
Spacer()
Image(systemName: "folder.fill")
}
}
}
I am unable to remove the trailing red animation on swiping onDelete . Is there any elegant way of doing that . .animation() seem not to be working
I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.
Here's a video. Why is this happening?
import SwiftUI
struct TestPopOver: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var showThemeEditor = false
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.showThemeEditor = true
}
}
}
}
}
.popover(isPresented: $showThemeEditor) {
CustomPopOver(isShowing: $showThemeEditor, text: text)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
struct CustomPopOver: View {
#Binding var isShowing: Bool
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.isShowing = false
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.
You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.
struct EditItem : Identifiable { //this will tell it what sheet to present
var id = UUID()
var str : String
}
struct ContentView: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.editItem = EditItem(str: text) //set the current item
}
}
}
}
}
.popover(item: $editItem) { item in //item is now a reference to the current item being presented
CustomPopOver(text: item.str)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CustomPopOver: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (#Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.