Finding the CGRect of a view - swift3

I have a view which subclasses a collectionView horizontal. Each cell takes the full width and the full height. Within each cell, I have a button and I'm not able to find its proper location relative to my viewController's main view.
I have tried the following and many other combination but
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIds.studyCell, for: [0,1]) as? StudyCell else { return nil }
transition.originFrame = self.view.convert(cell.explanationButton.frame, to: self.view.superview)
transition.presenting = true
return transition
}

Related

How to generate annotations on a certain zoom and only those close to where user zoomed in?

My app requests JSON data (latitude, longitude, and other information about a place) and then displays them on a map in a form of clickable annotations. I'm receiving around 30,000 of those, so as you can imagine, the app can get a little "laggy".
The solution I think would fit the app best is to show those annotations only on a certain zoom level (for example when the user zooms so only one city is visible at once, the annotations will show up). Since there's a lot of them, showing all 30,000 would probably crash the app, that's why I also aim at showing just those that are close to where the user zoomed in.
The code below shows immediately all annotations at once at all zoom levels. Is there a way to adapt it to do the things I described above?
struct Map: UIViewRepresentable {
#EnvironmentObject var model: ContentModel
#ObservedObject var data = FetchData()
var locations:[MKPointAnnotation] {
var annotations = [MKPointAnnotation]()
// Loop through all places
for place in data.dataList {
// If the place does have lat and long, create an annotation
if let lat = place.latitude, let long = place.longitude {
// Create an annotation
let a = MKPointAnnotation()
a.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
a.title = place.address ?? ""
annotations.append(a)
}
}
return annotations
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
// Show user on the map
mapView.showsUserLocation = true
mapView.userTrackingMode = .followWithHeading
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// Remove all annotations
uiView.removeAnnotations(uiView.annotations)
// HERE'S WHERE I SHOW THE ANNOTATIONS
uiView.showAnnotations(self.locations, animated: true)
}
static func dismantleUIView(_ uiView: MKMapView, coordinator: ()) {
uiView.removeAnnotations(uiView.annotations)
}
// MARK: Coordinator Class
func makeCoordinator() -> Coordinator {
return Coordinator(map: self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var map: Map
init(map: Map) {
self.map = map
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// Don't treat user as an annotation
if annotation is MKUserLocation {
return nil
}
// Check for reusable annotations
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: Constants.annotationReusedId)
// If none found, create a new one
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: Constants.annotationReusedId)
annotationView!.canShowCallout = true
annotationView!.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
} else {
// Carry on with reusable annotation
annotationView!.annotation = annotation
}
return annotationView
}
}
}
Been searching for an answer for a while now and found nothing that worked well. I imagine there's a way to get visible map rect and then condition that in Map struct, but don't know how to do that. Thanks for reading this far!
Your delegate can implement mapView(_:regionDidChangeAnimated:) to be notified when the user finishes a gesture that changes the map's visible region. It can implement mapViewDidChangeVisibleRegion(_:) to be notified while the gesture is happening.
You can get the map's visible region by asking it for its region property. Regarding zoom levels, the region documentation says this:
The region encompasses both the latitude and longitude point on which the map is centered and the span of coordinates to display. The span values provide an implicit zoom value for the map. The larger the displayed area, the lower the amount of zoom. Similarly, the smaller the displayed area, the greater the amount of zoom.
Your updateUIView method recalculates the locations array every time SwiftUI calls it (because locations is a computed property). You should check how often SwiftUI is calling updateUIView and decide whether you need to cache the locations array.
If you want to efficiently find the locations in the visible region, try storing the locations in a quadtree.
Finally figured that out...
The Coordinator class can implement mapView(_:regionDidChangeAnimated:) (as #rob mayoff said) that gets called after the user finishes a gesture that changes the map's visible region. When that happens, annotations on the map and their array are updated. Looks something like this...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if mapView.region.span.latitudeDelta < <Double that represents zoom> && mapView.region.span.longitudeDelta < <Double that represents zoom> {
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(map.getLocations(center: mapView.region.center))
}
}
... phrases (doubles missing from the if statement) in < > are to be replaced with your own code (the greater the double, the smaller zoom is needed to view the annotations). The array of annotations is updated by a function defined in Map struct and looks like this...
func getLocations(center: CLLocationCoordinate2D) -> [MKPointAnnotation] {
var annotations = [MKPointAnnotation]()
let annotationSpanIndex: Double = model.latlongDelta * 10 * 0.035
// Loop through all places
for place in data.dataList {
// If the place does have lat and long, create an annotation
if let lat = place.latitude, let long = place.longitude {
// Create annotations only for places within a certain region
if Double(lat)! >= center.latitude - annotationSpanIndex && Double(lat)! <= center.latitude + annotationSpanIndex && Double(long)! >= center.longitude - annotationSpanIndex && Double(long)! <= center.longitude + annotationSpanIndex {
// Create an annotation
let a = MKPointAnnotation()
a.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
a.title = place.adresa ?? ""
annotations.append(a)
}
}
}
return annotations
}
... where annotationSpanIndex determines in how big of a region around the center point will the annotations be shown (greater the index, bigger the region). This region should be ideally slightly larger than the zoom on which the annotations are shown.

