SwiftUI strangely shifting location of certain Views when invoking contextMenu modifier - swift

I'm having an issue where some SwiftUI views (under certain configurations) are shifting their location to the center of the screen when I use the .contextMenu(menuItems:) modifier, and I can't figure out why. I've recreated the problem in a simplified manner in the following code samples:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 200, y: 100))
path.addLine(to: CGPoint(x: 100, y: 300))
path.addLine(to: CGPoint(x: 300, y: 300))
path.addLine(to: CGPoint(x: 200, y: 100))
}
.contextMenu {
Text("hello world")
}
}
}
GIF that demonstrates how the triangle shifts to center screen for some reason due to the context menu invocation
struct ContentView: View {
var body: some View {
Circle()
.position(x: 0, y: 0)
.frame(width: 50, height: 50, alignment: .bottom)
.contextMenu {
Text("hello world")
}
}
}
GIF that demonstrates how the circle shifts to center screen and somehow gains a sharp edge due to the context menu invocation
I would really appreciate it if anyone knows what's going on here. I'm on Xcode 12.5.1, macOS 11.4, and iOS 14.5.

By adding certain frame to shape you can fix the issue but I don't have any explanation.
Path { path in
path.move(to: CGPoint(x: 200, y: 100))
path.addLine(to: CGPoint(x: 100, y: 300))
path.addLine(to: CGPoint(x: 300, y: 300))
path.addLine(to: CGPoint(x: 200, y: 100))
}
.frame(width: 400, height: 400, alignment: .center)
.contextMenu {
Text("hello world")
}

Related

Path transparent only on macOS

Why are paths slightly transparent on macOS and not iOS? How can I make them not transparent on macOS?
Minimal reproducible example:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 150, y: 150))
path.addLine(to: CGPoint(x: 150, y: 300))
}
.stroke(lineWidth: 8)
Path { path in
path.move(to: CGPoint(x: 150, y: 200))
path.addLine(to: CGPoint(x: 150, y: 450))
}
.stroke(lineWidth: 8)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}

How can I clip a Shape with another Shape in SwiftUI?

I want to clip the red square with the green half-circle. I tried .mask and .clipShape but
i can't get this to work. Can someone share how its done please?
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 500, height: 500)
Circle()
.trim(from: 0.5, to: 1.0)
.fill(Color.green)
.rotationEffect(Angle(degrees: 90))
.frame(width: 500, height: 500)
.offset(x: -250)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
with using eoFill like in the code:
struct ContentView: View {
var body: some View {
CustomShape()
.fill(Color.purple, style: FillStyle(eoFill: true, antialiased: true))
.frame(width: 200, height: 200, alignment: .center)
.shadow(radius: 10.0)
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height/2, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: true)
path.addRect(CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height))
}
}
}
Create a struct that conforms to Shape.
Implement func path(in rect: CGRect) -> Path
Create a Path. Add the rectangle, then the circle.
Fill the shape with isEOFilled set to true
My code is this basic one. Maybe someone finds this useful so i am attaching it here as well.
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
BoxCutOut()
.foregroundColor(Color.red)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
struct BoxCutOut: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x:0, y: 0))
path.addLine(to: CGPoint(x:rect.width, y:0))
path.addLine(to: CGPoint(x:rect.width, y:rect.height))
path.addLine(to: CGPoint(x:0, y:rect.height))
path.addArc(center: CGPoint(x: 0, y: rect.height/2), radius: rect.height/2, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true )
}
}
}

SwiftUI: Change the start position for animated progress bar (or Shape drawing in general)

The code below begins to draw a rectangle from the bottom left corner. How can I change it to begin to draw from the top middle? Hopefully there is a simple setting I am missing here.
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
Rectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.rotationEffect(Angle(degrees: 270.0))
.frame(width: 100, height:100)
Rectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
.frame(width: 100, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
You don't need to rotate it (moreover it might be not square), just create own rectangular shape with start point anywhere needed.
Demo prepared with Xcode 12.4 / iOS 14.4
struct MyRectangle: Shape {
func path(in rect: CGRect) -> Path {
Path {
$0.move(to: CGPoint(x: rect.midX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
$0.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
$0.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
}
}
}
struct ContentView: View {
#State private var progress: Double = 0
#State private var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
HStack{
ZStack {
MyRectangle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.gray)
.frame(width: 200, height:100)
MyRectangle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.blue)
.animation(.linear)
.frame(width: 200, height:100)
.onReceive(timer, perform: { _ in
if(progress < 100){
progress += 0.1/10
}
else{
progress = 0
}
})
}
}
.padding()
}
}
}

How to create a NavigationLink or Button from a shape in SwiftUI that is only triggered by tapping on the shape but not its frame

I have a simple shape I want to use as a button but I would like to have the link/button only triggered when clicked on the shape itself but not on its frame. With the following code the link is also triggered when clicking inside the frame of the triangle:
struct Triangle: Shape {
func path(in rect: CGRect)-> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
return path
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: Text("DetailView")){
Triangle()
.fill(Color.green)
.frame(width: 200, height: 200, alignment: .center)
}.navigationBarTitle(Text("Triangle"))
}
}
}
How can I adjust the code to have only tapping the triangle but not the frame triggering the link?
You can add a contentShape modifier to tell the system how to do hit testing for the NavigationLink:
NavigationLink(destination: Text("DetailView")){
Triangle()
.fill(Color.green)
.frame(width: 100, height: 100, alignment: .center)
}
.contentShape(Triangle())
There's a caveat here, which is SwiftUI includes some slop in the hit testing, so this will still accept some clicks/touches that are close to the borders of the shape, but still outside. I don't have a great solution for getting rid of that slop. But, you can see that the same thing happens on the basic Rectangle that normally makes up the navigation view as well -- e.g. put a border around it and note that you can still tap slightly outside of that border.
A secondary way you could experiment with is render the Path on its own, attach an onTapGesture to it and turn on/off a boolean connected to the NavigationLink that you'd then want to move outside your hierarchy. Something like:
struct ContentView: View {
#State var navigationLinkActive = false
var body: some View {
NavigationView {
Group {
if navigationLinkActive {
NavigationLink(destination: Text("Detail"), isActive: $navigationLinkActive) {
EmptyView()
}
}
Triangle()
.fill(Color.green)
.frame(width: 100, height: 100, alignment: .center)
.onTapGesture {
navigationLinkActive = true
}
}.navigationBarTitle(Text("Triangle"))
EmptyView()
}
}
}
Note that in this solution, you still have the hit slop issue, plus you lose the touch down/up highlighting that the Button has.

Repeating SwiftUI animation gets choppy over time

I've developed a repeating animation that starts out silky smooth but starts to get quite choppy after 10 or 20 seconds doing nothing else on my device. This is the entire code for the view (it is set as the content view of a single view app):
import SwiftUI
struct MovingCirclesView: View {
#State var animationPercent: Double = 0
var body: some View {
ZStack {
MovingCircle(animationPercent: $animationPercent, size: 300, movementRadius: 100, startAngle: 0)
.offset(x: -200, y: -200)
MovingCircle(animationPercent: $animationPercent, size: 400, movementRadius: 120, startAngle: .pi * 3/4)
.offset(x: 50, y: 300)
MovingCircle(animationPercent: $animationPercent, size: 350, movementRadius: 200, startAngle: .pi * 5/4)
.offset(x: 10, y: 30)
MovingCircle(animationPercent: $animationPercent, size: 230, movementRadius: 80, startAngle: .pi * 1/2)
.offset(x: 220, y: -300)
MovingCircle(animationPercent: $animationPercent, size: 230, movementRadius: 150, startAngle: .pi)
.offset(x: 220, y: 100)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.animation(Animation.linear(duration: 30).repeatForever(autoreverses: false))
.onAppear() { self.animationPercent = 1 }
}
private struct MovingCircle: View, Animatable {
#Binding var animationPercent: Double
let size: CGFloat
let movementRadius: CGFloat
let startAngle: Double
var body: some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(Color.white)
.opacity(0.1)
.offset(angle: .radians(.pi * 2 * self.animationPercent + self.startAngle), radius: self.movementRadius)
}
}
}
struct MovingCirclesView_Previews: PreviewProvider {
static var previews: some View {
MovingCirclesView()
.background(Color.black)
.edgesIgnoringSafeArea(.all)
}
}
struct AngularOffset: AnimatableModifier {
var angle: Angle
var radius: CGFloat
var animatableData: AnimatablePair<Double, CGFloat> {
get {
return AnimatablePair(angle.radians, radius)
}
set {
self.angle = .radians(newValue.first)
self.radius = newValue.second
}
}
func body(content: Content) -> some View {
return content.offset(CGSize(
width: CGFloat(cos(self.angle.radians)) * self.radius,
height: CGFloat(sin(self.angle.radians)) * self.radius
))
}
}
extension View {
func offset(angle: Angle, radius: CGFloat) -> some View {
ModifiedContent(content: self, modifier: AngularOffset(angle: angle, radius: radius))
}
}
The general idea is that there are a series of semi-transparent circles slowly moving in circles. I'm concerned this will not be worth the energy usage and was planning to profile anyway, but to my surprise, the animation seems to get bogged down rather quickly. I profiled it on an iPhone X and I don't see any increase in CPU usage nor Memory usage over time as the animation gets more and more choppy.
Does anyone have an idea of why the animation gets choppy? Anything I can do to fix that or do I have to throw out this idea?
Update: Xcode 13.4 / iOS 15.5
As on now it seems drawingGroup does not have such positive effect as was before, because with used .animation(.., value:) animation works properly and with low resource consumption.
Test module on GitHub
Original
Here is a solution - activating Metal by using .drawingGroup and using explicit animation.
Works fine with Xcode 11.4 / iOS 13.4 - tested during 5 mins, CPU load 4-5%
ZStack {
// .. circles here
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.drawingGroup()
.onAppear() {
withAnimation(Animation.linear(duration: 30).repeatForever(autoreverses: false)) {
self.animationPercent = 1
}
}
Note: findings - it looks like implicit animation recreates update stack in this case again and again, so they multiplied, but explicit animation activated only once.