Performance Issues with ActionButton in MarketplaceKit – XPC Calls Causing UI Hangs

Hi all,

I’m working on the alternative marketplace app and using MarketplaceKit and ActionButton. On the main page, users see a list of items, each with an ActionButton. I’m experiencing significant UI hangs when this page loads.

What I’ve Observed:

  • Instruments (Hangs and SwiftUI profilers) show that the hangs occur when ActionButton instances are rendered.
  • Creating or updating ActionButton properties triggers synchronous XPC communication with the managedappdistributiond process on the main thread.
  • Each XPC call takes about 2-3 ms, but with many ActionButtons, the cumulative delay is noticeable and impacts the user experience.
  • I have tested on iOS 18.7 and 26.1, using Xcode 26.2. But in general, the issue is not specific to a device or iOS version.
  • The problem occurs in both Debug and Release builds.
  • Hangs can be severe depending on the number of items in a section, generally between 200-600 ms, resulting in noticeable lag and a poor user experience.
  • I haven’t found much documentation on the internal workings of ActionButton or why these XPC calls are necessary.

I have tried Lazy loading and reducing the amount of ActionButton instances. That makes the hangs less noticeable, but there are still hitches when new sections with items are added to the view hierarchy.

This is not an issue with SwiftUI or view updates in general. If I replace ActionButton with UIButton, the hangs are completely gone.

Minimal Example:

Here’s a simplified version of how I’m using ActionButton in my SwiftUI view. The performance issue occurs when many of these views are rendered in the list:

struct ActionButtonView: UIViewRepresentable {
    let viewModel: ActionButtonViewModel
    let style: ActionButtonStyle

    func makeUIView(context: Context) -> ActionButton {
        return ActionButton(action: viewModel.action)
    }

    func updateUIView(_ uiView: ActionButton, context: Context) {
        uiView.update(\.size, with: context.coordinator.size)
        uiView.update(\.label, with: viewModel.title)
        uiView.update(\.isEnabled, with: context.environment.isEnabled)
        uiView.update(\.fontSize, with: style.scaledFont(for: viewModel.title))
        uiView.update(\.backgroundColor, with: style.backgroundColor.color)
        uiView.update(\.tintColor, with: style.textAndIconColor)
        uiView.update(\.cornerRadius, with: style.cornerRadius(height: uiView.frame.size.height))
        uiView.update(\.accessibilityLabel, with: viewModel.accessibilityLabel)
        uiView.update(\.accessibilityTraits, with: .button)
        uiView.update(\.accessibilityUserInputLabels, with: viewModel.accesibilityUserInputLabels)
        uiView.update(\.tintAdjustmentMode, with: .normal)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(viewModel: viewModel)
    }

    class Coordinator: NSObject {
      ...
    }
}

extension ActionButton {
    fileprivate func update<T>(_ keyPath: WritableKeyPath<ActionButton, T>, with value: T)
    where T: Equatable {
        if self[keyPath: keyPath] == value { return }
        var mutableSelf = self
        mutableSelf[keyPath: keyPath] = value
    }
}

From the Instruments samples, it’s clear that the performance issues originate from NativeActionButtonView.makeUIView(context:) and NativeActionButtonView.updateUIView(_:context:). The following stack trace is common for all blocking calls, indicating that NSXPCConnection is being used for cross-process communication:

mach_msg2_trap	
mach_msg2_internal	
mach_msg_overwrite	
mach_msg	
_dispatch_mach_send_and_wait_for_reply	
dispatch_mach_send_with_result_and_wait_for_reply	
xpc_connection_send_message_with_reply_sync	
__NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__	
-[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:]	
___forwarding___	
_CF_forwarding_prep_0	
__35-[_UISlotView _setContentDelegate:]_block_invoke_2	
-[_UISlotView _updateContent]	
...
NativeActionButtonView.sizeThatFits(_:uiView:context:)	
protocol witness for UIViewRepresentable.sizeThatFits(_:uiView:context:) in conformance NativeActionButtonView	
...

Additionally, the Thread State Trace shows that during the XPC calls, the main thread is blocked and is later made runnable by managedappdistributiond. This confirms that the app is indeed communicating with the managedappdistributiond process.

Since there is limited documentation and information available, I have some questions:

  • Is there a way to batch update ActionButton properties to reduce the number of XPC calls?
  • Is it possible to avoid or defer XPC communication when creating/updating ActionButton instances?
  • Are there best practices for efficiently rendering large numbers of ActionButtons in SwiftUI?
  • Is this a known issue, and are there any recommended workarounds?
  • Can Apple provide more details on ActionButton’s internal behavior and XPC usage?

Any insights or suggestions would be greatly appreciated!

Performance Issues with ActionButton in MarketplaceKit – XPC Calls Causing UI Hangs
 
 
Q