.contentShape losing effect when overlayed - swiftui

I'm finding .contentShape isn't working the same when it is on top of other shapes that also accept gesture interactions.
Take the following example code:
ZStack {
// Circle()
// .offset(y: -200)
// .gesture(TapGesture().onEnded { print("TAPPED CIRCLE") } )
let path = Path() { path in
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: 1000, y: 1000))
}
path
.stroke(Color.blue, style: StrokeStyle(lineWidth: 2))
.contentShape(path.stroke(style: StrokeStyle(lineWidth: 10)))
.gesture(TapGesture().onEnded { print("TAPPED PATH") })
}
With this code, as is, the contentShape on the path will make taps recognized from a significant distance.
Uncommenting the code for the Circle, suddenly the contentShape doesn't have the same effect and now taps need to be exactly on the path for them to work.
Why is this happening? Can I somehow get the same behaviour even when the "Circle code" is present?

I did an extra little bit of debugging and realized it was working fine. The behaviour is different, though, depending on whether the circle is behind or not.
Here is my extra "debugging":
ZStack {
Circle()
.offset(y: -200)
.gesture(TapGesture().onEnded { print("TAPPED CIRCLE") } )
let path = Path() { path in
path.move(to: CGPoint(x: 250, y: 0))
path.addLine(to: CGPoint(x: 250, y: 1000))
}
path
.stroke(Color.green, style: StrokeStyle(lineWidth: 40))
path
.stroke(Color.blue, style: StrokeStyle(lineWidth: 2))
.contentShape(path.stroke(style: StrokeStyle(lineWidth: 40)))
.gesture(TapGesture().onEnded { print("TAPPED PATH") })
}
All this is doing is adding an extra path, drawing with a stroke the same lineWidth as the contentShape so I can see where the clicks are changing from registering and not registering on the path.
Where the path is over the circle, the hit detection is actually better than where it is not.
It seems that when there isn't a shape underneath, also accepting gesture interaction, there is an additional buffer to where the gestures are recognized.
Losing the buffer, and hence changing the behaviour, is what was making me think there was a problem.

Related

DragGesture on Path in ScrollView doesn't work

Im working on a macOS app. It should be possible to move Shapes and Lines around in a ScrollView. Moving Shapes like Circle is working. But when I try to move or tap a Path (for drawing a Line), the drag gesture is not working at all.
When the ScrollView is removed, it's possible to move the Path around. But I need it in a ScrollView.
See code below
struct ContentView: View
{
#State private var offset = CGSize.zero
#State private var x: CGFloat = 0
#State private var y: CGFloat = 0
var body: some View
{
ScrollView( [.vertical, .horizontal])
{
ZStack
{
Path
{ path in
path.move(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 300, y: 300))
}
.stroke(.blue ,style: StrokeStyle(lineWidth: 5, lineCap: .round, dash: []))
.offset(x: offset.width+x, y: offset.height+y)
.simultaneousGesture( DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged
{ gesture in
offset = gesture.translation
}
.onEnded
{ _ in
x += offset.width
y += offset.height
offset = CGSize.zero
}
)
}
}
.frame(width: 800, height: 800)
}
}
When I replace the Path with a circle in the code, everything works fine.
Circle()
.foregroundColor(.blue)
.frame(width: 50, height: 50)
Replacing simultaneousGesture with gesture or highPriorityGesture doesn't change anything.
Is there a solution to drag a Path within a ScrollView? Or is there another approach to create Lines and drag them around in a ScrollView.

Clip image with semi-circle mask?

I'm trying to add a circle background for an image, then clip the image using a semi-circle of the background. This is what I'm trying to end up with:
In SwiftUI, I put together something but stuck on what clipShape should I use:
Circle()
.fill(Color.blue)
.aspectRatio(contentMode: .fit)
.frame(width: 48)
.padding(.top, 8)
.overlay(
Image("product-sample1")
.resizable()
.scaledToFit()
//.clipShape(???)
)
This is what it looks like:
I'm not sure how to achieve this but was given a clue to use this approach:
How can this be achieved in SwiftUI, or is there a better approach to doing this?
You can create own shape. Below is a simple demo variant (for square content).
private struct DemoClipShape: Shape {
func path(in rect: CGRect) -> Path {
Path {
$0.move(to: CGPoint.zero)
$0.addLine(to: CGPoint(x: rect.maxX, y: 0))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
$0.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.midY, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
$0.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.midY, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
$0.addLine(to: CGPoint.zero)
}
}
}
And here is a sample of how it could be applied - due to padding we need to offset clipping (mask) to compensate locations (also it can be passed as constant into shape constructor, which will be more correct and accurate, but I leave it for you):
// used larger values for better visibility
Circle()
.fill(Color.blue)
.frame(width: 148, height: 148)
.padding(.top, 18)
.padding(.bottom, 10)
.overlay(
Image("product-sample1")
.resizable()
.scaledToFit()
)
.mask(DemoClipShape().offset(x: 0, y: -11)) // << here !!
gives

Custom shape registers taps outside drawn path

