How do I delay the mutation of a struct in SwiftUI? - swiftui

I'm coding my app in SwiftUI, and am trying to follow their declarative conventions but I'm coming a little unstuck.
I have a data model which is passed down through the view hierarchy using Bindings, and one of my child views has the ability to change a few of the properties on the Model to trigger a layout change.
Because its animations are a function of the data, I need to stagger the change to the model to get the animation I want. Here's a simple example; assume I need to clear the contents of the child view for architectural reasons. What's the best way to get the content to draw in to the red panel before ChildView animates up, and then when dismissed, wait until the animation is finished before removing the content?
struct ContentView: View {
#State var contents: [String] = ["A", "B", "C", "A", "B", "C", "A", "B", "C"]
#State var showingPanel: Bool = false
var body: some View {
VStack {
HStack {
Button(action: {
self.contents = ["A", "B", "C", "A", "B", "C", "A", "B", "C"]
self.showingPanel = true
}) {
Text("Show Panel")
}
Button(action: {
self.showingPanel = false
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.contents = []
}
}) {
Text("Clear Panel")
}
}
ChildView(showingPanel: $showingPanel, contents: $contents)
}
}
}
struct ChildView: View {
#Binding var showingPanel: Bool
#Binding var contents: [String]
var body: some View {
ZStack {
Color.red
VStack {
ForEach(contents, id: \.self) { content in
Text(content).font(.title)
}
}.animation(nil)
}.offset(y: showingPanel ? 0 : UIScreen.main.bounds.size.height).animation(.default)
}
}
My first stab was to use the DispatchQueue to try and delay the change, but that doesn't look right and also doesn't work!

Related

SwiftUI .searchable modifier makes searchbar twink

I'm using .searchable to embed a search bar into a list view. However, when the .searchable is nested in a NavigationStack (iOS 16 API), it is twinking when the page is loaded (shows up at first and disappears quickly). I hope both pages have a searchable feature.
I can reproduce this issue both on my device iPhone 12 and the simulator iPhone 14. Am I putting the modifier in an incorrect place?
struct ContentView: View {
#State private var selection = "2"
#State var items: [String] = ["0", "1", "2", "3", "4"]
#State var searchText = ""
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
NavigationLink {
NestedListView(items: items)
} label: {
Text(item)
}
}
}
.searchable(text: $searchText)
}
}
}
struct NestedListView: View {
var items: [String]
#State var searchText = ""
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
}
.searchable(text: $searchText)
}
}

Double tap gesture fail to update

I am trying to make a popup menu (favorite menu), and I am stuck when I try to update my detail menu popup screen.
Here, when I double-tapped one of my Home() list objects, the popup menu is popup. From that popup menu, the user will select one of the two buttons. However, every time I tap on the list object, the popup menu shows only the first name of the first array. It looks like the Double Tap Gesture is not updated. It stuck in the first array although I tried to tap one a different list object. Please help me how to update my popup screen based on the selected list object. Also, NavigationLink might be the solution, but I don't want to use NavigationLink. I just want to toggle the screen.
All the detailed code examples are as below.
This is my sample array:
import SwiftUI
struct DataArray: Identifiable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
public struct ListDataArray {
static var dot = [
DataArray(number: 1,
cities: "Baltimore"
name1: "A",
name2: "B"),
DataArray(number: 2,
cities: "Frederick"),
DataArray(number: 3,
cities: "Catonsville"
name1: "Aa",
name2: "Bb",
name3: "Cc",
name4: "Dd"),
]
}
This is for my Double Tab Gesture:
struct Home: View {
#EnvironmentObject var tap: Tapping
var datas: [DataArray] = ListDataArray.dot
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach (data, id: \.id) { data in
if let data1 = data.name1 {
Text(data1)
}
if let city1 = data.cities {
Text(city1)
}
if let data2 = data.name2 {
Text(data2)
}
}
.onTapGesture (count: 2) {
self.tap.isDoubleTab = true
}
}
}
}
}
Finally, this is my target popup menu:
struct DetailMenu: View {
var dataArr = DataArray
#EnvironmentObject var tap: Tapping
var body: some View {
VStact {
Text(lyric.name1)
HStack {
Button(action: {
tap.isDoubleTab = false
}, label: {
Image(systemName: "suit.heart")
})
Button(action: {
tap.isDoubleTab = false
}, label: {
Image(systemName: "gear")
})
}
}
}
}
struct ContenView: View {
var body: some View {
ZStack {
if tap.isDoubleTab {
DetailMenu(dataArr: ListDataArray.dot[0])
} else {
AlphaHome() // This is just another struct
}
}
}
}

How can i remove the trailing red animation at the end of swipe deleting using .onDelete swiftUI

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

SwiftUI: Picker content does not display when selected in a list

