How to animate removal transition of multiple rows from SwiftUI List? - ios

Please see this sample view that demonstrates the problem:
struct ListRemovalTransition: View {
let list1 = ["A", "B", "C", "D"]
let list2 = ["A", "E", "F", "G", "H", "I", "J", "K"]
#State var toggle = false
var chosenList: [String] {
toggle ? list1 : list2
}
var body: some View {
VStack {
Toggle(isOn: $toggle) {
Text("Switch List")
}
List(chosenList, id: \.self) { item in
Text(item)
.transition(AnyTransition.opacity.animation(.default))
}
}
.padding()
}
}
struct ListRemovalTransition_Previews: PreviewProvider {
static var previews: some View {
ListRemovalTransition()
}
}
The desired outcome is that the individual rows fade out when removed without changing position. Instead what's happening seems like all the rows first overlap each other before being removed. I've added a transition with animation to the row Text but this has no impact.

Just add id(:) modifier to List to remove default animation. Then add transition(:) modifier to List for your desirable transition. It works perfectly. I just tested on Xcode 11.5. Here is my code...
struct ListRemovalTransition: View {
let list1 = ["A", "B", "C", "D"]
let list2 = ["A", "E", "F", "G", "H", "I", "J", "K"]
#State var toggle = false
var chosenList: [String] {
toggle ? list1 : list2
}
var body: some View {
VStack {
Toggle(isOn: $toggle) {
Text("Switch List")
}
List(chosenList, id: \.self) { item in
Text(item)
}
.id(UUID())
.transition(AnyTransition.opacity.animation(.default))
}
.padding()
}
}
https://media.giphy.com/media/dVu1CMqk3YdtZHefaE/giphy.gif
Thanks. X_X

Related

How to uniquely get Button if I extract as a subview in SwiftUI

So I am making a simple restaurant bill splitting app and I have a button that I want to highlight when the user clicks on it. I also want the other buttons to be not highlighted. In UIKit it would be simple to do this using sender.currentTitle.
Here is my code for the button
struct TipButton: View, Identifiable {
var id: Int
var tipPercentage: String
#State var didTap: Bool = false
#State var buttonLetter: String
#State var zeroPctButton: Bool = false
#State var tenPctButton: Bool = false
#State var twentyPctButton: Bool = false
var body: some View {
Button {
print("Tip is... \(Float(tipPercentage) ?? 7.7)")
didTap.toggle()
if buttonLetter == "A" {
zeroPctButton = true
tenPctButton = false
twentyPctButton = false
}
else if buttonLetter == "B" {
didTap = true
}
} label: {
Text("\(tipPercentage)%")
.font(.largeTitle)
.bold()
}
.background(didTap ? Color.green : Color.clear)
}
}
So far I've been playing around with it and adding different things like Identifiable and #State variables for the percentage amounts but can't figure this out.
In my main file I would have the view built along with these buttons like
struct ButtonsView: View {
var body: some View {
//blah blah
//some UI arranging code
TipButton(id: 1, tipPercentage: "0", buttonLetter: "A")
TipButton(id: 2, tipPercentage: "10", buttonLetter: "B")
TipButton(id: 3, tipPercentage: "20", buttonLetter: "C")
}
}
As you can see, I've tried id and buttonLetter
In short, I want to click on button A, have it highlight, then when I click button B, it highlights and button A is no longer highlighted
In order to do this, you'd want to move the #State from your child view into the parent view. Then, you can share it with the child view's through a Binding.
In this example, I have one #State variable that stores which id is highlighted. When a button is pressed, it simply updates the value.
struct TipButton: View, Identifiable {
var id: Int
var tipPercentage: String
#Binding var highlightedID : Int
var body: some View {
Button {
highlightedID = id
print("Tip is... \(Float(tipPercentage) ?? 7.7)")
} label: {
Text("\(tipPercentage)%")
.font(.largeTitle)
.bold()
}
.background(id == highlightedID ? Color.green : Color.clear)
}
}
struct ButtonsView: View {
#State private var highlightedID : Int = 3
var body: some View {
TipButton(id: 1, tipPercentage: "0", highlightedID: $highlightedID)
TipButton(id: 2, tipPercentage: "10", highlightedID: $highlightedID)
TipButton(id: 3, tipPercentage: "20", highlightedID: $highlightedID)
}
}

