Skip to content

MKDistanceFormatter constructor incorrectly requires UI thread in .NET iOS binding #25617

@tipa

Description

@tipa

Apple platform

iOS

Framework version

net10.0-*

Affected platform version

.NET SDK 10.0.300, iOS workload 26.5.10284, Xcode 26.5, iOS Simulator 26.5

Description

MapKit.MKDistanceFormatter throws UIKit.UIKitThreadAccessException when constructed on a background thread in .NET iOS.

This has been observed on iOS only. The same app code path does not reproduce on macOS.

The exception appears to come from the managed iOS binding, not from native MapKit. I verified this with a native Swift iOS simulator repro that constructs and uses MKDistanceFormatter on a background queue. The native repro succeeds, including when launched with Xcode's Main Thread Checker injected and MTC_CRASH_ON_REPORT=1.

The .NET binding appears to call UIApplication.EnsureUIThread() in MapKit.MKDistanceFormatter..ctor before sending the native init message. IL inspection of Microsoft.iOS.dll shows:

MapKit.MKDistanceFormatter..ctor
  IL_0007: call         Void EnsureUIThread()
  ...
  IL_002a: call         IntPtr IntPtr_objc_msgSend(IntPtr, IntPtr)

Version information

  • Apple platform: iOS
  • Target framework: net10.0-ios
  • .NET SDK: 10.0.300
  • iOS workload: 26.5.10284
  • Xcode: 26.5
  • iOS Simulator: 26.5

Expected behavior

MKDistanceFormatter can be constructed and used on a background thread, unless Apple's native framework requires main-thread access for this type.

Actual behavior

The .NET iOS binding throws before native MKDistanceFormatter init is called:

UIKit.UIKitThreadAccessException: UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.
   at UIKit.UIApplication.EnsureUIThread()
   at MapKit.MKDistanceFormatter..ctor()

Steps to Reproduce

Create a net10.0-ios app and run this from app startup or any UI callback:

_ = Task.Run(() =>
{
    var formatter = new MapKit.MKDistanceFormatter();
    var value = formatter.StringFromDistance(1234);
    Console.WriteLine(value);
});

Result:

UIKit.UIKitThreadAccessException

A minimal repro project is attached.

Native Swift comparison

This native Swift iOS simulator repro succeeds on a background queue:

DispatchQueue.global(qos: .utility).async {
    print("background queue; main thread =", Thread.isMainThread)
    let formatter = MKDistanceFormatter()
    let formatted = formatter.string(fromDistance: 1234)
    print("native MKDistanceFormatter background result =", formatted)
}

Observed output on iOS Simulator 26.5:

background queue; main thread = false
native MKDistanceFormatter background result = 1,2 km

The same native app also succeeds with Xcode's Main Thread Checker injected and configured to crash on reports.

Repro project output

The attached .NET iOS repro prints:

MKDistanceFormatterBindingBugRepro.zip

Background thread: 3, main thread: False
Caught exception: UIKit.UIKitThreadAccessException
UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.
   at UIKit.UIApplication.EnsureUIThread() in /Users/builder/azdo/_work/1/s/macios/src/UIKit/UIApplication.cs:line 134
   at MapKit.MKDistanceFormatter..ctor() in /Users/builder/azdo/_work/1/s/macios/src/build/dotnet/ios/generated-sources/MapKit/MKDistanceFormatter.g.cs:line 71
   at MKDistanceFormatterBindingBugRepro.AppDelegate.RunBackgroundRepro() in /private/tmp/MKDistanceFormatterBindingBugRepro/Program.cs:line 51

Did you find any workaround?

Workarounds are to marshal MKDistanceFormatter usage to the main thread, disable UIKit cross-thread checks globally, or avoid MKDistanceFormatter and use custom distance formatting.

The first adds unnecessary main-thread work for background processing, and disabling checks globally is not desirable.

Relevant logs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIf an issue is a bug or a pull request a bug fix

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions