Categories
Swift SwiftUI

Gradient text in SwiftUI

If we want to spice up the user interface, then we can make some titles in the app to use gradient colours. In WWDC’21 Apple introduced an API for making gradient text styles easy to create. The .foregroundStyle() view modifier takes in a type which conforms to ShapeStyle protocol. One of these types are gradient types in SwiftUI. Therefore, creating a fun gradient text is a matter of creating a text and applying a foregroundStyle view modifier with a gradient on it.

let gradientColors: [Color] = [.purple, .blue, .cyan, .green, .yellow, .orange, .red]
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.linearGradient(
colors: gradientColors,
startPoint: .leading,
endPoint: .trailing
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.ellipticalGradient(
colors: gradientColors
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.conicGradient(
colors: gradientColors,
center: .center
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.radialGradient(
colors: gradientColors,
center: .center,
startRadius: 50,
endRadius: 100
)
)
view raw Text.swift hosted with ❤ by GitHub

The new view modifier is great, but it is iOS 15+, macOS 12.0+. Another way for achieving this result is recreating it ourselves. This involves in overlaying the text with gradient and then masking it with the text.

Text("Hello, world!")
.font(.system(size: 60))
.myForegroundStyle(
LinearGradient(
colors: gradientColors,
startPoint: .leading,
endPoint: .trailing
)
)
.padding()
.background(.black)
extension View {
func myForegroundStyle<Content: View>(_ content: Content) -> some View {
self.overlay(content).mask(self)
}
}
view raw Mask.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS SpriteKit

Drawing gradients in SpriteKit

SpriteKitGradientTexture

I was working on an upcoming game in SpriteKit only to discover that adding a simple gradient is not so straight-forward as one would expect. Therefore I created an extension to SKTexture.

extension SKTexture
{
convenience init(radialGradientWithColors colors: [UIColor], locations: [CGFloat], size: CGSize)
{
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { (context) in
let colorSpace = context.cgContext.colorSpace ?? CGColorSpaceCreateDeviceRGB()
let cgColors = colors.map({ $0.cgColor }) as CFArray
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors, locations: UnsafePointer<CGFloat>(locations)) else {
fatalError("Failed creating gradient.")
}
let radius = max(size.width, size.height) / 2.0
let midPoint = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
context.cgContext.drawRadialGradient(gradient, startCenter: midPoint, startRadius: 0, endCenter: midPoint, endRadius: radius, options: [])
}
self.init(image: image)
}
convenience init(linearGradientWithAngle angleInRadians: CGFloat, colors: [UIColor], locations: [CGFloat], size: CGSize)
{
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { (context) in
let colorSpace = context.cgContext.colorSpace ?? CGColorSpaceCreateDeviceRGB()
let cgColors = colors.map({ $0.cgColor }) as CFArray
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors, locations: UnsafePointer<CGFloat>(locations)) else {
fatalError("Failed creating gradient.")
}
let angles = [angleInRadians + .pi, angleInRadians]
let radius = (pow(size.width / 2.0, 2.0) + pow(size.height / 2.0, 2.0)).squareRoot()
let points = angles.map { (angle) -> CGPoint in
let dx = radius * cos(angle) + size.width / 2.0
let dy = radius * sin(angle) + size.height / 2.0
return CGPoint(x: dx, y: dy)
}
context.cgContext.drawLinearGradient(gradient, start: points[0], end: points[1], options: [])
}
self.init(image: image)
}
}

This extension adds support for creating linear and radial gradients. Linear gradient can be drawn with an angle (in radians) although in most of the cases rotating SKSpriteNode would be enough. Gradients are drawn using core graphics APIs what are a little bit difficult to use but now nicely hidden in the extension. Both initialisers take in array of colors (UIColor) and CGFloat array with values in range of 0 to 1 defining the locations for colors in CGGradient.

For adding linear gradient or radial gradient to a SpriteKit scene we need to create a SKTexture and assign it to a SKSpriteNode. I created an example project SpriteKitGradientTexture for showing gradients in action. In the example project one SKSpriteNode is animating with textures containing linear gradients with different angles and the other SKSpriteNode just displays radial gradient.

final class GameScene: SKScene
{
override func didMove(to view: SKView)
{
let linearGradientSize = size
let linearGradientColors = [UIColor(red: 53.0 / 255.0, green: 92.0 / 255.0, blue: 125.0 / 255.0, alpha: 1.0),
UIColor(red: 108.0 / 255.0, green: 91.0 / 255.0, blue: 123.0 / 255.0, alpha: 1.0),
UIColor(red: 192.0 / 255.0, green: 108.0 / 255.0, blue: 132.0 / 255.0, alpha: 1.0)]
let linearGradientLocations: [CGFloat] = [0, 0.5, 1]
let textureCount = 8
let textures = (0..<textureCount).map { (index) -> SKTexture in
let angle = 2.0 * CGFloat.pi / CGFloat(textureCount) * CGFloat(index)
return SKTexture(linearGradientWithAngle: angle, colors: linearGradientColors, locations: linearGradientLocations, size: linearGradientSize)
}
let linearGradientNode = SKSpriteNode(texture: textures.first)
linearGradientNode.zPosition = 1
addChild(linearGradientNode)
let action = SKAction.animate(with: textures, timePerFrame: 0.5)
linearGradientNode.run(SKAction.repeatForever(action))
let radialGradientSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
let radialGradientColors = [UIColor.yellow, UIColor.orange]
let radialGradientLocations: [CGFloat] = [0, 1]
let radialGradientTexture = SKTexture(radialGradientWithColors: radialGradientColors, locations: radialGradientLocations, size: radialGradientSize)
let radialGradientNode = SKSpriteNode(texture: radialGradientTexture)
radialGradientNode.zPosition = 2
addChild(radialGradientNode)
let pulse = SKAction.sequence([SKAction.fadeIn(withDuration: 3.0), SKAction.fadeOut(withDuration: 1.0)])
radialGradientNode.run(SKAction.repeatForever(pulse))
}
}
view raw GameScene.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

SpriteKitGradientTexture (GitHub)