We’ve arrived at day 45 of the 100 Days of SwiftUI! Yesterday, we covered CGAffineTransform
, ImagePaint
, drawingGroup()
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 theanimatableData
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 toinsetAmount
, 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
Hacking with Swift, Paul Hudson (@twostraws)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 theanimatableData
property of our shape to be that latest interpolated value – 51, 52, 53, and so on, until 62 is reached.
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!
100 Days of SwiftUI – Day 45