I'm trying to implement same logic for different tabs in iOS application, but get Thread 1: EXC_BAD_INSTRUCTION (code = EXC_1386_INVOP, subcode 0x0). It's a simple application that should allow user to mark some points on the map(right now with annotations), and draws lines between them in process.
Class that contains logic:
class MapController : NSObject, MKMapViewDelegate{
var Map: MKMapView!
var points : [CGPoint]
init(_Map : MKMapView!, delClass : String)//, coder aDecoder: NSCoder)
{
self.Map = _Map
points = [CGPoint]()
self.Map.mapType = MKMapType.Satellite
let centre = CLLocationCoordinate2D(latitude: 40.0000000,
longitude: 49.0000000)
let span = MKCoordinateSpan(latitudeDelta: 10.01,
longitudeDelta: 10.01)
let region = MKCoordinateRegion(center: centre, span: span)
self.Map.setRegion(region, animated: false)
self.Map.regionThatFits(region)
let urlTemplate = "http://someip/mapcache/tms/1.0.0/test#GoogleMapsCompatible/{z}/{x}/{y}.png"
let carte_indice = MKTileOverlay(URLTemplate: urlTemplate)
carte_indice.geometryFlipped = true
carte_indice.canReplaceMapContent = false
print("Map")
self.Map.addOverlay(carte_indice)
}
func longPressGesture()
{
let lpg = UILongPressGestureRecognizer(target: self.Map, action: "longPressAction:")
lpg.minimumPressDuration = 1;
Map.addGestureRecognizer(lpg)
}
func longPressAction(myRecognizer : UILongPressGestureRecognizer)
{
let currPoint = myRecognizer.locationInView(Map)
let point = Map.convertPoint(currPoint, toCoordinateFromView: Map)
points.append(currPoint);
if(points.count>1)
{
let startPoint = Map.convertPoint(points[points.count-2], toCoordinateFromView: Map)
let endPoint = Map.convertPoint(currPoint, toCoordinateFromView: Map)
var lineCoords = [startPoint,endPoint]
var line = MKPolyline(coordinates: &lineCoords, count: 2)
var test = MKPolylineRenderer(polyline: line)
test.lineWidth = 10;
test.strokeColor = UIColor.redColor()
Map.addOverlay(line)
}
let myAnnotation = MKPointAnnotation();
myAnnotation.coordinate = point
myAnnotation.title = "Test"
myAnnotation.subtitle = "Test subtitle"
Map.addAnnotation(myAnnotation);
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKCircle {
let circle = MKCircleRenderer(overlay: overlay);
circle.strokeColor = UIColor.redColor();
circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1);
circle.lineWidth = 1;
return circle;
}else if overlay is MKTileOverlay {
var carte_Renderer = MKTileOverlayRenderer(overlay: overlay)
carte_Renderer.alpha = 0.9
return carte_Renderer
}else if overlay is MKPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay);
polylineRenderer.strokeColor = UIColor.blueColor();
polylineRenderer.lineWidth = 5;
return polylineRenderer;
}else {
return MKPolylineRenderer();
}
}
}
ViewController classes look like this:
class BuildTrack: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate{
#IBOutlet var testRef: MKMapView!
var mapController : MapController!
required init?(coder aDecoder : NSCoder)
{
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
mapController = MapController(_Map: testRef, delClass: "BuildTrack")
mapController.longPressGesture();
testRef.delegate = mapController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I guess it's because i don't declare a delegate right. I tried to do this in my MapController class like this:
self.Map = _Map
Map.delegate = BuildTrack.self()
, but got same exception when i clicked map(now i don't even see the map, it crashes in the init of MapController), looks like something gets disposed before time.
Is the problem really in delegates, and is this approach ok? When i had one ViewController and all logic was inside it, it worked fine, problem occurred when i tried to separate logic from interface.
I see problem in using outlet testRef before it have value
Try to add ! in mapController declaration
var mapController: MapController!
and remove mapController initialization from init:
required init?(coder aDecoder : NSCoder) {
super.init(coder: aDecoder)
}
Related
I'm developing a new SwiftUI app and I'm trying to figure out how to make this Swift project compatible with SwiftUI. In this example I am developing Map with directions.
The problem is that I can't make the UIViewRepresentable work. I get an error:
Type 'DirectionsMap' does not conform to protocol 'UIViewRepresentable'
Here is my code:
import SwiftUI
import MapKit
struct DirectionsMap: UIViewRepresentable {
#EnvironmentObject var model: ViewModel
var business: Businesses
// Start Coordinates for Directions
var start: CLLocationCoordinate2D {
return model.locationManager.location?.coordinate ?? CLLocationCoordinate2D()
}
// End Coordinates for Directions
var end: CLLocationCoordinate2D {
if let lat = business.coordinates?.latitude, let long = business.coordinates?.longitude {
return CLLocationCoordinate2D(latitude: lat, longitude: long)
}
else {
return CLLocationCoordinate2D()
}
}
func makeUIView(context: Context) -> MKMapView {
// Create Map
let map = MKMapView()
map.delegate = context.coordinator
map.showsUserLocation = true
map.userTrackingMode = .followWithHeading
// Create Directions request
var request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end))
// Create Directions Object
let directions = MKDirections(request: request)
directions.calculate { response, error in
if error == nil && response != nil {
for route in response!.routes {
map.addOverlay(route.polyline)
map.setVisibleMapRect(route.polyline.boundingMapRect, edgePadding: UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100), animated: true)
}
}
}
let annotation = MKPointAnnotation()
annotation.coordinate = end
annotation.title = business.name ?? ""
map.addAnnotation(annotation)
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
static func dismantleUIView(_ uiView: MKMapView, coordinator: ()) {
uiView.removeAnnotations(uiView.annotations)
uiView.removeOverlays(uiView.overlays)
}
//MARK: - Coordinator
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}
class Coordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(polyline: overlay as! MKPolyline)
renderer.strokeColor = .blue
renderer.lineWidth = 5
return renderer
}
}
Try adding
struct DirectionsMap: UIViewRepresentable {
typealias UIViewType = MKMapView // <-- This
Just something I have noticed, when changing the types of the make and update functions the protocol requires explicit typealias.
I have already checked that coordinates are available, but when I launch the map snaps to Antartica, and the route is not drawn when I go to the place in the map it should be.
I am attempting to do this programatically so maybe there is something I missed, if you spot it please let me know. There is no additional code
import UIKit
import MapKit
class ViewController: UIViewController {
var locationManager : CLLocationManager?
var routeData : Route?
var routeCoordinates : [CLLocation] = []
var routeOverlay: MKOverlay?
let mapView : MKMapView = {
let map = MKMapView()
map.overrideUserInterfaceStyle = .dark
map.translatesAutoresizingMaskIntoConstraints = false
return map
}()
override func viewWillAppear(_ animated: Bool) {
setMapConstraints()
}
override func viewDidLoad() {
super.viewDidLoad()
if let routeJSON = self.getJSON() {
self.parseJSON(jsonData: routeJSON)
}
drawRoute(routeData: routeCoordinates)
print(routeCoordinates)
}
private func setMapConstraints() {
view.addSubview(mapView)
mapView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
//Omitted - functions for parsing local JSON file - tested and is working fine
//Function that handles drawing of route on map
func drawRoute(routeData: [CLLocation]) {
if routeCoordinates.count == 0 {
return
}
let coordinates = routeCoordinates.map { (location) -> CLLocationCoordinate2D in
return location.coordinate
}
DispatchQueue.main.async {
self.routeOverlay = MKPolyline(coordinates: coordinates, count: coordinates.count)
self.mapView.addOverlay(self.routeOverlay!, level: .aboveRoads)
let customEdgePadding: UIEdgeInsets = UIEdgeInsets(top: 40, left: 20, bottom: 40, right: 20)
self.mapView.setVisibleMapRect(self.routeOverlay!.boundingMapRect, edgePadding: customEdgePadding, animated: false)
}
}
}
extension ViewController : MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .systemIndigo
renderer.lineCap = .round
renderer.lineWidth = 3.0
return renderer
}
}
I'm struggling to get KVO updates within a callout already displayed.
My use case: I want to display on an open callout the real time distance between user location and the annotation I add to the map. Annotation does not change its position.
I add annotations to mapView, using a custom annotation I have defined. No issue here.
On each annotation selected, the callout displays all the information defined in the custom annotation
However, the distance is refreshed in the callout ONLY if I unselect the annotation and reselect it
The distance property is declared as #objc dynamic so it can be observed.
I compute the distance each time the user location change. This part works too.
I cannot figure out what I'm missing to have the callout updated without closing and reopening it.
The code I'm using is what is described here by Rob: Swift -How to Update Data in Custom MKAnnotation Callout?
So my question: is it possible to change realtime a value (observed) in a notificationView callout ? If yes is KVO the best approach ?
In the link below, how would be implemented the mapView viewFor method ?
Any example would be very helpful.
It's my first post here, so please if I did it wrong, let me know and I will provide more information and details.
But my situation is trivial: the standard callout performs Key-Value Observation (KVO) on title and subtitle. (And the annotation view observes changes to coordinate.). But how to display change of values in the current open callout ? That is the think I do not get.
CustomAnnotation class:
class CustomAnnotation: NSObject, MKAnnotation {
#objc dynamic var title: String?
#objc dynamic var subtitle: String?
#objc dynamic var coordinate: CLLocationCoordinate2D
#objc dynamic var distance: CLLocationDistance
var poiColor: String?
var poiPhone: String?
init(title: String, subtitle: String, coordinate: CLLocationCoordinate2D, poiColor: String, poiPhone: String, distance: CLLocationDistance) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.poiColor = poiColor
self.poiPhone = poiPhone
self.distance = distance
super.init()
}
}
CustomAnnotationView class:
class CustomAnnotationView: MKMarkerAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .required
canShowCallout = true
detailCalloutAccessoryView = createCallOutWithDataFrom(customAnnotation: annotation as? CustomAnnotation)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeAnyObservers()
}
override var annotation: MKAnnotation? {
didSet {
removeAnyObservers()
if let customAnnotation = annotation as? CustomAnnotation {
updateAndAddObservers(for: customAnnotation)
}
}
}
private var subtitleObserver: NSKeyValueObservation?
private var distanceObserver: NSKeyValueObservation?
private let subtitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
}
private extension CustomAnnotationView {
func updateAndAddObservers(for customAnnotation: CustomAnnotation) {
subtitleLabel.text = customAnnotation.subtitle
subtitleObserver = customAnnotation.observe(\.subtitle) { [weak self] customAnnotation, _ in
self?.subtitleLabel.text = customAnnotation.subtitle
}
let locationManager = CLLocationManager()
let theLatitude:CLLocationDegrees = (locationManager.location?.coordinate.latitude)!
let theLongitude:CLLocationDegrees = (locationManager.location?.coordinate.longitude)!
// Get pin location
let pointLocation = CLLocation(latitude: customAnnotation.coordinate.latitude, longitude: customAnnotation.coordinate.longitude)
//Get user location
let userLocation = CLLocation(latitude: theLatitude, longitude: theLongitude)
// Return distance en meters
let distanceFromUser = pointLocation.distance(from: userLocation)
customAnnotation.distance = distanceFromUser*100
distanceLabel.text = String(format: "%.03f", customAnnotation.distance)+" cm"
distanceObserver = customAnnotation.observe(\.distance) { [weak self] customAnnotation, _ in
self?.distanceLabel.text = "\(customAnnotation.distance) cm"
}
}
func removeAnyObservers() {
subtitleObserver = nil
distanceObserver = nil
}
func createCallOutWithDataFrom(customAnnotation: CustomAnnotation?) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = true
view.addSubview(subtitleLabel)
view.addSubview(distanceLabel)
NSLayoutConstraint.activate([
subtitleLabel.topAnchor.constraint(equalTo: view.topAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
subtitleLabel.bottomAnchor.constraint(equalTo: distanceLabel.topAnchor),
distanceLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
distanceLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
distanceLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
if let customAnnotation = customAnnotation {
updateAndAddObservers(for: customAnnotation)
}
return view
}
}
And to finish:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let annotation = annotation as? CustomAnnotation
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "CustomAnnotation") as? CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: "CustomAnnotation")
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}
Thank you.
You would appear to have correctly configured the observers for the subtitle and distance. The problem is that a change in location is not triggering an update to distance. Thus, there is nothing triggering the KVO.
You have an observer for distance, which will trigger an update of the label. But you are not changing distance. You should remove the CLLocationManager code from that routine where you add the observers, and instead create a location manager (not within the annotation view, though) which uses its delegate to update all of the annotation distances, e.g.:
class ViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.distanceFilter = 5
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let currentLocation = locations.last(where: { $0.horizontalAccuracy >= 0 }) else { return }
mapView.annotations
.compactMap { $0 as? CustomAnnotation }
.forEach {
$0.distance = CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude)
.distance(from: currentLocation)
}
}
}
Obviously, you would remove the CLLocationManager code from updateAndAddObservers.
I am trying to create an application, one of its function is to drawing the line while users are moving.
Here is the class
class traceuserViewController: UIViewController,CLLocationManagerDelegate, MKMapViewDelegate {
var locationManager = CLLocationManager()
var startLocation: CLLocation?
var endLocation: CLLocation?
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.locationManager.distanceFilter = 30.0
self.locationManager.startMonitoringSignificantLocationChanges()
self.locationManager.startUpdatingLocation()
mapView.showsUserLocation = true
mapView.mapType = .hybrid
self.mapView.delegate = self
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//user's current location
let nowlocation = locations.last
userLocations.append(nowlocation!)
print("HERE IS THE LOCATION ARRAY")
print(userLocations)
//show the current location region
let center = CLLocationCoordinate2D(latitude: nowlocation!.coordinate.latitude, longitude: nowlocation!.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.7, longitudeDelta: 0.7))
self.mapView.setRegion(region, animated: true)
drawRoute(locationArray: userLocations)
}
func drawRoute(locationArray: [CLLocation]) {
if (locationArray.count) > 1 {
var destinationLocIndex = (locationArray.count) - 1
var startLocIndex = (locationArray.count) - 2
let destinationloc = locationArray[destinationLocIndex].coordinate
let startLoc = locationArray[startLocIndex].coordinate
var routeArray = [startLoc, destinationloc]
//test if the function works well or not
print(routeArray)
var geodesicLine = MKGeodesicPolyline(coordinates: routeArray , count: routeArray.count)
mapView.add(geodesicLine, level: .aboveRoads)
}
}
//draw in the mapview
private func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer! {
if overlay is MKPolyline{
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = UIColor.blue
polylineRenderer.lineWidth = 5.0
return polylineRenderer
}else{
os_log("Failed to draw the polyline", log: OSLog.default, type: .debug)
return nil
}
}
After many times trying, I still have no idea why it doesn't draw the route on the map when the user is moving, can anyone please I've me some hints?
cheers
I'm inferring that you are using Swift 3 from the code snippet (e.g. the signature of didUpdateLocations; the use of .hybrid rather than Swift 2.3's .Hybrid; etc.).
But, the signature for mapView(_:rendererFor:) is incorrect. In Swift 3, it is:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
...
}
If you ever have a delegate method that doesn't appear to work, add a breakpoint in it and you can confirm if it's called at all or not (and if it is called, you can step through it and diagnose the problem further).
this is my code for drawing first Line,
for another line with another color how can i do ?????
func mapView(mapView : MKMapView! , rendererForOverlay overlay: MKOverlay!) ->MKOverlayRenderer! {
if overlay is MKPolyline {
var polyLineRenderer = MKPolylineRenderer(overlay: overlay)
polyLineRenderer.strokeColor = UIColor.blackColor()
polyLineRenderer.lineWidth = 10
return polyLineRenderer
}
return nil
}
Finally i found the way :
In the top of the class i put
var toGo : MKPolyline?
var toCome : MKPolyline?
and after that in view Did load :
var polyLineGoes = MKPolyline(coordinates: &coordinateGoes, count: coordinateGoes.count)
toGo = polyLineGoes
mapView.addOverlay(polyLineGoes)
var polyLineComes = MKPolyline(coordinates: &coordinateComes, count: coordinateComes.count)
toCome = polyLineComes
mapView.addOverlay(polyLineComes)
at the End of class :
func mapView(mapView : MKMapView! , rendererForOverlay overlay: MKOverlay!) ->MKOverlayRenderer! {
if overlay is MKPolyline {
if ( toGo != nil) && (toCome != nil ) {
if overlay as? MKPolyline == toGo {
var polyLineRenderer = MKPolylineRenderer(overlay: overlay)
polyLineRenderer.strokeColor = UIColor.redColor()
polyLineRenderer.lineWidth = 3
return polyLineRenderer
} else if overlay as? MKPolyline == toCome {
print(overlay.description)
var polyLineRenderer = MKPolylineRenderer(overlay: overlay)
polyLineRenderer.strokeColor = UIColor.blueColor()
polyLineRenderer.lineWidth = 3
return polyLineRenderer
}
}
}
return nil
}
I was facing a same problem, but I just find another way to solve it.
In the mapView the code will be:
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer!{
if overlay is MKPolyline {
var polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = variables.lineColor
polylineRenderer.lineWidth = 4
return polylineRenderer
}
return nil
}
and I just have a struct named variables, and it contains variable lineColor. Also, I have another CONSTANT struct it has some CONSTANT to make code in mapView class more readable. The code in these two structs will be:
struct CONSTANT {
static let greenColor = UIColor.greenColor().colorWithAlphaComponent(0.5)
static let blueColor = UIColor.blueColor().colorWithAlphaComponent(0.5)
static let redColor = UIColor.redColor().colorWithAlphaComponent(0.5)
}
struct variables {
// let the default lineColor be green
static var lineColor = CONSTANT.greenColor
}
With these, I will just change my variables.lineColor to the color in CONSTANT struct. For example,
lineColor = CONSTANT.greenColor
mapView.addOverlay(myPolyLine, level: MKOverlayLevel.AboveRoads)
lineColor = CONSTANT.blueColor
mapView.addOverlay(myPolyLine, level: MKOverlayLevel.AboveRoads)
lineColor = CONSTANT.redColor
mapView.addOverlay(myPolyLine, level: MKOverlayLevel.AboveRoads)
First post here :D
I have found yet another way.
First, we need to extend MKPolyline:
extension MKPolyline {
struct ColorHolder {
static var _color: UIColor?
}
var color: UIColor? {
get {
return ColorHolder._color
}
set(newValue) {
ColorHolder._color = newValue
}
}
}
In viewDidLoad, we can assign every polyline a color now:
var polyline1 = MKPolyline(coordinates: coordinates1, count: coordinates1.count)
polyline1.color = UIColor(.red)
mapView.addOverlay(polyline1)
var polyline2 = MKPolyline(coordinates: coordinates2, count: coordinates2.count)
polyline2.color = UIColor(.green)
mapView.addOverlay(polyline2)
Finally, we have our mapView-function:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay_ = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = overlay_.color
return renderer
} else {
return MKOverlayRenderer()
}
}