SwiftUI: How to know when view is moved inside scroll view?

I need some direction to resolve a problem. I have multiple videos inside a scroll view and I want only one of them to play at a time. I know about geo.frame to know the position of the view in screen but how can I constantly check if the screen moved to a point? I want to use this code in the video player view but I can only put this to .onAppear and it won't work because it is only called once. Is there a method that I can check when the screen is moved (scroll view is dragged) so that I can play the video in the middle and stop the other ones?
if (geo.frame(in: .global).midY > 200 && geo.frame(in: .global).midY < 800) {
avPlayer.play()
print("Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
print("Video to play: \(postID)")
}
You can put this code inside your view body using the Geometry reader. The value of geo is updated as the items are being scrolled and moved around.
Put this in a view of a row in your table of videos:
struct Example: View {
private func checkLocation(geo: GeometryProxy) -> Bool {
let loc = geo.frame(in: .global).midY
print ("Loc is: \(loc)")
if loc > 200 {
return true
}
return false
}
var body: some View {
GeometryReader { geo in
if checkLocation(geo: geo) {
Text ("This is working")
}
}
}
}

What is the best way to get Drag Velocity?

I was wondering how can one get DragGesture Velocity?
I understand the formula works and how to manually get it but when I do so it is no where what Apple returns (at least some times its very different).
I have the following code snippet
struct SecondView: View {
#State private var lastValue: DragGesture.Value?
private var dragGesture: some Gesture {
DragGesture()
.onChanged { (value) in
self.lastValue = value
}
.onEnded { (value) in
if lastValue = self.lastValue {
let timeDiff = value.time.timeIntervalSince(lastValue.time)
print("Actual \(value)") // <- A
print("Calculated: \((value.translation.height - lastValue.translation.height)/timeDiff)") // <- B
}
}
var body: some View {
Color.red
.frame(width: 50, height: 50)
.gesture(self.dragGesture)
}
}
From above:
A will output something like Value(time: 2001-01-02 16:37:14 +0000, location: (250.0, -111.0), startLocation: (249.66665649414062, 71.0), velocity: SwiftUI._Velocity<__C.CGSize>(valuePerSecond: (163.23212105439427, 71.91841849340494)))
B will output something like Calculated: 287.6736739736197
Note from A I am looking at the 2nd value in valuePerSecond which is the y velocity.
Depending on how you drag, the results will be either different or the same. Apple provides the velocity as a property just like .startLocation and .endLocation but unfortunately there is no way for me to access it (at least none that I know) so I have to calculate it myself, theoretically my calculations are correct but they are very different from Apple. So what is the problem here?
This is another take on extracting the velocity from DragGesture.Value. It’s a bit more robust than parsing the debug description as suggested in the other answer but still has the potential to break.
import SwiftUI
extension DragGesture.Value {
/// The current drag velocity.
///
/// While the velocity value is contained in the value, it is not publicly available and we
/// have to apply tricks to retrieve it. The following code accesses the underlying value via
/// the `Mirror` type.
internal var velocity: CGSize {
let valueMirror = Mirror(reflecting: self)
for valueChild in valueMirror.children {
if valueChild.label == "velocity" {
let velocityMirror = Mirror(reflecting: valueChild.value)
for velocityChild in velocityMirror.children {
if velocityChild.label == "valuePerSecond" {
if let velocity = velocityChild.value as? CGSize {
return velocity
}
}
}
}
}
fatalError("Unable to retrieve velocity from \(Self.self)")
}
}
Just like this:
let sss = "\(value)"
//Intercept string
let start = sss.range(of: "valuePerSecond: (")
let end = sss.range(of: ")))")
let arr = String(sss[(start!.upperBound)..<(end!.lowerBound)]).components(separatedBy: ",")
print(Double(arr.first!)!)

Accessing collectionView cells visible/invisible

I got a function to flip my collection view cells, which is working fine. My problem is that I want to flip all the cells and not just this one visible cell, so when I swipe to next cell it will be flipped.
This is what I am using to flip the one visible cell only. Any help in the right direction would be appreciated.
func flipAction() {
let visibleRect = CGRect(origin: mainCollecView.contentOffset, size: mainCollecView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
let visibleIndexPath = mainCollecView.indexPathForItem(at: visiblePoint)
let cell = mainCollecView.cellForItem(at: visibleIndexPath!) as! MainCollectionViewCell
if cell.isFlipped == false {
//Flip card
cell.flip()
cell.isFlipped = true
flipBtn.setImage(UIImage(named: "reversed"), for: .normal)
} else {
// Flip the card back
flipBtn.setImage(UIImage(named: "Calendar"), for: .normal)
cell.flipBack()
cell.isFlipped = false
}
}
You can use different approaches. But if you want all to be flipped, use a variable in you Class, like
var isFlipped:Bool = false
Inside your function now:
flipAction() {
isfliped = !isfliped
mycollection.reload() //where mycollection is yours
}
Then inside your
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
if isfliped == false {
// your code for cell when is not flipped
} else {
//your code for cell when is flipped
}
This way all cells will be flipped or not at once, not depending if they are visible or not.

How to remove Place Marker in Google map swift 3

I am Making demo of Google map i have add marker in google map that about 20-30 marker on google map so i want to do when user enter place name in textfield that place will be display and it's marker and all previous marker which i add in google map that should also display. so i have add marker when user enter place in UITextField..i successfully did all task..but when user search another place the previous search marker place still that position..so i don't know how to remove previous marker when user search another place..
//here is my code
//IBOutlet
#IBOutlet var ViewMap: GMSMapView!
func GetLocationFromAddress(address: String) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
let coordinates:CLLocationCoordinate2D = placemark.location!.coordinate
print("lat", coordinates.latitude)
print("long", coordinates.longitude)
let position = CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
let marker = GMSMarker(position: position)
marker.title = "Name Of Location"
marker.map = self.ViewMap
let camera = GMSCameraPosition.camera(withLatitude: coordinates.latitude,
longitude: coordinates.longitude,
zoom: self.zoomLevel)
self.ViewMap.camera = camera
self.ViewMap.animate(to: camera)
}
})
}
//function call
#IBAction func btnSearchAction(_ sender: Any) {
GetLocationFromAddress(address: self.txtSearch.text!)
}
Any of your help make my day good..thanks in advance!!!!
I think you should save last search marker in variable like searchedMarker of type GMSMarker.
and next time when you search again for any location and you get action in btnSearchAction method.
#IBAction func btnSearchAction(_ sender: Any)
{
searchedMarker.map = nil
GetLocationFromAddress(address: self.txtSearch.text!)
}
and also update searchedMarker in GetLocationFromAddress method on creation of GMSMarker.