Why do my SwiftUI gestures on a Map work only once? - ios

Setup:
I am converting an app from UIKit to SwiftUI.
The app uses now instead of an MKMapView a SwiftUI Map.
Unfortunately, not all features of an MKMapView are already provided by Apple for Map, e.g. dragging a map annotation and getting the coordinate of the drag end point.
Anyway, I am trying thus to implement a similar function in SwiftUI. This is the development body of my annotation view:
var body: some View {
GeometryReader { geo in // 1
let frameInGlobal = geo.frame(in: .global)
Image("PinRed")
.background(Color.green) // 2
.offset(CGSize(width: dragAmount.width, height: dragAmount.height - liftOff)) // 3
.defersSystemGestures(on: .all) // 4
.onTapGesture {
print("Global center: \(frameInGlobal.midX) x \(frameInGlobal.midY)")
}
.gesture(drag(frameInGlobal: frameInGlobal)) // 5
.simultaneousGesture( // 6
LongPressGesture(minimumDuration: 0.2)
.onEnded { _ in // 7
liftOff = 40.0
}
)
}
.frame(width: 30, height: 30)
.background(Color.red) // 2
}
<1> GeometryReader is used to get the screen coordinates during dragging.
<2> The background colors are there so that it can easily be identified what happens.
<3> The pin image is offset by the drag amount plus a vertical shift so that the pin is visible above the finger.
<4> The system gestures, e.g. zoom in after double tap and long press to drag the map, are deferred so that the custom gestures come through.
<5> The custom drag gesture essentially changes a #GestureState var dragAmount = CGSize.zero so that the pin image follows the finger.
<6> A simultaneousGesture is required so that the custom tap gesture as well as the custom long press gesture can be used.
<7> That the pin is lift off, indicates that dragging can start.
Problem:
If the liftOff value is 0.0, I can tap the annotation, which prints out its position, and I can long press it and drag it around. After dragging stopped, it still accepts further taps and long presses.
However, when the liftOff value is e.g. 40.0, the annotation can only be tapped as long as it is not dragged, and when it has been dragged the 1st time, it cannot be dragged again. This means the custom gestures do no longer come through. Instead, only the system gestures work.
Here is an example of the situation after the 1st drag (green is the image background, red the GeometryReader background).
Question:
Why are the custom gestures only accepted when the pin is not lift off?
And how to do it right?
Edit:
I just found out that my custom gestures work as long as Image (green square) and GeometryReader (red square) overlap, and the tap point, long press point and start point of a drag are within the overlap area.

Problem solved. It has nothing to do with SwiftUI:
If in the code example above liftOff is 0, the green View (Image) and the red View (GeometryReader) have always the same position on the screen. If the green View is tapped, the hit position is forwarded to the custom gestures. Since the red View has the same position on the screen, the red View handles the hit.
If however liftOff is larger than the frame size of the green and red View, both do not overlap on the screen. If now the green View is tapped, the hit position is forwarded to the red View that does not overlap the green View, i.e. the red View is not hit, and the hit is forwarded to the next View behind the green View, which is the Map itself. Thus the custom gestures are not triggered, but the system gestures.
In the code above, this can easily be checked by adding line 3 additionally as last view modifier of the GeometryReader.

Related

Drag gesture — get position of finger from finger's origin AND allow scrolling in ScrollView

I am trying to get the position of the finger during a drag gesture (from an origin point of where the finger first began the drag). I want to do this while simultaneously allowing for scrolling within a ScrollView.
I can do these independently, but it seems scrolling in a ScrollView prevents the drag gesture.
I have tried adding the .gesture() part of the code below to the Component(), the VStack, and I also tried wrapping all of this in a GeometryReader and adding it to that. Each time, the .gesture() only prints when no scrolling happens (so, it prints when dragging horizontally).
How can I achieve this?
ScrollView {
VStack(alignment: .center, spacing: 0) {
ForEach((0..<reports.count), id: \.self) {i in
Component(report: reports[i])
.offset(y: -(CGFloat(i) * 12.0))
}
}
}
.gesture(
DragGesture()
.onChanged { value in
self.position = value.translation
print(self.position)
}
.onEnded { _ in
self.position = .zero
}
)
Thanks to everyone for the suggestions (some of which were deleted by their authors). Unfortunately, as DatBlaueHus states:
Unfortunately, the Scrollview drag handler kills all simultaneous drag handlers as well, so afaik there is no stable solution for your original issue short of wrapping a UIKit solution for that issue.
I've solved my issue by using a custom ScrollView implementation. I need to play around with the feel of it — it doesn't scroll quite as smoothly as the stock ScrollView, but it solves my issue and recognises gestures simultaneously.
this answer might work, but it also seems like a weird workaround.
worst case you must build your own scrollview, as to have access to all touches and then offsetting stuff so scroll also works..

How spacing is calculated in HStack

