Animating annotations position change in MKMapView

Annotations are mainly used to displays static “pins” in MKMapView but sometimes you might need to make them move and animate their position changes so it looks better to the users.

There are a few things you need to do to achieve this.

Coordinate property specifics

The MKAnnotation protocol has a coordinate property that is used by the MKMapView to position the annotation to its corresponding location on the map.

If you just update the property you will quickly see that nothing happens, the annotation does not move on the map.

MKMapView uses KVO to know when the coordinate property changes so in Swift you need to mark the coordinate property in your MKAnnotation as @objc dynamic to make it work

final class LocationViewModel: NSObject, MKAnnotation {
    @objc dynamic coordinate: CLLocationCoordinate2D

    ...
}

With this change you will notice that the annotation now moves on the map, but it is not smooth, it basically jumps from the old position to the new one.

[Read More]
iOS  Xcode  MapKit 

Clustering annotations in MKMapView

If you need to display many annotations in your MKMapView it is recommended to cluster them for better performance.

Map clustering

This means instead of showing all the visible annotations you group annotations that are close together into one single annotation cluster representing them instead.

This cluster annotation usually shows the number of annotations it represents. As you then zoom in to get finer detail the clusters break up and show the actual annotations.

Clustering is supported in MKMapView on iOS 11 and newer, no need to use any custom library. If you need to support older versions of iOS, there are libraries like Cluster that you can use.

Custom cluster view implementation

Let’s say you use a custom annotation view to show your annotations and you want to add support for clustering. You first create a custom MKAnnotationView in the same way

final class LocationDataMapClusterView: MKAnnotationView {

    // MARK: Initialization
    private let countLabel = UILabel()

    override var annotation: MKAnnotation? {
    	didSet {
			 guard let annotation = annotation as? MKClusterAnnotation else {
            	assertionFailure("Using LocationDataMapClusterView with wrong annotation type")
            	return
        	}

    		countLabel.text = annotation.memberAnnotations.count < 100 ? "\(annotation.memberAnnotations.count)" : "99+"
    	}
    }

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

		displayPriority = .defaultHigh
        collisionMode = .circle

        frame = CGRect(x: 0, y: 0, width: 40, height: 50)
        centerOffset = CGPoint(x: 0, y: -frame.size.height / 2)

       
        setupUI()
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Setup
    private func setupUI() {
        ...
    }
}

The idea is the same as when custom annotation view, but there is a difference. You need to define displayPriority to tell the map that your cluster annotation has a higher priority that normal annotation.

[Read More]
iOS  Xcode  MapKit 

Using custom annotation views in MKMapView

If you want to display completely custom views as “pins” on the map in your iOS application, you should use annotations. All your data needs to be represented as objects conforming to the MKAnnotation protocol, with title, subtitle and coordinate as the required properties.

Custom view implementation

Visually you represent an MKAnnotation with a MKAnnotationView. You can create a custom class that subclasses MKAnnotationView and implement your custom UI in that class.

Here is an sample MKAnnotationView with fixed size that displays just one custom view

final class LocationAnnotationView: MKAnnotationView, Reusable {

    // MARK: Initialization

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

        frame = CGRect(x: 0, y: 0, width: 40, height: 50)
        centerOffset = CGPoint(x: 0, y: -frame.size.height / 2)

        canShowCallout = true
        setupUI()
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: Setup

    private func setupUI() {
        backgroundColor = .clear

        let view = MapPinView()
        addSubview(view)

        view.frame = bounds
    }
}

The MKAnnotationView is by default aligned to its corresponding position on map with the bottom left corner. If your MKAnnotationView looks like a pin for example, you need to align it to the position on the map with the bottom center point. To do that you use the centerOffset property as shown.

Registering the custom view with MKMapView

The next step is to tell MKMapView to user your custom class.

Using single custom MKAnnotationView

If you want to display only one type of annotation in your MKMapView, just register your custom class with the MKMapViewDefaultAnnotationViewReuseIdentifier reuse identifier