I am trying to make a list of selectionable rows in Swift UI, the rows including a Picker. Everything works fine, except that the content of the Picker disappears when selected, see attached screenshot (but is actually visible when the window of the app is not active (i.e. when I click on another window))
I tried everything I could think of, I could not solve this issue. Below a minimal code to reproduce the problem. Anyone has any idea, how to get around this problem
SelectionList.swift
struct SelectionList: View {
#State var dudes: [String] = ["Tim", "Craig", "Phil"]
#State var selectedRow = Set<String>()
var body: some View {
return List(selection: $selectedRow) {
ForEach(self.dudes, id: \.self) { item in
SelectionRow()
}
}
}
}
SelectionRow.swift
struct SelectionRow: View {
#State var selectedFruit = 0
let fruits = ["Apples", "Oranges", "Bananas", "Pears"]
var body: some View {
Picker(selection: self.$selectedFruit, label: EmptyView()) {
ForEach(0 ..< fruits.count, id: \.self) {
Text(self.fruits[$0])
}
}
}
}

SwiftUI List animation when changing data source

I have a List in my SwiftUI app, with on top buttons to see the previous/next category of items to be loaded into the list. When you press the buttons, the source of the List changes, and the List updates with its default animation (where rows are folding away).
Code for a simplified reproducible version (see below for a screenshot):
import SwiftUI
struct ContentView: View {
private let models = [
["a", "b", "c", "d", "e", "f"],
["g", "h"],
["i", "j", "k", "l"],
]
#State private var selectedCategory = 1
private var viewingModels: [String] {
models[selectedCategory]
}
var body: some View {
VStack(spacing: 0.0) {
HStack {
Button(action: previous) {
Text("<")
}
Text("\(selectedCategory)")
Button(action: next) {
Text(">")
}
}
List(viewingModels, id: \.self) { model in
Text(model)
}
}
}
private func previous() {
if selectedCategory > 0 {
selectedCategory -= 1
}
}
private func next() {
if selectedCategory < (models.count - 1) {
selectedCategory += 1
}
}
}
Now, I don't want to use the default List animation here. I want the list items to slide horizontally. So when you press the > arrow to view the next category of items, the existing items on screen should move to the left, and the new items should come in from the right. And the reverse when you press the < button. Basically it should feel like a collection view with multiple pages that you scroll between.
I already found that wrapping the contents of the previous and next functions with withAnimation changes the default List animation to something else. I then tried adding .transition(.slide) to the List (and to the Text within it) to change the new animation, but that doesn't have an effect. Not sure how to change the animation of the List, especially with a different one/direction for the 2 different buttons.
Using a ScrollView with a List per category is not going to scale in the real app, even though yea that might be a solution for this simple example with very few rows :)
If your button is wrapped as you suggested and I added a simple direction boolean:
Button(action: {
withAnimation {
slideRight = true
self.previous()
}
}) {
Text("<")
}
And opposite for the other direction:
Button(action: {
withAnimation {
slideRight = false
self.next()
}
}) {
Text(">")
}
Then you can transition your view like this:
List(viewingModels, id: \.self) { model in
Text(model)
}
.id(UUID())
.transition(.asymmetric(insertion: .move(edge: slideRight ? .leading : .trailing),
removal: .move(edge: slideRight ? .trailing : .leading)))
Note that in order for the list to not animate, we need to give the list a new unique ID each time, see this article: https://swiftui-lab.com/swiftui-id/
UPDATE:
I wanted to provide the full shortened code that works, also removed the UUID() usage based on comments below.
import SwiftUI
struct ContentView: View {
private let models = [
["a", "b", "c", "d", "e", "f"],
["g", "h"],
["i", "j", "k", "l"],
]
#State private var selectedCategory = 0
#State private var slideRight = true
private var viewingModels: [String] {
models[selectedCategory]
}
var body: some View {
VStack(spacing: 0.0) {
HStack {
Button(action: {
withAnimation {
if(self.selectedCategory - 1 < 0) { self.selectedCategory = self.models.count - 1 }
else { self.selectedCategory -= 1 }
self.slideRight = true
}
}) {
Image(systemName: "arrow.left")
}
Text("\(selectedCategory + 1)")
Button(action: {
withAnimation {
if(self.selectedCategory + 1 > self.models.count - 1) { self.selectedCategory = 0 }
else { self.selectedCategory += 1 }
self.slideRight = false
}
}) {
Image(systemName: "arrow.right")
}
}.font(.title)
List(viewingModels, id: \.self) { model in
Text(model)
}
.id(selectedCategory)
.transition(.asymmetric(insertion: .move(edge: slideRight ? .leading : .trailing),
removal: .move(edge: slideRight ? .trailing : .leading)))
}.padding(10)
}
}