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 

Determining which frameworks use UIWebView

Apple has deprecated UIWebView and will not be accepting new apps with UIWebView starting April 2020 and app updates with UIWebView starting December 2020. If your app uses UIWebView you should replace it with WKWebView.

The check for UIWebView has already been implemented as a warning after a build submission:

ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of app updates that use UIWebView APIs starting from December 2020. See https://developer.apple.com/documentation/uikit/uiwebview for more information.

Removing UIWebView from your app might seem quite straightforward, but you probably use some 3rd party libraries and they might also contain UIWebView. You need to find all of them and update them, if available, or replace them. This process is not exactly trivial.

If you use 3rd party libraries as code, for example via Cocoapods, you can just do a text search for UIWebView in their sources.

For example doing grep -r 'UIWebView' . in RxSwift sources prior to 5.1.0 gives you a lot of results as RxSwift has included UIWebView binding before 5.1.0.

❯ grep -r 'UIWebView' .
./RxCocoa/iOS/UIWebView+Rx.swift://  UIWebView+Rx.swift
./RxCocoa/iOS/UIWebView+Rx.swift:    extension Reactive where Base: UIWebView {
./RxCocoa/iOS/UIWebView+Rx.swift:        public var delegate: DelegateProxy<UIWebView, UIWebViewDelegate> {
./RxCocoa/iOS/UIWebView+Rx.swift:                .methodInvoked(#selector(UIWebViewDelegate.webViewDidStartLoad(_:)))
./RxCocoa/iOS/UIWebView+Rx.swift:                .methodInvoked(#selector(UIWebViewDelegate.webViewDidFinishLoad(_:)))
./RxCocoa/iOS/UIWebView+Rx.swift:                .methodInvoked(#selector(UIWebViewDelegate.webView(_:didFailLoadWithError:)))
./RxCocoa/iOS/Proxies/RxWebViewDelegateProxy.swift:extension UIWebView: HasDelegate {
./RxCocoa/iOS/Proxies/RxWebViewDelegateProxy.swift:    public typealias Delegate = UIWebViewDelegate
...

If your 3rd party libraries come as a .framework file without sources, there is a different way to check for UIWebView.

You can use nm to get the symbols table of the executable in a .framework and do a grep there

❯ nm AWSDK.framework/AWSDK | grep -i UIWebView
                 U _OBJC_CLASS_$_UIWebView
                 U _OBJC_CLASS_$_UIWebView
0000000000002a00 S __OBJC_LABEL_PROTOCOL_$_UIWebViewDelegate
0000000000002998 D __OBJC_PROTOCOL_$_UIWebViewDelegate
0000000000002348 s l_OBJC_$_PROTOCOL_INSTANCE_METHODS_OPT_UIWebViewDelegate
00000000000023b0 s l_OBJC_$_PROTOCOL_METHOD_TYPES_UIWebViewDelegate
0000000000002330 s l_OBJC_$_PROTOCOL_REFS_UIWebViewDelegate
000000000000aaa0 S __OBJC_LABEL_PROTOCOL_$_UIWebViewDelegate
000000000000a858 D __OBJC_PROTOCOL_$_UIWebViewDelegate
0000000000005780 s l_OBJC_$_PROTOCOL_INSTANCE_METHODS_OPT_UIWebViewDelegate
00000000000057e8 s l_OBJC_$_PROTOCOL_METHOD_TYPES_UIWebViewDelegate
0000000000005768 s l_OBJC_$_PROTOCOL_REFS_UIWebViewDelegate
AWSDK.framework/AWSDK(UIWebView+LongPress.o):
...