I created a custom Arc shape in SwiftUI, but when I add an onTapGesture modifier to it, it registers taps outside the area I drew for the Arc (it registers taps anywhere within the rect used to draw the Arc). How can I make sure the taps only register within the drawn path of the Arc, not the entire rect?
Here's the Arc shape I drew:
struct Arc: Shape {
let angle: Angle
let thickness: CGFloat
let clockwise: Bool
func path(in rect: CGRect) -> Path {
var path = Path()
let innerArcEndPoint = CGPoint(x: rect.maxX - thickness, y: 0)
let innerArcStartPoint = CGPoint(
x: innerArcEndPoint.x * cos(CGFloat(angle.radians)),
y: innerArcEndPoint.x * sin(CGFloat(angle.radians)))
path.move(to: innerArcStartPoint)
path.addArc(center: rect.origin, radius: innerArcEndPoint.x, startAngle: angle, endAngle: Angle.zero, clockwise: !clockwise)
path.addLine(to: CGPoint(x: rect.maxX, y: 0))
path.addArc(center: rect.origin, radius: rect.maxX, startAngle: Angle.zero, endAngle: angle, clockwise: clockwise)
path.closeSubpath()
return path
}
}
Here's me using the Arc with an onTapGesture modifier:
struct TestTappingArc: View {
var body: some View {
Arc(angle: Angle(degrees: 45), thickness: 20, clockwise: false)
.foregroundColor(.blue) // The only area I want to be tappable
.background(Color.orange) // The area I DON'T want to be tappable (but currently is tappable)
.frame(width: 100, height: 100)
.onTapGesture {
print("hello")
}
}
}
And here's what it looks like on screen:
You need to use contentShape with same shape to restrict hit-testing area (because view is always rectangular), like
Arc(angle: Angle(degrees: 45), thickness: 20, clockwise: false)
.foregroundColor(.blue)
.contentShape(Arc(angle: Angle(degrees: 45), thickness: 20, clockwise: false))
.onTapGesture { // << here !!
print("hello")
}
.background(Color.orange)
.frame(width: 100, height: 100)
Thanks Asperi for that answer--that does fix the problem. However, I realized an even simpler solution is just to move the background to after the onTapGesture modifier, like this:
Arc(angle: Angle(degrees: 45), thickness: 20, clockwise: false)
.foregroundColor(.blue)
.frame(width: 100, height: 100)
.onTapGesture {
print("hello")
}
.background(Color.orange)

SwiftUI: Rotated VStack results in aliased ("jaggy") drawing

I have a View with a VStack containing a Rectangle and a Spacer. The view is rotated with the rotationEffect() modifier.
The result looks bad as it is not antialiased.
How can I rotate the vertical stack, but having the output antialiased as I expect?
import SwiftUI
struct TestView: View {
var body: some View {
ZStack {
RectangleView()
}.frame(width: 300, height: 300)
}
}
struct RectangleView: View {
var body: some View {
VStack {
Rectangle()
.fill(.green)
.frame(width: 2, height: 200)
Spacer()
}.rotationEffect(Angle(degrees: 130))
}
}
In my experience, this is a product of using Rectangle() and similar default shapes, specifically when filling them. I can't attest to the precise reason why, but I do know that this problem is best solved with Paths instead.
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 200))
}
.stroke(Color.green, lineWidth: 2.0)
Here's a way you could do this inline. If you want a reusable structure though, do something like this:
struct Thingy: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
// you can use the rect properties to center things this way
return path
}
}
With either approach, rotating them should no longer cause this jaggedness.
It may be possible to achieve the same result when applying strokes instead of fills and foregroundColors, but Paths were more suited to my needs anyway, so that is where I found my solution to this problem. I haven't run these exact code snippets, but I can attest to their efficacy as I've encountered the same frustration in my own projects.
All you need is to clip the shape and specify to use antialiasing:
Rectangle()
.fill(.green)
.clipped(antialiased: true)
This is how it looks:

How do I color a SwiftUI Rectangle two different colors?

Supposing I have a simple SwiftUI Rectangle such as:
Rectangle()
.frame(width: 20, height: 20)
How can I color one half of it white at a 45 degree angle, and the other half black at a 45 degree angle?
My initial guess was to layer two Rectangles over each other with ZStack, but that way seems hacky to me.
Simple geometry can be achieved by transforming the shape:
Rectangle()
.rotation(.degrees(45), anchor: .bottomLeading)
.scale(sqrt(2), anchor: .bottomLeading)
.frame(width: 200, height: 200)
.background(Color.red)
.foregroundColor(Color.blue)
.clipped()
The least "hacky" way could be Gradients. With a code like this:
Rectangle()
.fill(
LinearGradient(
gradient: Gradient(stops: [
Gradient.Stop(color: .red, location: 0.5),
Gradient.Stop(color: .black, location: 0.5)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing))
.frame(width: 200, height: 200)
You get a rectangle like this:
What actually happens is, that SwiftUI creates two color-gradients: One from 0.0 - 0.5, from red to red and a second gradient from 0.5 to 1.0 from black to black.
Also note, that the fill must immediately follow the path (here Rectangle) as it is the Shape variant of fill.
You should use path for drawing this. My idea is to put Rectangle and Triangle into ZStack:
struct GradientRectangle: View {
var body: some View {
ZStack {
Rectangle()
Triangle()
.foregroundColor(.red) // here should be .white in your case
}
.frame(width: 300, height: 300)
}
}
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
the result will be: