Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mask rectangle with a text in SwiftUI

I want to achieve following using SwiftUI:

masked rectangle

This is what I have tried:

Text("test").mask(Rectangle().frame(width: 200, height: 100).foregroundColor(.white))

Also the other way around:

Rectangle().frame(width: 200, height: 100).foregroundColor(.white).mask(Text("test"))

Both of those samples gave me the inverse result of what I wanted. Meaning that only the text was showing in white with the rectangle being "masked away".

I also thought of the alternative where I simply combine Text and Rectangle in a ZStack. The rectangle having the foreground color and the text the background color. This would result in the same effect. However I don't want to do this as this seems like a hack to me. For instance if I want to add a gradient or an image to the background this method wouldn't work very well.

Is there a good way on how to do this in SwiftUI? I wouldn't mind if it is through a UIViewRepresentable.

like image 591
ph1psG Avatar asked Oct 01 '19 06:10

ph1psG


People also ask

What is mask SwiftUI?

SwiftUI gives us the mask() modifier for masking one with another, which means you can mask an image using text or an image using an image, or more. For example, this creates a 300x300 image of stripes, then masks it using the text “SWIFT!”

What is SwiftUI overlay?

As mentioned earlier, SwiftUI has a built-in modifier for applying overlay named . overlay that can be attached to the existing image view. The requirement inside an overlay modifier is to place another object that will serve as the coating of the image.


Video Answer


2 Answers

Please refer to this anwser first, and then you'll understand the following code I made:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        
        // text used in mask
        let text = Text("Text")
            .font(.system(size: 80, weight: .black, design: .rounded))
            .scaledToFit()                   // center text in view
        
        // container
        return ZStack {
            // background color
            Color.white.grayscale(0.3)
            // text card
            Gradient.diagonal(.yellow, .green)      // my custom extension 
                .inverseMask(text)                    // ⭐️ inverse mask
                // shadow for text
                .shadow(color: Color.black.opacity(0.7), radius: 3, x: 3, y: 3)
                .frame(width: 300, height: 200)
                // highlight & shadow
                .shadow(color: Color.white.opacity(0.9), radius: 18, x: -18, y: -18)
                .shadow(color: Color.black.opacity(0.3), radius: 14, x:  14, y:  14)
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

and the result is:

enter image description here

The key extension used in the above code is .inverseMask():

import SwiftUI

extension View {
    // view.inverseMask(_:)
    public func inverseMask<M: View>(_ mask: M) -> some View {
        // exchange foreground and background
        let inversed = mask
            .foregroundColor(.black)  // hide foreground
            .background(Color.white)  // let the background stand out
            .compositingGroup()       // ⭐️ composite all layers
            .luminanceToAlpha()       // ⭐️ turn luminance into alpha (opacity)
        return self.mask(inversed)
    }
}

----[Edited]-----

My custom extension for Gradient:

import SwiftUI

extension Gradient {
    
    // general linear gradient ---------------------------
    
    public static func linear(
        from start: UnitPoint, 
        to     end: UnitPoint, 
        colors    : [Color]       // use array
    ) -> LinearGradient 
    {
        LinearGradient(
            gradient  : Gradient(colors: colors), 
            startPoint: start, 
            endPoint  : end
        )
    }
    
    public static func linear(
        from start: UnitPoint, 
        to     end: UnitPoint, 
        colors    : Color...     // use variadic parameter
    ) -> LinearGradient 
    {
        linear(from: start, to: end, colors: colors)
    }
    
    // specialized linear gradients ------------------------
    
    // top to bottom
    public static func vertical(_ colors: Color...) -> LinearGradient {
        linear(from: .top, to: .bottom, colors: colors)
    }
    
    // leading to trailing
    public static func horizontal(_ colors: Color...) -> LinearGradient {
        linear(from: .leading, to: .trailing, colors: colors)
    }
    
    // top leading to bottom trailing
    public static func diagonal(_ colors: Color...) -> LinearGradient {
        linear(from: .topLeading, to: .bottomTrailing, colors: colors)
    }
    
    // top leading to bottom trailing
    public static func diagonal2(_ colors: Color...) -> LinearGradient {
        linear(from: .bottomLeading, to: .topTrailing, colors: colors)
    }
}
like image 160
lochiwei Avatar answered Nov 02 '22 20:11

lochiwei


Actually, even if it may seems like an hack to you, it's how SwiftUI works.

You can avoid this "hack" by creating a custom view

An example could be:

public struct BackgroundedText: View {

    var first_color = Color.green
    var second_color = Color.white
    var text_color = Color.green

    var size = CGSize(width: 200, height: 100)
    var xOffset: CGFloat = 50
    var yOffset: CGFloat = 50

    var text = "Hello world!"

    init(_ txt: String, _ txt_color: Color, _ fColor: Color, _ sColor: Color, _ size: CGSize, _ xOff: CGFloat, _ yOff: CGFloat) {
        self.text = txt
        self.text_color = txt_color
        self.first_color = fColor
        self.second_color = sColor
        self.size = size
        self.xOffset = xOff
        self.yOffset = yOff
    }


    public var body: some View {
        ZStack{
            Rectangle()
                .frame(width: self.size.width,
                       height: self.size.height)
                .foregroundColor(self.first_color)

            Rectangle()
            .frame(width: self.size.width - xOffset,
                   height: self.size.height - yOffset)
            .foregroundColor(self.second_color)

            Text(self.text)
                .foregroundColor(self.text_color)

        }
    }
}

So you can use the view in this way:

struct ContentView: View {
    var body: some View {
        BackgroundedText("Hello", .green, .green, .white, CGSize(width: 200, height: 100), 50, 50)
    }
}

If you want, you can make the rectangle resize based on text inside

like image 39
Andrew21111 Avatar answered Nov 02 '22 21:11

Andrew21111