I want to achieve following using SwiftUI:
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
.
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!”
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.
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:
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)
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With