1

I want to use web image as annotation on Apple Map. In other parts of my app I'm using SDWebImageSwiftUI, so I'd like to use SDWebImage also here. I know there is a SDWebImage plugin for MapKit, but it doesn't look like I can do some operations on image before displaying it. The effect I want to achieve is very close to the Facebook Messenger app:

Facebook Messenger screenshot

I store square images of user's in DigitalOcean Space (Amazon S3 compatible bucket), so it has to be loaded from the server. After fetching image I need to make it round and add white background (just like messenger app).

I create annotation view like this:

class MapUserAnnotationView: MKAnnotationView {
  static let reuseId = "quickEventUser"
  var photo: String?
  
  override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    if let ann = annotation as? MapQuickEventUserAnnotation {
      self.photo = ann.photo
    }
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func prepareForDisplay() {
    super.prepareForDisplay()
    image = "???"
    // Here I need to load image and make edit (probably)
  }
}

I tried something like this:

  override func prepareForDisplay() {
    super.prepareForDisplay()
    let img = UIImageView()
    img.sd_setImage(with: URL(string: photo ?? ""), completed: {_,_,_,_ in })
    image = img.image
  }

but it's not working (annotation is not displayed, completed is not called and there is no image load error log)

I know I'm missing something trivial, but I have small experience with MapKit, and I cannot find any source about implementing something similar.

How should I implement it? Is my way of thinking close to correct solution?

Maksymilian Tomczyk
  • 1,100
  • 8
  • 16

2 Answers2

2

In your prepareForDisplay method you are trying to use the image downloaded by SDWebImage before it has downloaded as you set image = img.image outside of the SDWebImage completion block.

However I don't think you'll be able to get the style you want using the default image property of your MKAnnotationView so would suggest creating a UIImageView styled as you wish and added as a subview of the MKAnnotationView.

Note that since you won't be using the default image property you'll need to manually set the frame of the MKAnnotationView.

Since you normally use MKAnnotationView by dequeuing (i.e. it reuses views as you move around the map) you shouldn't update the content based on the annotation provided in the init but instead use didSet of the annotation property as this property will be set to the new annotation when the MKAnnotationView is dequeued.

Example implementation:

class MapUserAnnotationView: MKAnnotationView {
    static let reuseId = "quickEventUser"
    var photo: String?
    override var annotation: MKAnnotation? {
        didSet {
            if let ann = annotation as? MapQuickEventUserAnnotation {
                self.photo = ann.photo
            }
        }
    }

    let imageView: UIImageView = {
        let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        imageView.layer.cornerRadius = 25.0
        imageView.layer.borderWidth = 3.0
        imageView.layer.borderColor = UIColor.white.cgColor
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        return imageView
    }()

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        addSubview(imageView)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepareForDisplay() {
        super.prepareForDisplay()
        if let photoURL = photo {
            let url = URL(string: photoURL)
            imageView.sd_setImage(with: url)
        } else {
            imageView.image = nil
        }
    }
}
samaitch
  • 1,172
  • 7
  • 9
  • Cool, I already figured it out, but another way (you still were first here). Didn't know about didSet {} in context of annotations, thanks! Also, is `UIImageView` border inset or not? – Maksymilian Tomczyk Jun 24 '21 at 10:29
0

First and short solution

Okay, so after some research I found short and clean solution for this. Problem with .sd_setImage completion block not being called I fixed according to this SO post: SDWebImage download image completion block not being called. After this one it was really easy to style annotations - everything what I needed is already included in SDWebImage. My prepareForDisplay() function finally looks like this:

override func prepareForDisplay() {
  super.prepareForDisplay()
  let url = URL(string: photo ?? "")
  SDWebImageManager.shared.loadImage(with: url, options: .scaleDownLargeImages, progress: nil, completed: { resultImage,_,_,_,_,_ in
    DispatchQueue.main.async {
      let size = 40
      guard let result = resultImage else { return }
      guard let resized = result.sd_resizedImage(with: CGSize(width: size, height: size), scaleMode: .aspectFit) else { return }
      guard let modified = resized.sd_roundedCornerImage(withRadius: CGFloat(size / 2), corners: .allCorners, borderWidth: 5, borderColor: .white) else { return }
      self.image = modified
      }
    })
  }

Of course it lacks proper handling of image load/modification errors (it just returns, so we can end up without any annotation), but it's not the case in this question, so I'm not posting it here to keep code clean. But there is another problem. Border rendered with sd_roundedCornerImage is inset border, which is not what I want. To address this issue I've created UIImage extension.

Second solution

UIImage extension:

import UIKit

extension UIImage {
  func transformToMapAnnotation(stroke: CGFloat = 6, color: CGColor = UIColor.white.cgColor) -> UIImage {
    UIGraphicsBeginImageContext(CGSize(width: size.width + 2 * stroke, height: size.height + 2 * stroke))
    let context = UIGraphicsGetCurrentContext()!
    context.setFillColor(color)

    let outerRect = CGRect(x: 0, y: 0, width: self.size.width + 2 * stroke, height: self.size.height + 2 * stroke)
    let outerPath = UIBezierPath(roundedRect: outerRect, cornerRadius: (size.height + 2 * stroke) / 2)
    let innerRect = CGRect(x: stroke, y: stroke, width: self.size.width, height: self.size.height)
    let innerPath = UIBezierPath(roundedRect: innerRect, cornerRadius: size.height / 2)

    outerPath.fill()
    innerPath.addClip()
    self.draw(at: CGPoint(x: stroke, y: stroke))
    
    let result = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return result!
  }
}

And now my prepareForDisplay() function looks like this:

override func prepareForDisplay() {
  super.prepareForDisplay()
  let url = URL(string: photo ?? "")
  SDWebImageManager.shared.loadImage(with: url, options: .scaleDownLargeImages, progress: nil, completed: { resultImage,_,_,_,_,_ in
    DispatchQueue.main.async {
      let size = 30
      guard let result = resultImage else { return }
      guard let resized = result.sd_resizedImage(with: CGSize(width: size, height: size), scaleMode: .aspectFit) else { return }
      let modified = resized.transformToMapAnnotation()
      self.image = modified
    }
  })
}
Maksymilian Tomczyk
  • 1,100
  • 8
  • 16