ios - 如何让替换 View 淡入旧 View 而不是与它交叉淡入淡出?

标签 ios swiftui

默认情况下,SwiftUI 转换似乎使用 .opacity 因此当 View 在动画期间出现/消失时,它将淡入/淡出。这会创建一个交叉淡入淡出效果,这种淡入淡出效果通常非常好,但有时在一个重叠 View 替换另一个 View 时可能不受欢迎。

我有一种情况,我宁愿让新 View 淡入到即将被替换的 View 之上。旧 View 根本不应该改变不透明度,而只是在转换完成后消失。

我不希望在过渡期间看到旧 View 不是 100% 不透明度的任何点。例如,默认的处理方式会导致过渡中间的一个点,在该点我们会看到后 View 和前 View 的 50% 不透明度版本相互叠加。

明确地说,我已经有了一个解决方法:如果我使用 ZStack 始终保持背景 View ,我可以获得我想要的效果 - 这样只有新 View 淡入(因为它是唯一的 View 变化)。不过,我的解决方案感觉是错误的、浪费的和不雅的。 (尽管在实际图像加载后它完全不可见且不需要,但系统会不断合成背景 View 。我只希望该背景 View 存在直到转换完成,但我无法弄清楚如何让它做到这一点。)

这里有一些代码可以说明我的意思。顶部 View 使用默认过渡和淡入淡出显示,但代码符合我的预期 - 当新 View 存在时,我们使用它并且只使用它。底部以我想要的方式出现 - 但这样做的代价是始终将背景 View 保持在那里,以便只有新 View 在首次出现时才会消失:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    let transaction = Transaction(animation: .linear(duration: 10))
    let imageURL = URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/main_image_star-forming_region_carina_nircam_final-5mb.jpg")!
    
    var body: some View {
        VStack(spacing: 10) {
            AsyncImage(url: imageURL, transaction: transaction) { phase in
                if let img = phase.image {
                    img.resizable()
                } else {
                    Color.red
                }
            }
            .aspectRatio(CGSize(width: 3600, height: 2085), contentMode: .fit)

            AsyncImage(url: imageURL, transaction: transaction) { phase in
                ZStack {
                    Color.red
                    
                    if let img = phase.image {
                        img.resizable()
                    }
                }
            }
            .aspectRatio(CGSize(width: 3600, height: 2085), contentMode: .fit)
        }
        .frame(width: 500)
        .padding(10)
        .background(Color.yellow)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

因为我显然无法嵌入视频,所以这里有一个指向正在运行的 Playground 的链接,因为它有助于理解我在这里的意思,我认为:https://www.dropbox.com/s/scwxfoa9pojo0yq/SwiftUICrossfade.mov?dl=0

最佳答案

一种方法可能是将自定义过渡应用于背景 View 。我只是简单地测试了这个,但添加了这个后,顶部和底部 View 在转换过程中看起来是匹配的。

import SwiftUI
import PlaygroundSupport

struct AllOrNothingTransition: Animatable, ViewModifier {
    
    var animatableData: CGFloat = 0
    
    func body(content: Content) -> some View {
        // The goal is for `content` to remain in place, unchanged,
        // until the transition completes and it is removed, so we
        // simply pass it back unchanged regardless of the value
        // of `animatableData`.
        content
    }
}

extension AnyTransition {
    static var allOrNothing: AnyTransition {
        // For this use case, the specific values passed in as `animatableData`
        // are not important, as long as they differ from each other.
        // SwiftUI needs to have differing values to interpolate between,
        // otherwise it will assume there is nothing to animate and
        // remove the transitioning view immediately at the start of
        // of the transition (this is based on observation, not a knowledge
        // of the inner workings of SwiftUI).
        AnyTransition.modifier(
            active: AllOrNothingTransition(animatableData: 0),
            identity: AllOrNothingTransition(animatableData: 1)
        )
    }
}

struct ContentView: View {
    let transaction = Transaction(animation: .linear(duration: 10))
    let imageURL = URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/main_image_star-forming_region_carina_nircam_final-5mb.jpg")!
    
    var body: some View {
        VStack(spacing: 10) {
            AsyncImage(url: imageURL, transaction: transaction) { phase in
                
                if let img = phase.image {
                    img.resizable()
                } else {
                    Color.red
                        .transition(.allOrNothing)
                }
            }
            .aspectRatio(CGSize(width: 3600, height: 2085), contentMode: .fit)

            AsyncImage(url: imageURL, transaction: transaction) { phase in
                ZStack {
                    Color.red
                    
                    if let img = phase.image {
                        img.resizable()
                    }
                }
            }
            .aspectRatio(CGSize(width: 3600, height: 2085), contentMode: .fit)
        }
        .frame(width: 500)
        .padding(10)
        .background(Color.yellow)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

关于ios - 如何让替换 View 淡入旧 View 而不是与它交叉淡入淡出?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73409016/

相关文章:

ios - 核心数据 : How to check if value is compatible with attribute?

ios - Swift - 从字典数组中打印对象

objective-c - 两个 UIViewController 的故事

swift - IntentHandler 不适用于 widgetFamily

SwiftUI 和 Combine,如何创建可重用的发布者来检查字符串是否为空

ios - 带有 SwiftUI + 拖放重新排序的 UICollectionView 可能吗?

ios - 移动客户端不应该终止与API的连接吗?

ios - 读取重定向的 NSURLDataTask 的数据/响应/正文

swiftui - 在 SwiftUI 中将 SF Symbols 调整为相同大小看起来不正确

自定义控件中图层的 SwiftUI 背景颜色