How to clip subviews using ZStack with SwiftUI? - ios

I need to clip the view behind a Text by using its Rectangle. When I add a Text over this 1-pixel height rectangle, I need it to "clip" the subview below, so the text can be readable.
Of course, if I use a solid background color, it's easy to do, as I just set it and it will clip the subview.
Here is a POC to test it:
struct test: View {
let gradient = Gradient(colors: [Color.blue, Color.purple])
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
.frame(width: 200, height: 200)
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: 100, height: 1, alignment: .center)
Text("XXXX")
.background(Color.green)
}
}
}
}
Any ideas? I don't think I can handle it using a mask.

Sometimes a solution might be not-to-do instead of to-do-but.
Here is a possible implementation of above principle to solve your issue.
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
.frame(width: 200, height: 200)
HStack(spacing: 0) {
Rectangle()
.fill(Color.white)
.frame(width: 30, height: 1, alignment: .center)
Text("XXXX")
Rectangle()
.fill(Color.white)
.frame(width: 30, height: 1, alignment: .center)
}
}

Related

How can I prevent users from scrolling past a certain point?

I have a ScrollView, when I scroll up, the content on the screen would move down. But I wanna limit how far the content can move down. I don't want the user to be able to scroll beyond the gray rectangle at the top. In other words, I don't want the user to see the white area above the gray rectangle when they scroll up. How can I achieve this in SwiftUI?
import SwiftUI
struct ScrollViewIssue: View {
var body: some View {
ScrollView {
VStack {
Rectangle()
.foregroundColor(.gray)
.frame(height: 200)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
}
}.ignoresSafeArea()
}
}
struct ScrollViewIssue: View {
var body: some View {
ScrollView {
VStack {
Rectangle()
.foregroundColor(.gray)
.frame(height: UIScreen.main.bounds.height)
.frame(height: 200, alignment: .bottom)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
Circle()
.frame(width: 300, height: 300)
}
}.ignoresSafeArea()
}
}

SwiftUI ZStack takes all screen height but should be fixed height

My code:
public var body: some View {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
Text("Text")
.padding(.bottom, 42)
.foregroundColor(.red)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.background(Color.red)
}
makes ZStack takes almost all screen height. But I expect it will take height from .frame() method.
I have a workaround for you, it's a bit messed up but works
public var body: some View {
ZStack {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.zIndex(0)
ZStack {
VStack {
VStack {
Text("Text")
.padding(.top, 42)
.foregroundColor(.red)
}
.frame(width: UIScreen.main.bounds.width, height: 182)
VStack {
Text("Your texts here")
}
.frame(width: UIScreen.main.bounds.width)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
.zIndex(1)
}
.background(Color.red)
}
I simply made your ellipse on another layer and text on the other.
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.zIndex(0)
The .zIndex(0) makes sure that the view is in the background.
ZStack {
VStack { // This VStack contains all your text
VStack { // First VStack
Text("Text")
.padding(.top, 42)
.foregroundColor(.red)
}
.frame(width: UIScreen.main.bounds.width, height: 182)
VStack { //Second VStack
Text("Your texts here")
}
.frame(width: UIScreen.main.bounds.width)
Spacer()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
.zIndex(1)
Here, the ZStack takes up the entire screen. We added a VStack which contains your texts.
The first VStack has your main label over the Ellipse, and its frame is hardcoded according to the height of the Ellipse (1/2 the height as the other half of the ellipse is outside the screen).
The second VStack starts from the end of our first VStack which was the functionality needed, finally added a spacer() so that the text is placed at the top rather than middle.
The zIndex(1) makes sure that is placed over the elements at zIndex(0)

View centered in superview with view on top of it in SwiftUI

I'm trying to achieve something that is quite easy in UIKit - one view that is always in in the center (image) and the second view (text) is on top of it with some spacing between two views. I tried many different approaches (mainly using alignmentGuide but nothing worked as I'd like).
code:
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack {
Text("Test")
.padding([.bottom], 20) // I want to define spacing between two views
Image(systemName: "circle")
.resizable()
.alignmentGuide(VerticalAlignment.center, computeValue: { value in
value[VerticalAlignment.center] + value.height
})
.frame(width: 20, height: 20)
}
}
.frame(width: 100, height: 100)
result:
As you can see image is not perfectly centered and it actually depends on the padding value of the Text. Is there any way to force vertical and horizontal alignment to be centered in the superview and layout second view without affecting centered view?
I think the “correct” way to do this is to define a custom alignment:
extension VerticalAlignment {
static var custom: VerticalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
return .init(CustomAlignment.self)
}
}
Then, tell your ZStack to use the custom alignment, and use alignmentGuide to explicitly set the custom alignment on your circle:
PlaygroundPage.current.setLiveView(
ZStack(alignment: .init(horizontal: .center, vertical: .custom)) {
Color.white
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
VStack {
Text("Test")
Circle()
.stroke(Color.white)
.frame(width: 20, height: 20)
.alignmentGuide(.custom, computeValue: { $0.height / 2 })
}
}
.frame(width: 300, height: 300)
)
Result:
You can center the Image by moving it to ZStack. Then apply .alignmentGuide to the Text:
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
Text("Test")
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + $0.height }
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
}
.frame(width: 100, height: 100)
}
}
Note that as you specify the width/height of the Image explicitly:
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
you can specify the .alignmentGuide explicitly as well:
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + 50 }
Here is possible alternate, using automatic space consuming feature
Tested with Xcode 12 / iOS 14
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack(spacing: 0) {
Color.clear
.overlay(
Text("Test").padding([.bottom], 10),
alignment: .bottom)
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
Color.clear
}
}
.frame(width: 100, height: 100)
}
}
Note: before I used Spacer() for such purpose but with Swift 2.0 it appears spacer becomes always just a spacer, ie. nothing can be attached to it - maybe bug.

