I am trying to write a simple view that displays text, two lines at a time. It must be possible for the user to move up and down in the stack of text.
In order to make it look good, the items scrolled in and out of the stack must transition in and out from the top or bottom based on the direction the user selected. Below is some test code that demonstrates the problem. It works correctly as long as you keep sliding in one direction, but fails the first time you change direction.
It almost works except for when you change direction. The problem is that the .transition is applied to the Text in line 18 based on the current slideUp state variable. It therefore has the wrong value when the state (direction of movement) changes.
Can anyone see a simple, elegant way of solving this?
import SwiftUI
struct ContentView: View {
private struct DisplayItem {
var id: Int
var text: String
}
private let numVisibleItems = 2
@State private var slideUp: Bool = true
@State private var startIndex: Int = 0
var items: [String] = []
var body: some View {
HStack {
VStack {
ForEach(visibleItems(), id: \DisplayItem.id) { item in
Text("\(item.text)").font(.system(size: 60)).animation(.easeInOut)
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .move(edge: !self.slideUp ? .top : .bottom )),
removal: AnyTransition.opacity.combined(with: .move(edge: self.slideUp ? .top : .bottom ))))
}
}
.animation(.easeInOut(duration: 1.0))
.frame(width: 200, height: 200)
VStack {
Button(action: {
self.slideUp = true
self.startIndex = min(self.items.count-1, self.startIndex + 1)
}, label: { Text("Slide Up") })
Button(action: {
self.slideUp = false
self.startIndex = max(0, self.startIndex - 1)
}, label: { Text("Slide Down") })
}
}
}
private func visibleItems() -> [DisplayItem] {
let endIndex = min(startIndex + numVisibleItems - 1, items.count - 1)
var result = [DisplayItem]()
for i in startIndex...endIndex {
result.append(DisplayItem(id: i, text: items[i]))
}
return result
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let data = (0..<10).map { formatter.string(from: NSNumber(value: $0))! }
return ContentView(items: data)
}
}
Finally, with a dispatch, seems to work as intended.
If that works, thanks to close the thread, otherwise, please describe very precisely what behavior you would want to get.
struct ContentView: View {
private struct DisplayItem {
var id: Int
var text: String
}
private let numVisibleItems = 3
@State private var slideUp: Bool = true
@State private var startIndex: Int = 0
// var items: [String] = []
var items: [String] = ["Hello", "You", "Boys", "Girls"]
var body: some View {
HStack {
VStack {
ForEach(visibleItems(), id: \DisplayItem.id) { item in
Text("\(item.text)").font(.system(size: 30)).animation(.easeInOut)
.transition(.asymmetric(
insertion: AnyTransition.opacity.combined(with: .move(edge: !self.slideUp ? .top : .bottom )),
removal: AnyTransition.opacity.combined(with: .move(edge: self.slideUp ? .top : .bottom ))))
}
}
.animation(.easeInOut(duration: 1.0))
.frame(width: 200, height: 200)
VStack {
Button(action: {
self.slideUp = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.startIndex = min(self.items.count-1, self.startIndex + 1)
}
}, label: { Text("Slide Up") })
Button(action: {
self.slideUp = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.startIndex = max(0, self.startIndex - 1)
}
}, label: { Text("Slide Down") })
}
}
}
private func visibleItems() -> [DisplayItem] {
let endIndex = min(startIndex + numVisibleItems - 1, items.count - 1)
var result = [DisplayItem]()
for i in startIndex...endIndex {
result.append(DisplayItem(id: i, text: items[i]))
}
return result
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let data = (0..<10).map { formatter.string(from: NSNumber(value: $0))! }
return ContentView(items: data)
}
}