Animate background with color gradient in SwiftUI

Animating the background of an app with a color gradient in SwiftUI can be a great way to enhance your app. By animating a change in the color gradient of the background, you can make your app feel more dynamic and engaging. One way to achieve this effect is by using AngularGradient to keep the background moving. However, other color gradients could easily be used instead.

Related articles on color gradients in SwiftUI:



AngularGradient in a rectangle

Let's start with a simple AngularGradient such as that described in Simple AngularGradient in SwiftUI. The center point of the AngularGradient is animated to move through a list of UnitPoints every two seconds. The UnitPoints are relative to the frame containing the AngularGradient. The colors and the duration of the animation can be modified to achieve the desired effect.


 1struct AngularGradientView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 2,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 0.01, saturation: 0.2, brightness: 0.9),
 9                    Color(hue: 0.01, saturation: 1.0, brightness: 0.9)]
10    
11    let unitPoints: [UnitPoint] = [
12        .init(x:0.2, y: 0.8),
13        .init(x:0.5, y: 0.2),
14        .init(x:0.8, y: 0.9)]
15    
16    var body: some View {
17        VStack {
18            Rectangle()
19                .fill(
20                    AngularGradient(colors: oneColor,
21                                    center: unitPoints[centerPointIndex])
22                )
23                .animation(Animation.easeInOut.speed(0.2),
24                           value: centerPointIndex)
25                .frame(width: 400, height: 400)
26                .onReceive(timer) { input in
27                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
28                }
29        }
30        .padding()
31    }
32}

Animate moving the center of an AngularGradient in SwiftUI



Full background

Place the AngularGradient in a ZStack and modify the view to ignore the safe areas to use it as a background in SwiftUI. This will ensure that the gradient fills the entire screen and moves as the background.


 1struct FullBackgroundView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 2,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 0.01, saturation: 0.2, brightness: 0.9),
 9                    Color(hue: 0.01, saturation: 1.0, brightness: 0.9)]
10    
11    let unitPoints: [UnitPoint] = [
12        .init(x:0.2, y: 0.8),
13        .init(x:0.5, y: 0.2),
14        .init(x:0.8, y: 0.9)]
15    
16    var body: some View {
17        ZStack {
18            Rectangle()
19                .fill(
20                    AngularGradient(colors: oneColor,
21                                    center: unitPoints[centerPointIndex])
22                )
23                .animation(Animation.easeInOut.speed(0.20),
24                           value: centerPointIndex)
25                .onReceive(timer) { input in
26                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
27                }
28                .edgesIgnoringSafeArea(.all)
29        }
30    }
31}

Animate AngularGradient for entire background in SwiftUI



Purple color

Tweak the colors to use an array of alternating colors to create a star like pattern and adjust the time slightly.


 1struct PurpleBackgroundView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 3,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
 9                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
10                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
11                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
12                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
13                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
14                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0)]
15    
16    let unitPoints: [UnitPoint] = [
17        .init(x:0.2, y: 0.8),
18        .init(x:0.1, y: 0.3),
19        .init(x:0.9, y: 0.1),
20        .init(x:0.8, y: 0.9)]
21
22    var body: some View {
23        ZStack {
24            Rectangle()
25                .fill(
26                    AngularGradient(colors: oneColor,
27                                    center: unitPoints[centerPointIndex])
28                )
29                .backgroundStyle(.regularMaterial)
30                .animation(Animation.easeInOut.speed(0.08),
31                           value: centerPointIndex)
32                .onReceive(timer) { input in
33                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
34                }
35                .edgesIgnoringSafeArea(.all)
36        }
37    }
38}

Use alternating colors in AngularGradient for star effect in SwiftUI



Use Material

A blur effect can be applied over the animated AngularGradient with the use of Material in SwiftUI. This adds a layer over the background and tones down the animation so it is not too distracting. This does seem to hide the animation too much.


 1    struct MaterialBackgroundView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 3,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
 9                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
10                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
11                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
12                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
13                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
14                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0)]
15    
16    let unitPoints: [UnitPoint] = [
17        .init(x:0.2, y: 0.8),
18        .init(x:0.1, y: 0.3),
19        .init(x:0.9, y: 0.1),
20        .init(x:0.8, y: 0.9)]
21    
22    var body: some View {
23        ZStack {
24            Rectangle()
25                .fill(
26                    AngularGradient(colors: oneColor,
27                                    center: unitPoints[centerPointIndex])
28                )
29                .backgroundStyle(.regularMaterial)
30                .animation(Animation.easeInOut.speed(0.08),
31                           value: centerPointIndex)
32                .onReceive(timer) { input in
33                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
34                }
35                .edgesIgnoringSafeArea(.all)
36
37            Rectangle()
38                .background(.ultraThinMaterial)
39                .edgesIgnoringSafeArea(.all)
40        }
41    }
42}

Use material layer over the animated AngularGradient change in SwiftUI



Use Color with Opacity

An alternative way to tone down the animation is to place a color layer over the background and controll how much of the background is seen with the opacity setting.


 1struct ColorBackgroundView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 3,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
 9                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
