100 Days of SwiftUI Day 45

100 Days of SwiftUI – Day 45

We’ve arrived at day 45 of the 100 Days of SwiftUI! Yesterday, we covered CGAffineTransformImagePaintdrawingGroup() and more. Today, we’re learning about drawing special effects and animations. Let’s dive in!

Special effects in SwiftUI: blurs, blending, and more

SwiftUI gives us a lot of flexibility and control to create the effects we want, including the ability to apply real-time blurs, blend modes, saturation adjustment, and more. Here are a few examples:

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct ContentView: View {
    @State private var amount = 0.0
    var body: some View {
//        ZStack {
//            Image("black-sands")
//                .colorMultiply(.red)
//
//            Rectangle()
//                .fill(.red)
//                .blendMode(.multiply) .blendMode is used to define how views are drawn over each other.
        
        VStack {
            ZStack {
                Circle()
                    .fill(Color(red: 1, green: 0, blue: 0))
                    .frame(width: 200 * amount)
                    .offset(x: -50, y: -80)
                    .blendMode(.screen)
                
                Circle()
                    .fill(Color(red: 0, green: 1, blue: 0))
                    .frame(width: 200 * amount)
                    .offset(x: 50, y: -80)
                    .blendMode(.screen)
                
                Circle()
                    .fill(Color(red: 0, green: 0, blue: 1))
                    .frame(width: 200 * amount)
                    .blendMode(.screen)
                    
            }
            .frame(width: 300, height: 300)
            
            Slider(value: $amount)
                .padding()
            
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.black)
        .ignoresSafeArea()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct ContentView: View {
    
    @State private var amount = 0.0
    
    var body: some View {
        VStack {
            Image("black-sands")
                .resizable()
                .scaledToFit()
                .frame(width: 200, height: 200)
                .saturation(amount)
                .blur(radius: (1 - amount) * 20)
            
            Slider(value: $amount)
                .padding()
            
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.black)
        .ignoresSafeArea()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Animating simple shapes with animatableData

SwiftUI uses an animatableData property to let us animate changes to shapes.

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct Trapezoid: Shape {
    var insetAmount: Double
    
    var animatableData: Double {
        get {insetAmount}
        set {insetAmount = newValue}
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: CGPoint(x: 0, y: rect.maxY))
        path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: 0, y: rect.maxY))
        
        return path
    }
}

struct ContentView: View {
    @State private var insetAmount = 50.0
    
    var body: some View {
        Trapezoid(insetAmount: insetAmount)
            .frame(width: 200, height: 100)
            .onTapGesture {
                withAnimation{
                    insetAmount = Double.random(in: 10...90)
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

What’s happening here is quite complex: when we use withAnimation(), SwiftUI immediately changes our state property to its new value, but behind the scenes it’s also keeping track of the changing value over time as part of the animation. As the animation progresses, SwiftUI will set the animatableData property of our shape to the latest value, and it’s down to us to decide what that means – in our case we assign it directly to insetAmount, because that’s the thing we want to animate.

Remember, SwiftUI evaluates our view state before an animation was applied and then again after. It can see we originally had code that evaluated to Trapezoid(insetAmount: 50), but then after a random number was chosen we ended up with (for example) Trapezoid(insetAmount: 62). So, it will interpolate between 50 and 62 over the length of our animation, each time setting the animatableData property of our shape to be that latest interpolated value – 51, 52, 53, and so on, until 62 is reached.

Hacking with Swift, Paul Hudson (@twostraws)

Animating complex shapes with AnimatablePair

If we want to animate more than a single property inside our shape, we can’t use animatableData by itself. Instead, we need to use it with a wrapper called AnimatablePair.

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct Checkerboard: Shape {
    var rows: Int
    var columns: Int
    
    var animatableData: AnimatablePair<Double, Double> { // stores two values, Double and Double
        get {
            AnimatablePair(Double(rows), Double(columns)) // Type conversion. Our rows and columns are ints and are now being converted to doubles
        }

        
        set {
            rows = Int(newValue.first) // Converts the data back to an int before passing in the value
            columns = Int(newValue.second)
        }
    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        let rowSize = rect.height / Double(rows) // A row is the max height divided by the amount of rows we specify
        let columnsize = rect.width / Double(columns)  // A column is the max width divided by the amount of colums we specify
        
        for row in 0..<rows { // Loop through rows
            for column in 0..<columns {  // Loop through columns
                if (row + column).isMultiple(of: 2) { // If the result is a multiple of 2
                    let startX = columnsize * Double(column) // Specifies how the rows and columns are drawn from the start position
                    let startY = rowSize * Double(row)
                    
                    let rect = CGRect(x: startX, y: startY, width: columnsize, height: rowSize)
                    path.addRect(rect)
                }
            }
        }
        return path
    }
}


struct ContentView: View {
    
    @State private var rows = 4
    @State private var columns = 4
    
    var body: some View {
        Checkerboard(rows: rows, columns: columns)
            .onTapGesture {
                withAnimation(.linear(duration: 3)){
                    rows = 8
                    columns = 16
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wrap up

And that’s it for day 45! Tomorrow, we’ll reviewing what we learned over the course of the past 3 days and complete a challenge to see if we’ve mastered drawing in SwiftUI. Time to recharge!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 2 – Booleans & String Interpolation

100 Days of SwiftUI – Day 62

100 Days of SwiftUI – Day 83

100 Days of SwiftUI – Day 88