• Dec 21, 2024

Mastering Mask Modifier in SwiftUI


SwiftUI’s mask modifier is used to mask one view with another. This modifier is used for masking the original view. When a mask is applied to a view, all the pixels that are transparent in the mask get hidden in the original view.

Let’s learn with an example. We start with a LinearGradient view.


struct ContentView: View {
    var body: some View {
            LinearGradient(
                gradient: Gradient(colors: [.indigo, .orange]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .frame(height: 300)
        }
}

Next, we will add a mask modifier with a Text view.

struct ContentView: View {
    var body: some View {
            LinearGradient(
                gradient: Gradient(colors: [.indigo, .orange]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .mask {
                Text("Hello DevTechie!")
                    .font(.custom("Noteworthy Bold", size: 40))
            }
            .frame(height: 300)
        }
}

Mask’s content can contain any type of SwiftUI view. Let’s draw border around the Text view.

struct ContentView: View {
    var body: some View {
            LinearGradient(
                gradient: Gradient(colors: [.indigo, .orange]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .mask {
                Text("Hello DevTechie!")
                    .font(.custom("Noteworthy Bold", size: 40))
                    .padding(20)
                    .border(.primary, width: 10.0)
            }
            .frame(height: 300)
        }
}

Mask modifier takes alignment parameter as an argument. If not specified then the alignment is at the center of the screen but we can update it to be any Alignment type. The alignment for mask is applied in relation to the main view.

struct ContentView: View {
    var body: some View {
            LinearGradient(
                gradient: Gradient(colors: [.indigo, .orange]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .mask(alignment: .topLeading) {
                Text("Hello DevTechie!")
                    .font(.custom("Noteworthy Bold", size: 40))
                    .padding(20)
                    .border(.primary, width: 10.0)
            }
            .frame(height: 300)
        }
}

We can also animate between different values of mask alignments.

struct ContentView: View {
    let alignments = [Alignment.leading, .trailing, .top, .bottom, .topLeading, .topTrailing, .bottomTrailing , .bottomLeading]
    @State private var alignment = Alignment.leading
    var body: some View {
            LinearGradient(
                gradient: Gradient(colors: [.indigo, .orange]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .mask(alignment: alignment) {
                Text("Hello DevTechie!")
                    .font(.custom("Noteworthy Bold", size: 40))
                    .padding(20)
                    .border(.primary, width: 10.0)
            }
            .onTapGesture {
                withAnimation {
                    alignment = alignments.randomElement() ?? .leading
                }
            }
            .frame(height: 300)
        }
}

Let’s build a bit complex UI with this knowledge in hand. We will start by creating a custom shape to draw star with variable corners and smoothness.

struct Star: Shape {
    let corners: Int
    let smoothness: Double
    
    func path(in rect: CGRect) -> Path {
        guard corners >= 2 else { return Path() }
        
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        var currentAngle = -CGFloat.pi / 2
        let angleAdjustment = .pi * 2 / Double(corners * 2)
        let innerX = center.x * smoothness
        let innerY = center.y * smoothness
        
        var path = Path()
        path.move(to: CGPoint(x: center.x * cos(currentAngle) + center.x,
                              y: center.x * sin(currentAngle) + center.y))
        
        for corner in 0..<corners * 2 {
            let sinAngle = sin(currentAngle)
            let cosAngle = cos(currentAngle)
            let x: Double
            let y: Double
            
            if corner.isMultiple(of: 2) {
                x = center.x * cosAngle + center.x
                y = center.x * sinAngle + center.y
            } else {
                x = innerX * cosAngle + center.x
                y = innerY * sinAngle + center.y
            }
            
            path.addLine(to: CGPoint(x: x, y: y))
            currentAngle += angleAdjustment
        }
        
        path.closeSubpath()
        return path
    }
}

Let’s use this star inside the content view and animate the shape of the star

struct ContentView: View {
    @State private var isAnimating = false
    @State private var scale = 1.0
    
    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [.blue, .indigo, .mint]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
            
            VStack(spacing: 20) {
                Circle()
                    .fill(
                        LinearGradient(
                            gradient: Gradient(colors: [.purple, .mint, .pink]),
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                    )
                    .frame(width: 300, height: 300)
                    .mask {
                        ZStack {
                            ForEach(0..<6) { index in
                                Star(corners: 5, smoothness: 0.7)
                                    .frame(width: 300, height: 300)
                                    .rotationEffect(.degrees(isAnimating ? Double(index) * 60 : 0))
                            }
                        }
                    }
                    .scaleEffect(scale)
                    
                Text("DevTechie")
                    .font(.system(size: 48, weight: .bold))
                    .foregroundStyle(
                        LinearGradient(
                            colors: [.mint, .pink],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
            }
            .onAppear {
                withAnimation(.linear(duration: 20).repeatForever(autoreverses: false)) {
                    isAnimating = true
                }
                withAnimation(.easeInOut(duration: 2).repeatForever()) {
                    scale = 0.2
                }
            }
        }
    }
}