In SwiftUI when a horizontal array of Circles are made like this:
HStack(spacing : 4) {
Foreach(0..<5) { index in
Circle()
}
}
How the (horizontal) spacing value of 4 is applied? Is it between the centre of two circles or from their edges instead?
Building on top of #Asperi's comment: the spacing is applied in the same way it's applied to Text or Button standard views, which is between the frames of the views.
If you click on an element in the preview (this doesn't work when on live preview mode), you can see the frame of an element outlined in blue. The spacing is applied between the edges of the frames of each view.

Context menu preview not with rounded corners in SwiftUI

When using a plain styled List in SwiftUI with more than one Text view inside a VStack as show below, the preview of the view when showing its context menu doesn't have corner radius. If you remove one of the Text views it will have corner radius. Also the rows that you need to scroll down to will also show corner radius most of the time. I've tried using the contentShape modifier with RoundedRectangle but doesn't fix it. How can I get it to show with corner radius all the time?
List {
ForEach(1...20, id: \.self) { _ in
VStack {
Text("Hello")
Text("World")
}.contextMenu {
Button {} label: { Text("Hello") }
}
}
}.listStyle(.plain)
I thinks you should fill a bug to Apple in this case.
After some try I have notice that if you define a frame for your Label , the view is correctly rounded.
I tryed to fixedSize the text without success :
Text("world").fixedSize()
The problem seems to come from _UIMorphingPlatterView, more precisely the _UIPlatterClippingView which use _UIPortalView.
If the frame is not fixed the clip is not apply correctly.
The debug view hierarchy give this :

SwiftUI: Change Popover Arrow Color

In SwiftUI, how does one change the color of the arrow that connects a popover to its anchor point?
When working with the underlying UIPopoverController outside of SwiftUI, I believe it's done by changing the backgroundColor property, but I don't see a way to access that here. Even setting background as the very last modifier only changes the view within the popover; not the popover itself.
For example, adding the following code to a view:
#State private var showDetailedView: Bool = false
// ...
.popover(isPresented: self.$showDetailedView) {
Text("Hello!")
.padding()
.background(Color.red)
}
.onTapGesture {
self.showDetailedView = true
}
...results in an arrow that's still the default background color (this example taken from native macOS in Dark Mode):
...and like this on iOS (running via Catalyst), which is even worse!
Here is a pure SwiftUI solution using GeometryReader and two .frame calls. The key idea is to make a background larger than the size of your presented view. Since SwiftUI does not clip contents at this moment, this will override the default background on the popover arrow.
Do notice that this only works with a solid background. In Catalyst, a solid background is already painted so transparent content would reveal the ugly black as you have posted. We might have to resort to things like UIViewRepresentable for such case.
Consider the following example that changes the color of an arrow on the top edge:
.background(GeometryReader { geometry in
Color
.white
.frame(width: geometry.size.width,
height: geometry.size.height + 100)
.frame(width: geometry.size.width,
height: geometry.size.height,
alignment: .bottom)
})
Explanation:
The first inner frame creates a white rectangle that is 100px higher than your presented view.
The second outer frame creates a new frame that is of the same size as your presented view. this is achieved through the GeometryReader.
The alignment: argument in the second outer frame makes sure that these two frames align on the bottom.
Since the outer frame is as large as the GeometryReader, it fills the whole background of your presented view.
The "overflowed" content overrides the default black arrow color.
To make this work with arbitrary arrow edge, you might want to change the inner frame, increasing both the width and height. As for the alignment for outer frame, using the default argument of .center should work.

Issue with horizontal scrollview inside a vertical scrollview in ios swift

My layout is currently like this:
View
-- View
-- Vertical ScrollView
------ View
--------- Horizontal Paginated ScrollView
--------- View
------------- Horizontal ScrollView -- not working properly
See this image for view hierarchy screenshot from xcode:
Using Swift.
I am adding subviews dynamically to this "Size Select Scroll View"
Two Issues:
After adding views, there is no margin between the subviews. Each subview's coord. are like this: (10.0, 0.0, 44.0, 44.0), (54.0, 0.0, 44.0, 44.0), (98.0, 0.0, 44.0, 44.0), (142.0, 0.0, 44.0, 44.0) etc.
But the appearance is like this without the 10 points gap between each subview: http://i.stack.imgur.com/mCGRW.png
Scrolling horizontally is a pain. It only works on maybe 1/4 height from top of the scrollview area and very difficult to scroll. How do i layout subviews so that this scrollview is properly scrollable?
Note: I am explicitly setting content size of size scrollview to more than required so that i can see the scrolling.
I found out the issue. According to the Apple Docs the touch events will be passed to a subview only if it lies entirely in its parent.
In my case, the scrollview was going out of bounds of its parent view ( Details View), because of which touch events were weird. I increased the parent view's size to fit the scrollview and it works fine now.
From the docs (https://developer.apple.com/library/content/qa/qa2013/qa1812.html):
The most common cause of this problem is because your view is located outside the bounds of its parent view. When your application receives a touch event, a hit-testing process is initiated to determine which view should receive the event. The process starts with the root of the view hierarchy, typically the application's window, and searches through the subviews in front to back order until it finds the frontmost view under the touch. That view becomes the hit-test view and receives the touch event. Each view involved in this process first tests if the event location is within its bounds. Only after the test is successful, does the view pass the event to the subviews for further hit-testing. So if your view is under the touch but located outside its parent view's bounds, the parent view will fail to test the event location and won't pass the touch event to your view.
This is always a challenge in iOS.
There are various solutions which unfortunately depend on the exact situation.
Here's a drop-in solution which is often the right solution.
/*
So, PASS any touch to the NEXT view, BUT ALSO if any of OUR
subviews are buttons, etc, then THOSE should ALSO work normally. :/
*/
import UIKit
class Passthrough: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
(Of course, you can also just drop the call in to some class, eg
class SomeListOrWhatever: UICollectionView, .. {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("MIGHT AS WELL TRY THIS")
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
Even if you "don't totally understand what the problem is", this is "one of" the solutions!
For example, this is (usually!) the precise solution to the exact issue quoted from the doco by #kishorer747
It's definitely a real nuisance in iOS.

Resources