Multiple List with SwiftUI

Let say That I have an Array of arrays like so:
var arrays = [["one", "two", "three", "Four"], ["four", "five", "six"]]
My goal is to create a list for each child of each subarray then navigate to the next child.
Example 1: one -> (NavigationLink) two -> (NavigationLink) three -> (NavigationLink) Four.
Example 2: four -> (NavigationLink) five -> (NavigationLink) six.
Each arrow is a NavigationLink between each List.
Here's what I've tried so far:
struct ContentView: View {
let arrays = [["A", "B", "C", "D"], ["E", "F", "G", "H"]]
var body: some View {
NavigationView {
List {
ForEach(arrays, id: \.self) { item in
NavigationLink( destination: DestinationView(items: item)) {
Text(item.first!).font(.subheadline)
}
}
}
.navigationBarTitle("Dash")
}
}
}
struct DestinationView : View {
var items: [String]
var body: some View {
List {
ForEach(items, id: \.self) { item in
NavigationLink( destination: DestinationView(items: items)) {
Text(item).font(.subheadline)
}
}
}
}
}
You can do it like this by slicing the array each time you pass it to the DestinationView
import SwiftUI
struct ContentView: View {
let arrays = [["A", "B", "C", "D"], ["E", "F", "G", "H"]]
var body: some View {
NavigationView {
List {
ForEach(arrays, id: \.self) { item in
NavigationLink( destination: DestinationView(items: Array(item[1..<item.count]))) {
Text(item.first!).font(.subheadline)
}
}
}
.navigationBarTitle("Dash")
}
}
}
struct DestinationView : View {
var items: [String]
var body: some View {
List {
if self.items.count > 0 {
NavigationLink( destination: DestinationView(items: Array(items[1..<items.count]))) {
Text(items[0]).font(.subheadline)
}
}
}
}
}

SwiftUI NavigationLink in the list doesn't get the right detail page with isActive

I just want to simply navigate to a detail page from a List if press any cell. I have a list like this:
When I click cell c it gets d or others. Rather than this page.
Here is my code:
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var isCellSelected = false
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
isActive: $isCellSelected,
label: {
RowView(text: items[index])
})
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
var text: String
var body: some View {
VStack {
Text(text)
}
}
}
If I remove isActive: $isCellSelected then it works as expected. I need to use isCellSelected to pop back to root view. Not sure how to fix this issue.
Any help? thanks!
EDIT
Update removed isActive and try set selection = nil
truct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: DetailView(text: "\(items[index])", isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
RowView(text: items[index])
})
}
}
}
}
}
When press Back button, doesn't go back.
It is not recommended to share a single isActive state among multiple NavigationLinks.
Why don't you use selection instead of isActive?
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil //<-
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
tag: index, //<-
selection: $selectedTag, //<-
label: {
RowView(text: items[index])
})
}
}
}
}
}
You can set nil to selectedTag to pop back. Seems NavigationLink in List does not work as I expect. Searching for workarounds and update if found.
A dirty workaround:
(Tested with Xcode 12.3/iPhone simulator 14.3. Please do not expect this to work on other versions of iOS including future versions.)
struct DetailView: View {
var text: String
#Binding var isCellSelected: Bool
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = false
}
}
}
}
struct Item {
var text: String
var isActive: Bool = false
}
struct ContentView: View {
#State var items = ["a", "b", "c", "d"].map {Item(text: $0)}
#State var listId: Bool = false //<-
var body: some View {
// Text(items.description) // for debugging
NavigationView {
List {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index].text)",
isCellSelected: $items[index].isActive)
.onAppear{ listId.toggle() } //<-
,
isActive: $items[index].isActive,
label: {
RowView(text: items[index].text)
})
}
}
.id(listId) //<-
}
}
}
Another workaround:
struct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
ZStack {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index])",
isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
EmptyView()
})
}
List {
ForEach(items.indices) { index in
Button(action: {
selectedTag = index
}) {
HStack {
RowView(text: items[index])
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.secondary)
}
}
}
}
}
}
}
}