SwiftUI Mask a rectangle inside a rounded rectangle

Hello there. I am wondering, in SwiftUI, how do you mask the contents of a rounded rectangle so that a child rectangle clips the corners.
In my example I have a white rounded rectangle and a pink rectangle on a zstack, I've tried to apply clipping, but the pink rectangle does not conform to the corners.
I've tried applying .mask to the white rectangle, but it gives different results to expectations (sometimes it doesn't show the pink rectangle).
I did find an example where you can set your own cornerRadius
Round Specific Corners SwiftUI
But I was wondering if perhaps there was a way to mask the internals/body of the pink rectangle so that it conforms to the parent's rounded rectangle?
My code follows;
var body: some View {
GeometryReader { geometry in
Color.gray
.edgesIgnoringSafeArea(.top)
.overlay(
ZStack (alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16,
style: .continuous)
.foregroundColor(.white)
.shadow(radius: 10)
// Tried using .mask here
Rectangle()
.fill(Color.pink)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 150, alignment: .top)
.clipped()
}
.frame(width: 300, height: 450, alignment: .center)
)
}
.edgesIgnoringSafeArea(.all)
}
Edit: To clarify:
The pink rectangle should remain as a rectangle, but clip the top left and right to match the parent white rounded rectangle.
If I correctly understood your goal, here is a solution - the only needed clip in right place is after internal content (two rectangles in this case) is constructed. So clipping with RoundedRectangle gives rounded corners around entire card. (As well as shadow most probably is needed to entire card, so placed at the end).
UPDATE: re-tested with Xcode 13.3 / iOS 15.4
ZStack (alignment: .topLeading) {
Rectangle()
.foregroundColor(.white)
Rectangle()
.fill(Color.pink)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 150, alignment: .top)
}
.clipShape(RoundedRectangle(cornerRadius: 16)) // << here !!
.frame(width: 300, height: 450, alignment: .center)
.shadow(radius: 10)
#Asperi already posted a great answer, I have done this aswell with using mask modifier in SwiftUI. Furthermore you only have to set cornerRadius once.
VStack(spacing: 0)
{
ZStack(alignment: .center)
{
Rectangle()
.fill(Color.red)
.frame(width: 66, height: 20)
}
ZStack(alignment: .center)
{
Rectangle()
.fill(Color.white)
.frame(width: 66, height: 46)
}
}
.mask(Rectangle()
.cornerRadius(3.0)
.frame(width: 66, height: 66)
)
check this out
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
Color.gray
.edgesIgnoringSafeArea(.top)
.overlay(
ZStack (alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16,
style: .continuous)
.foregroundColor(.white)
.shadow(radius: 10)
// Tried using .mask here
Rectangle()
.fill(Color.pink)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 150, alignment: .top)
.clipShape(RoundedRectangle(cornerRadius: 16)) // <<<<<<
}
.frame(width: 300, height: 450, alignment: .center)
)
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I think the easier way is to apply cornerradius to ZStack
ZStack (alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16,
style: .continuous)
.foregroundColor(.white)
.shadow(radius: 10)
// Tried using .mask here
Rectangle()
.fill(Color.pink)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 150, alignment: .top)
//.clipped() //<<= here
}
.frame(width: 300, height: 450, alignment: .center)
.cornerRadius(20) //<<= here

SwiftUI edges visible after using overlay

I'm trying to create rounded edges in one of my views using overlay.
.background(Color.gray.opacity(0.2))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
The issue is that i still can see the edges on the background and also the stroke. How to crop the edges?
Your view will not be tappable through overlay, even with transparency, so the solution is to use clip shape and background as shown below
struct DemoRoundRectView: View {
var body: some View {
Text("DEMO")
.frame(width: 100, height: 50)
.background(Color.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 10)) // clip corners
.background(
RoundedRectangle(cornerRadius: 10) // stroke border
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
}
}
struct ContentView: View {
var body: some View {
Rectangle()
.fill(Color.orange)
.frame(width: 200, height: 200, alignment: .center)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 1)
)
} }
try this.
It seems that right now your modifiers are something like:
.cornerRadius(10)
.background(Color.gray.opacity(0.2))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
Instead, you need to change the order of the view modifiers, as order matters! You are rounding the corners then applying a background, whereas you should be applying the background so that the corner radius can then clip that.
Try this instead:
.background(Color.gray.opacity(0.2))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)

Resources