简体   繁体   中英

How do I get a replacement view to fade in over top of the old one rather than crossfading with it?

By default, SwiftUI transitions seem to use .opacity so when a view is appearing/disappearing during an animation it will fade in/out. This creates a crossfade that is usually quite nice but can sometimes be undesirable when one overlapping view is replacing another.

I have a situation where I'd rather have the new view fade in over top of the view that is about to be replaced. The old view should not change opacity at all but simply disappear when the transition is finished.

I do not want there to be any point during the transition where I'm seeing the old view at anything but 100% opacity. For example, the default way this is handled results in a point in the middle of the transition where where we're seeing 50% opacity versions of both the back and front view composited over top of each other.

To be clear, I have one workaround already: I can get the effect I want if I use a ZStack to keep the background view there at all times - this way only the new view fades in (since it's the only view that changes). My solution feels wrong, wasteful, and inelegant, though. (The view in the background is going to get composited by the system constantly despite it being totally invisible and unwanted after the actual image loads in. I only want that background view to exist until the transition is complete, but I can't figure out how to make it do that.)

Here's some code that shows what I mean. The top view appears using the default transition and crossfading but with the code the way I'd expect - when the new view exists, we use it and only it. The bottom appears the way I want it to - but it does so at the expense of keeping the background view there at all times so that only the new view fades when it first appears:

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())

Since I apparently cannot embed a video, here's a link to one of the playground running since it helps to see it to make sense of what I mean here, I think: https://www.dropbox.com/s/scwxfoa9pojo0yq/SwiftUICrossfade.mov?dl=0

One way might be to apply a custom Transition to the background view. I only tested this briefly but with this added it looks like the top and bottom views match during their transitions.

import SwiftUI
import PlaygroundSupport

struct AllOrNothingTransition: Animatable, ViewModifier {
    
    var animatableData: CGFloat = 0
    
    func body(content: Content) -> some View {
        content
            .opacity(animatableData == 0 ? 0 : 1)
    }
}

extension AnyTransition {
    static var allOrNothing: AnyTransition {
        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())

If you dislike using ZStack , consider the combination of Color + .overlay .

In this case, Color can be used as both background color and a placeholder.

import PlaygroundSupport
import SwiftUI

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) {
      Color.red // `Color` as both background and a placeholder
        .aspectRatio(CGSize(width: 3600, height: 2085), contentMode: .fit)
        .overlay(
          AsyncImage(url: imageURL, transaction: transaction) { phase in
            if let img = phase.image {
              img.resizable()
            }
          }
        )
    }
    .frame(width: 500)
    .padding(10)
    .background(Color.yellow)
  }
}

PlaygroundPage.current.setLiveView(ContentView())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM