Time to work our ViewModifier muscles! In this week's tutorial we're going to put a twist on the classic dissolve effect!

Dissolve Effect using a Rectangle

Before getting started, please consider subscribing using this link, and if you aren't reading this on TrailingClosure.com, please come check us out sometime!

Getting Started

In the example above I showed what it looks like when a Rectangle is used as a mask for the dissolve effect. However, in this tutorial we're going to create a ViewModifier that accepts any View as the shape it uses to create the dissolve effect. Stay tuned to see how powerful this effect really is!

  1. Create a new ViewModifier named ShapeDissolveModifier. Below is the boilerplate for our struct. There are two variables defined. The first is our mask for the dissolve effect. Our Mask type is defined above as a Generic on the ShapeDissolveModifier. The second is the progress of the dissolve animation. As this increases from 0 to 1, the view will dissolve.
struct ShapeDissolveModifier<Mask: View>: ViewModifier {
    
    let mask: Mask
    var progress: Double
    
    func body(content: Content) -> some View {
        content
    }
}
  1. Create a new function buildMask(GeometryProxy, Double) -> some View. This function's job is to take the mask template we defined earlier, mask, and create 100 copies of varying opacity. These copies combine to create a full mask which will dissolve over time.
func buildMask(geometry: GeometryProxy, progress: Double) -> some View {
    // Create Dissolve Mask here...
}
  1. Within the function we use the GeometryProxy to calculate the size of the mask piece copies.
func buildMask(geometry: GeometryProxy, progress: Double) -> some View {
    
    let width = geometry.size.width
    let height = geometry.size.height

    let wUnit = width/10.0
    let hUnit = height/10.0

    // resize the mask to 1/10th of the parent view.
    let maskPiece = mask
        .frame(width: wUnit, height: hUnit, alignment: .center)
}
  1. Next we'll need to create three closures to allow us to generate the x, y, and opacity values for the mask copies.
func buildMask(geometry: GeometryProxy, progress: Double) -> some View {
    
    let width = geometry.size.width
    let height = geometry.size.height

    let wUnit = width/10.0
    let hUnit = height/10.0

    // resize the mask to 1/10th of the parent view.
    let maskPiece = mask
        .frame(width: wUnit, height: hUnit, alignment: .center)
    
    // Calculate X coordinate for a mask copy
    let xCoord = { (x:Int) -> CGFloat in
        wUnit * CGFloat(x)
    }
    
    // Calculate Y coordinate for a mask copy
    let yCoord = { (y:Int) -> CGFloat in
        hUnit * CGFloat(y)
    }
    
    // Calculate a random opacity for a mask copy
    let opacity = { () -> Double in
        return Double.random(in: 0...3) * progress + progress
    }
    
}
  1. Now to put them all together. We'll use ForEach to generate 100 mask pieces with the appropiate values using the closures we just made.
func buildMask(geometry: GeometryProxy, progress: Double) -> some View {
    
    let width = geometry.size.width
    let height = geometry.size.height

    let wUnit = width/10.0
    let hUnit = height/10.0

    // resize the mask to 1/10th of the parent view.
    let maskPiece = mask
        .frame(width: wUnit, height: hUnit, alignment: .center)
    
    // Calculate X Coordinate for a mask copy
    let xCoord = { (x:Int) -> CGFloat in
        wUnit * CGFloat(x)
    }
    
    // Calculate Y Coordinate for a mask copy
    let yCoord = { (y:Int) -> CGFloat in
        hUnit * CGFloat(y)
    }
    
    // Calculate Random Opacity for a mask copy
    let opacity = { () -> Double in
        return Double.random(in: 0...3) * progress + progress
    }
    
    // Combine all of the mask pieces together
    let fullMask = Group {
        ForEach(0..<100) { x in
            maskPiece
                .offset(x: xCoord(x%10), y: yCoord(x/10))
                .opacity(opacity())
        }
    }

    return fullMask
}

Usage Examples!

Now here is the fun part. I've gone ahead and put together a few different examples. Mostly it's just me playing around with it, but you'll get glimpse at some neat uses.

These are different effects based off of just changing the mask being passed into the ShapeDissolveModifier.

Rectangle()
    .foregroundColor(.blue)
    .frame(width: 300, height: 300, alignment: .center)
    .cornerRadius(10)
    .modifier(ShapeDissolveModifier(mask:
        Rectangle()
    ,progress: progress))
    .onAppear {
        withAnimation(Animation.easeInOut(duration: 3.0)) {
            self.progress = 1.0
        }
    }
Rectangle Mask
Rectangle()
    .scaleEffect(0.9)

Use it on Images

Image("mountains")
    .resizable()
    .scaledToFill()
    .foregroundColor(.blue)
    .frame(width: 300, height: 300, alignment: .center)
    .cornerRadius(10)
    .modifier(ShapeDissolveModifier(mask:
        Rectangle()
    ,progress: progress))
    .onAppear {
        withAnimation(Animation.easeInOut(duration: 3.0)) {
            self.progress = 1.0
        }
    }
Triangle()
    .rotation(Angle(degrees: 90))
    .scaleEffect(3)
Circle()
    .scaleEffect(1.5)

Support Future Tutorials Like This One!

Please consider subscribing using this link. If you aren't reading this on TrailingClosure.com, please come check us out sometime!

We want to see your work! If you've built something using this tutorial, send us pics! Find us on Twitter @TrailingClosure, or email us at [email protected]