英文:
How to use transition() on view together with onAppear() or task() in SwiftUI?
问题
假设我有一个名为MainView的视图,其中包含一个按钮,当我点击该按钮时,我希望在另一个视图DataView上呈现一些数据。有两个要求:
- 在屏幕上切换DataView时应该有一个动画效果。
- DataView的数据需要在视图创建后的某个时刻加载。
作为第二个要求的结果,我创建了一个状态变量,指示加载是否已完成。当加载尚未完成时,会显示相应的信息标签(例如,“正在加载数据”)。如果加载完成,将呈现加载的数据。
现在,我需要在某个地方启动加载过程,因此我通过DataView的*onAppear()方法进行同步加载,或者通过task()*方法进行异步加载。
然后,我添加了用于实现第一个要求的动画代码,但应用程序的行为不如我所期望:在加载标签存在时,一切都按计划进行,但在我将其更改为数据后,数据不会继续以动画方式呈现,而是立即出现在其目的地。
我明白,在动画期间子视图切换时,SwiftUI可能难以跟踪一切并实现我期望发生的事情。我也不介意在动画完成后开始加载数据。也许我甚至更喜欢这种方式,但我理解没有简单的方法告诉动画何时完成。与此同时,我有一种感觉,我想要实现的,即在视图出现后开始加载数据,对于几乎每个应用程序来说都是一个基本场景。那么,我在实现上出了什么问题?
import SwiftUI
import PlaygroundSupport
struct MainView: View {
@State var show = false
var body: some View {
VStack {
if show {
DataView()
.transition(.move(edge: .leading))
}
Spacer()
Button("Show data") {
withAnimation(.linear(duration: 4)) {
show.toggle()
}
}
}
}
}
struct DataView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Data view")
if loaded {
Text("The data")
} else {
LoaderView()
}
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
loaded = true
}
}
}
struct LoaderView: View {
var body: some View {
Text("Loading data")
}
}
PlaygroundPage.current.setLiveView(
MainView()
.frame(width: 300, height: 200, alignment: .center))
编辑:我根据vacawama的答案添加了LoaderView到代码中。
英文:
Assume that I have a MainView with a button and when I tap on it I want to present some data on another view - DataView. There are two requirements:
- There should be an animation for transitioning DataView on the screen
- Data for DataView needs to be loaded sometime after creation of the view
As a consequence of the second requirement I created a state variable indicating if loading is finished. When it is not yet finished appropriate informational label is presented (i.e. "loading data"). If loading is done, the loaded data is presented.
Now I need to initiate loading process somewhere so I do it via the onAppear() method of the DataView in case of synchronous loading or via task() method in case of async loading.
Then I add code for animating to fulfill requirement number one and the app does not behave as I want it to: while there is a loading label everything goes as planned, but after I change it for the data, the data does not continue to be animated but pops out at its destination right away.
I get it that when the subviews switch during animation it may be hard for SwiftUI to track everything and accomplish what I expect it to happen. And I don't mind to start loading the data after the animation is finished. Maybe I would even prefer it that way, but it is my understanding that there is no easy way to tell that the animation is finished. And at the same time I have a feeling that what I want to accomplish, i.e. start data loading after view appears is a basic scenario for nearly every app. So where I did go wrong with my implementation?
import SwiftUI
import PlaygroundSupport
struct MainView: View {
@State var show = false
var body: some View {
VStack {
if show {
DataView()
.transition(.move(edge: .leading))
}
Spacer()
Button("Show data") {
withAnimation(.linear(duration: 4)) {
show.toggle()
}
}
}
}
}
struct DataView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Data view")
if loaded {
Text("The data")
} else {
LoaderView()
}
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
loaded = true
}
// it does not work with onAppear also
// .onAppear {
// loaded = true
// }
}
}
struct LoaderView: View {
var body: some View {
Text("Loading data")
}
}
PlaygroundPage
.current.setLiveView(
MainView()
.frame(width: 300, height: 200, alignment: .center))
EDIT: I updated code by adding LoaderView to counter vacawama's answer
答案1
得分: 1
原文:
Due to the if-else
structure, SwiftUI sees the two Text
views as different views which is why the animation stops as soon as loaded
changes to true
. If you replace the if
statement with a ternary operator ? :
to replace the input data to the Text
view, then SwiftUI will see the Text
view as the same view and the animation will continue after loading
is set to true
.
替换为:
由于 if-else
结构,SwiftUI 将两个 Text
视图视为不同的视图,这就是为什么动画在 loaded
改变为 true
时立即停止的原因。如果您用三元运算符 ? :
替换 if
语句来替换 Text
视图的输入数据,那么SwiftUI将把Text
视图视为相同的视图,动画将在 loading
设置为 true
后继续播放。
原文:
After your Edit, the same issue applies. Swapping out a view that is animating will lose the animation. You need to find a way to unite the "Loading data" message and the actual display of the information. Here is a way you could do it by passing the data to the LoadingView
:
struct DataView: View {
@State private var loaded = false
@State private var data = ""
var body: some View {
VStack {
Text("Data view")
LoaderView(loaded: loaded, data: data)
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
data = "The data"
loaded = true
}
}
}
struct LoaderView: View {
let loaded: Bool
let data: String
var body: some View {
Text(loaded ? data: "Loading data")
}
}
替换为:
在您的 编辑 后,相同的问题仍然存在。更换正在进行动画的视图会导致动画丢失。您需要找到一种方法来将“Loading data”消息与实际信息的显示合并在一起。以下是一种通过将数据传递给 LoadingView
来实现的方法:
struct DataView: View {
@State private var loaded = false
@State private var data = ""
var body: some View {
VStack {
Text("Data view")
LoaderView(loaded: loaded, data: data)
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
data = "The data"
loaded = true
}
}
}
struct LoaderView: View {
let loaded: Bool
let data: String
var body: some View {
Text(loaded ? data: "Loading data")
}
}
原文:
Another way to approach this is to have "Loading data"
and your data displayed at all times, but make one of them invisible depending on the state of loaded
:
struct DataView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Data view")
ZStack {
Text("The data").opacity(loaded ? 1 : 0)
LoaderView().opacity(loaded ? 0 : 1)
}
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
loaded = true
}
}
}
替换为:
另一种方法是始终显示 “Loading data”
和您的数据,但根据 loaded
的状态使它们中的一个不可见:
struct DataView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Data view")
ZStack {
Text("The data").opacity(loaded ? 1 : 0)
LoaderView().opacity(loaded ? 0 : 1)
}
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
loaded = true
}
}
}
英文:
Due to the if-else
structure, SwiftUI sees the two Text
views as different views which is why the animation stops as soon as loaded
changes to true
. If you replace the if
statement with a ternary operator ? :
to replace the input data to the Text
view, then SwiftUI will see the Text
view as the same view and the animation will continue after loading
is set to true
.
replace:
if loaded {
Text("The data")
} else {
Text("Loading data")
}
with:
Text(loaded ? "The data" : "Loading data")
After your Edit, the same issue applies. Swapping out a view that is animating will lose the animation. You need to find a way to unite the "Loading data" message and the actual display of the information. Here is a way you could do it by passing the data to the LoadingView
:
struct DataView: View {
@State private var loaded = false
@State private var data = ""
var body: some View {
VStack {
Text("Data view")
LoaderView(loaded: loaded, data: data)
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
data = "The data"
loaded = true
}
}
}
struct LoaderView: View {
let loaded: Bool
let data: String
var body: some View {
Text(loaded ? data: "Loading data")
}
}
Alternate Solution
Another way to approach this is to have "Loading data"
and your data displayed at all times, but make one of them invisible depending on the state of loaded
:
struct DataView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Data view")
ZStack {
Text("The data").opacity(loaded ? 1 : 0)
LoaderView().opacity(loaded ? 0 : 1)
}
}
.task {
try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC)))
loaded = true
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论