10                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
11                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
12                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
13                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
14                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0)]
15    
16    let unitPoints: [UnitPoint] = [
17        .init(x:0.2, y: 0.8),
18        .init(x:0.1, y: 0.3),
19        .init(x:0.9, y: 0.1),
20        .init(x:0.8, y: 0.9)]
21    
22    var body: some View {
23        ZStack {
24            Rectangle()
25                .fill(
26                    AngularGradient(colors: oneColor,
27                                    center: unitPoints[centerPointIndex])
28                )
29                .backgroundStyle(.regularMaterial)
30                .animation(Animation.easeInOut.speed(0.08),
31                           value: centerPointIndex)
32                .onReceive(timer) { input in
33                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
34                }
35                .edgesIgnoringSafeArea(.all)
36
37            Rectangle()
38                .fill(.white.opacity(0.5))
39                .edgesIgnoringSafeArea(.all)
40        }
41    }
42}

Use color layer over the animated AngularGradient change in SwiftUI



Cards with material background

This is an example of series of cards, showing images, over the animated background that is behind a material.


 1struct MaterialCardsView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 3,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
 9                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
10                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
11                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
12                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0),
13                    Color(hue: 285.0/360.0, saturation: 0.4, brightness: 1.0),
14                    Color(hue: 285.0/360.0, saturation: 0.2, brightness: 1.0)]
15    
16    let unitPoints: [UnitPoint] = [
17        .init(x:0.2, y: 0.8),
18        .init(x:0.1, y: 0.3),
19        .init(x:0.9, y: 0.1),
20        .init(x:0.8, y: 0.9)]
21    
22    let imageList = ["castle1", "mountain4", "mountain1", "mountain2", "mountain3", "castle2", "mountain5"]
23    
24    var body: some View {
25        ZStack {
26            Rectangle()
27                .fill(
28                    AngularGradient(colors: oneColor,
29                                    center: unitPoints[centerPointIndex])
30                )
31                .backgroundStyle(.regularMaterial)
32                .animation(Animation.easeInOut.speed(0.08),
33                           value: centerPointIndex)
34                .onReceive(timer) { input in
35                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
36                }
37                .edgesIgnoringSafeArea(.all)
38            
39            Rectangle()
40                .background(.ultraThinMaterial)
41                .edgesIgnoringSafeArea(.all)
42            
43            ScrollView {
44                VStack(spacing: 30) {
45                    ForEach(0..<imageList.count, id: \.self) { index in
46                        CardView(imageName: imageList[index])
47                            .frame(width: 350, height: 200)
48                    }
49                    Spacer()
50                }
51                .padding()
52            }
53        }
54    }
55}

Display cards on material over animated background



Cards with color background

This is an example of series of cards, showing images, over the animated background that is behind an opaque color layer. The use of opacity can be a great way to control how much of the animation is seen.


 1struct ColorWithCardsView: View {
 2    @State private var centerPointIndex = 0
 3    
 4    let timer = Timer.publish(every: 3,
 5                              on: .main,
 6                              in: .common).autoconnect()
 7    
 8    let oneColor = [Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
 9                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
10                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
11                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
12                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0),
13                    Color(hue: 285.0/360.0, saturation: 0.9, brightness: 1.0),
14                    Color(hue: 285.0/360.0, saturation: 0.7, brightness: 1.0)]
15    
16    let unitPoints: [UnitPoint] = [
17        .init(x:0.2, y: 0.8),
18        .init(x:0.1, y: 0.3),
19        .init(x:0.9, y: 0.1),
20        .init(x:0.8, y: 0.9)]
21    
22    let imageList = ["castle1", "mountain4", "mountain1", "mountain2", "mountain3", "castle2", "mountain5"]
23    
24    var body: some View {
25        ZStack {
26            Rectangle()
27                .fill(
28                    AngularGradient(colors: oneColor,
29                                    center: unitPoints[centerPointIndex])
30                )
31                .backgroundStyle(.regularMaterial)
32                .animation(Animation.easeInOut.speed(0.08),
33                           value: centerPointIndex)
34                .onReceive(timer) { input in
35                    centerPointIndex = (centerPointIndex + 1) % unitPoints.count
36                }
37                .edgesIgnoringSafeArea(.all)
38
39            Rectangle()
40                .fill(.white.opacity(0.5))
41                .edgesIgnoringSafeArea(.all)
42            
43            
44            ScrollView {
45                VStack(spacing: 30) {
46                    ForEach(0..<3, id: \.self) { index in
47                        CardView(imageName: imageList[index])
48                            .frame(width: 350, height: 200)
49                    }
50                    Spacer()
51                }
52                .padding()
53            }
54        }
55    }
56}

Display cards on color over animated background




Conclusion

Animating a change in the color gradient of the background can make your app feel more dynamic and engaging. This article demonstrated one way to achieve this effect is by using AngularGradient to keep the background moving. There is a balance between animation to make the app engaging and animation that can be distracting. A Material layer can be used to tone down the background, but this seems to hide the background too much. A color layer seems a better fit with control of how much of the background animation is shown controlled by the opacity level.

The source code for ColorGradientApp is available on GitHub.