Reputation: 738
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
.
Upvotes: 13
Views: 7265
Reputation: 23
I had a similar requirement. Actually my requirement was that the text is a multiline text that scrolls up to reveal one line at a time ( timed with someone narrating the text. The background was an image.
I solved it the following way. A ZStack with the image first, then the Text layer with the multiline text positioned the way I want it, and then another layer of the image with a rectangular hole made where I want the text to appear through. The approach may meet your needs - you will need to position the hole, change colors etc. to meet your needs. The code shows a rectangle hole about three quarters of the way down the image.
struct TestView : View {
var body: some View {
GeometryReader { proxy in
ZStack {
Image("MyImage")
.resizable()
.scaledToFit()
Text("Multiline\ntext\nfor\nscrolling")
.font(.title).foregroundColor(.white)
.position(x: proxy.size.width * 0.5, y: proxy.size.height * 0.75 )
Image("MyImage")
.resizable()
.scaledToFit()
.mask(self.makeMask(for: proxy.size))
}
}
}
func makeMask(for sz : CGSize) -> some View {
return VStack(spacing: 0) {
Rectangle().fill(Color.black)
.frame(height: sz.height * 0.75 + 4)
Rectangle().fill(Color.clear)
.frame(height: 40)
Rectangle().fill(Color.black)
.frame(height: sz.height * 0.25 - 40)
}
}
}
Upvotes: 0
Reputation: 1358
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)
}
}
Upvotes: 5
Reputation: 908
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
Upvotes: 2