如何在SwiftUI中将`transition()`与`onAppear()`或`task()`一起用于视图?

huangapple go评论72阅读模式
英文:

How to use transition() on view together with onAppear() or task() in SwiftUI?

问题

假设我有一个名为MainView的视图,其中包含一个按钮,当我点击该按钮时,我希望在另一个视图DataView上呈现一些数据。有两个要求:

  1. 在屏幕上切换DataView时应该有一个动画效果。
  2. 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:

  1. There should be an animation for transitioning DataView on the screen
  2. 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.

如何在SwiftUI中将`transition()`与`onAppear()`或`task()`一起用于视图?

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
        }
    }
}

huangapple
  • 本文由 发表于 2023年3月7日 08:22:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/75657006.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定