mapView.register(LocationAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

This is enough to make MKMapView to completely handle creating and recycling instances of your custom MKAnnotationViews for you. No need to implement any MKMapView delegate methods for providing annotation views.

[Read More]
iOS  Xcode  MapKit 

Logging error messages from assert and fatalerror

I often use fatalerror(message:) in my code base to deal with invalid states when the application cannot continue. A typical example can be a method that requires to be called only after the user has logged in:

guard let loggedUser = dataStore.user else {
	fatalerror("Invalid use before signup is complete")
}

The problem is that the fatalerror message does not appear in the crash log. You can of course take a look at the whole stack trace to figure out where the fatalerror originated but seeing the message in the logs yout get from your uses immediately would be much better.

I use PLCrashReporter to store crash logs locally so users can export them from the application together with all the logs.

I tried logging the message every time before calling fatalerror

guard let loggedUser = dataStore.user else {
	Log.error?.message("Invalid use before signup is complete")
	fatalerror("Invalid use before signup is complete")
}

but this is really not ideal, it is just writing boilerplate code you can easily forget.

I have not found a way to directly log the fatalerror message, so I created my own fail method

func fail(_ logMessage: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
    let formattedMessage = formatLogMessage(logMessage, file: file, function: function, line: line)
    Log.error?.message(formattedMessage)
    fatalError(formattedMessage, file: file, line: line)
}

You can format the message you log any way you want, I just log the filename, function name and line number. Getting the filename from a StaticString is a bit tricky though

func formatLogMessage(_ logString: String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) -> String {
    let filename = (file.withUTF8Buffer {
        String(decoding: $0, as: UTF8.self)
    } as NSString).lastPathComponent
    return "[\(filename):\(line) \(function)]: \(logString)"
}

Instead of calling fatalerror(message:) I now call fail(message:) instead in all the places it is needed and the message is always logged.

As a downside if you have debugger attached it stops in the actual fatalerror call not on the fail method call, so you need to move one method up in the stack trace to see the actual place your application failed.

For me it is worth it, I am much more interested in the error messages in the logs than this.

In my code base I also define a failDebug(message:) method with the same code just replacing fatalerror(message:) with assertionFailure(message:).

[Read More]
iOS  Xcode 

Workaround for Swift scripts crashing after update to Xcode 11.4

If you have already updated to Xcode 11.4 that ships with Swift 5.2 you might have noticed that your Swift scripts all started crashing when accessing anything related to URL.

I use a script to generate a list of libraries used in the app and their licenses and running it now results in

Stack dump:
0.	Program arguments: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -interpret test.swift -enable-objc-interop -stack-check -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name test
1.	Apple Swift version 5.2 (swiftlang-1103.0.32.1 clang-1103.0.32.29)
2.	While running user code "fetch_licenses.swift"
0  swift                    0x000000010e62d4ea PrintStackTraceSignalHandler(void*) + 42
1  swift                    0x000000010e62ccc0 SignalHandler(int) + 352
2  libsystem_platform.dylib 0x00007fff71c8242d _sigtramp + 29
3  libsystem_platform.dylib 0x0000000000004936 _sigtramp + 2386044198
4  libsystem_platform.dylib 0x00000001172ce020 _sigtramp + 2774842384
5  swift                    0x000000010a3b09ba llvm::MCJIT::runFunction(llvm::Function*, llvm::ArrayRef<llvm::GenericValue>) + 458
6  swift                    0x000000010a3b7a2b llvm::ExecutionEngine::runFunctionAsMain(llvm::Function*, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, char const* const*) + 2011
7  swift                    0x000000010a38caea performCompileStepsPostSILGen(swift::CompilerInstance&, swift::CompilerInvocation&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule> >, bool, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, swift::PrimarySpecificPaths const&, bool, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 14362
8  swift                    0x000000010a3814a5 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 55813
9  swift                    0x000000010a2f74d3 main + 1283
10 libdyld.dylib            0x00007fff71a847fd start + 1
11 libdyld.dylib            0x000000000000000b start + 2388113423
fish: '/usr/bin/env xcrun swift fetch_…' terminated by signal SIGSEGV (Address boundary error)

This is a Swift bug that has been already reported.

Luckily I found a workaround!.

If you do not run the script directly with swift but instead compile it first with swiftc and then run the binary, everything works just fine.

Update: The problem is caused by having other tools that bundle Swift libraries installed on the system, like swiftlint and uninstalling them solves the crash.

macOS  Xcode