Changing #State variable does not update the View in SwiftUI

I have a following View (took out irrelevant parts):
struct Chart : View {
var xValues: [String]
var yValues: [Double]
#State private var showXValues: Bool = false
var body = some View {
...
if showXValues {
...
} else {
...
}
...
}
}
then I wanted to add a way to modify this value from outside, so I added a function:
func showXValues(show: Bool) -> Chart {
self.showXValues = show
return self
}
so I build the Chart view from the outside like this:
Chart(xValues: ["a", "b", "c"], yValues: [1, 2, 3])
.showXValues(true)
but it works as if the value was still false. What am I doing wrong? I thought updating an #State variable should update the view. I am pretty new to Swift in general, more so to SwiftUI, am I missing some kind of special technique that should be used here?
As in the comments mentioned, #Binding is the way to go.
Here is a minimal example that shows the concept with your code:
struct Chart : View {
var xValues: [String]
var yValues: [Double]
#Binding var showXValues: Bool
var body: some View {
if self.showXValues {
return Text("Showing X Values")
} else {
return Text("Hiding X Values")
}
}
}
struct ContentView: View {
#State var showXValues: Bool = false
var body: some View {
VStack {
Chart(xValues: ["a", "b", "c"], yValues: [1, 2, 3], showXValues: self.$showXValues)
Button(action: {
self.showXValues.toggle()
}, label: {
if self.showXValues {
Text("Hide X Values")
}else {
Text("Show X Values")
}
})
}
}
}
There is no need to create func-s. All I have to do is not mark the properties as private but give them an initial value, so they're gonna become optional in the constructor. So user can either specify them, or not care. Like this:
var showXLabels: Bool = false
This way the constructor is either Chart(xLabels:yLabels) or Chart(xLabels:yLabels:showXLabels).
Question had nothing to do with #State.

SwiftUI context menu not passing correct variable

I would expect that sheet that is presented show the actual string that was pressed in the list.
i.e, long press G and the presenting sheet should show G
Unfortunately it doesn't, I'm assuming this is a SwiftUI bug.
Does anyone have a clean solution for this?
Thanks
struct ContentView: View {
let items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
#State var showing = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item).font(.largeTitle)
.contextMenu {
self.contextEdit(item)
}
}
}
}
}
private func contextEdit(_ item: String) -> some View {
Button(action: {
self.showing.toggle()
print(item)
}) {
Text("Edit")
Image(systemName: "circle")
}.sheet(isPresented: $showing) {
Text(item)
}
}
}
sheets should only be used on the top level. This causes unexpected behaviour as the warnings in your output should also say.
Here is a working example:
struct ContentView: View {
let items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
#State var showing = false
#State var currentItem: String = ""
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item).font(.largeTitle)
.contextMenu {
Button(action: {
self.currentItem = item
self.showing.toggle()
}) {
Text("Edit")
Image(systemName: "circle")
}
}
}
}
}.sheet(isPresented: self.$showing) {
Text(self.currentItem)
}
}
}
I hope this helps
Greetings krjw
After a lot of trail and error I found that his works well for my needs
struct PresentingContextItem<Destination: View>: View {
#State private var showingDestination: Bool = false
var destination: () -> Destination
let title: String
let image: String
init(title: String, image: String, #ViewBuilder _ destination: #escaping () -> Destination) {
self.destination = destination
self.title = title
self.image = image
}
var body: some View {
Button(action: {
self.showingDestination.toggle()
}) {
Text(self.title)
Image(systemName: self.image)
}.sheet(isPresented: self.$showingDestination) {
self.destination()
}
}
}
Usage
struct ContentView: View {
let items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
var body: some View {
NavigationView {
List(items, id: \.self) { item in
Text(item).font(.largeTitle)
.contextMenu {
// option 1
PresentingContextItem(title: "Create", image: "circle") {
Text("Edit...\(item)").foregroundColor(.red)
}
// option 2
self.starItem(item)
}
}
}
}
private func starItem(_ item: String) -> some View {
PresentingContextItem(title: "Star", image: "star") {
Text("Star...\(item)").foregroundColor(.green)
}
}
}

Resources