Noise Grain Overlay
A view modifier that lays a subtle animated film grain over any view.

- Effects
- iOS 18.0+
- intermediate
- Origin
- Original work
- Found by
- noise · grain · film grain · texture · overlay · modifier · ruido · grano · grano de película · textura · superposición · modificador
Description
Section titled “Description”Adds a subtle animated film grain on top of this view.
struct NoiseGrainOverlayDemo: View { var body: some View { ZStack { LinearGradient(colors: [.orange, .pink, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing) .noiseGrainOverlay(intensity: 0.22) .ignoresSafeArea()
VStack(alignment: .leading, spacing: 10) { Image(systemName: "film") .font(.largeTitle) .accessibilityHidden(true) Text("Golden Hour") .font(.system(.title, design: .serif, weight: .bold)) Text("Grain takes the digital edge off flat color.") .font(.subheadline) .foregroundStyle(.white.opacity(0.85)) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, minHeight: 260, alignment: .bottomLeading) .background(Color(red: 0.16, green: 0.12, blue: 0.3)) .noiseGrainOverlay(intensity: 0.15) .clipShape(.rect(cornerRadius: 28)) .shadow(color: .black.opacity(0.35), radius: 24, y: 12) .padding(28) } }}API reference
Section titled “API reference”noiseGrainOverlay(intensity:grainSize:)
| Parameter | Type | Default |
|---|---|---|
intensity | Double | 0.16 |
grainSize | Double | 1.25 |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A subtle film grain over any view: tiny light and dark specks redrawn a few times a second.////// With Reduce Motion on, the grain is still.struct NoiseGrainOverlay: ViewModifier { var intensity: Double = 0.16 var grainSize: Double = 1.25 var framesPerSecond: Double = 12
@Environment(\.accessibilityReduceMotion) private var reduceMotion
/// A tiny seeded generator: the same frame number always gives the same grain. private struct Generator: RandomNumberGenerator { var state: UInt64
mutating func next() -> UInt64 { state ^= state << 13 state ^= state >> 7 state ^= state << 17 return state } }
func body(content: Content) -> some View { content.overlay { TimelineView(.animation(minimumInterval: 1 / framesPerSecond, paused: reduceMotion)) { timeline in Canvas { context, size in let frame = reduceMotion ? 0 : UInt64(timeline.date.timeIntervalSinceReferenceDate * framesPerSecond) drawGrain(frame: frame, in: context, size: size) } } .allowsHitTesting(false) .accessibilityHidden(true) } }
/// Every speck goes into one of two paths, so a frame is only two fills. private func drawGrain(frame: UInt64, in context: GraphicsContext, size: CGSize) { var generator = Generator(state: (frame &+ 1) &* 0x9E37_79B9_7F4A_7C15) var light = Path() var dark = Path()
for index in 0..<min(Int(size.width * size.height / 18), 20_000) { let speck = CGRect( x: Double.random(in: 0..<max(size.width, 1), using: &generator), y: Double.random(in: 0..<max(size.height, 1), using: &generator), width: grainSize, height: grainSize ) if index.isMultiple(of: 2) { light.addRect(speck) } else { dark.addRect(speck) } }
context.fill(light, with: .color(.white.opacity(intensity))) context.fill(dark, with: .color(.black.opacity(intensity))) }}
extension View { /// Adds a subtle animated film grain on top of this view. func noiseGrainOverlay(intensity: Double = 0.16, grainSize: Double = 1.25) -> some View { modifier(NoiseGrainOverlay(intensity: intensity, grainSize: grainSize)) }}
// MARK: - Demo
struct NoiseGrainOverlayDemo: View { var body: some View { ZStack { LinearGradient(colors: [.orange, .pink, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing) .noiseGrainOverlay(intensity: 0.22) .ignoresSafeArea()
VStack(alignment: .leading, spacing: 10) { Image(systemName: "film") .font(.largeTitle) .accessibilityHidden(true) Text("Golden Hour") .font(.system(.title, design: .serif, weight: .bold)) Text("Grain takes the digital edge off flat color.") .font(.subheadline) .foregroundStyle(.white.opacity(0.85)) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, minHeight: 260, alignment: .bottomLeading) .background(Color(red: 0.16, green: 0.12, blue: 0.3)) .noiseGrainOverlay(intensity: 0.15) .clipShape(.rect(cornerRadius: 28)) .shadow(color: .black.opacity(0.35), radius: 24, y: 12) .padding(28) } }}
#Preview { NoiseGrainOverlayDemo()}