CABTMIDICentralViewController in SwiftUI broken on iOS 27

When wrapping CABTMIDICentralViewController in a UIViewControllerRepresentable and presenting modally, I experience layout crashes since iOS 27. Does anyone have a workaround?

import SwiftUI
import CoreAudioKit

struct BluetoothButton: View {
    @State var showsBluetooth = false
    let title = "Check for Bluetooth MIDI Devices"

    var body: some View {
        #if os(macOS)
        Button {
            let wc = CABTLEMIDIWindowController()
            wc.showWindow(nil)
        } label: { label }
        #else
        Button {
            showsBluetooth = true
        } label: { label }
        .sheet(isPresented: $showsBluetooth) {
            BluetoothCentralView()
        }
        #endif
    }

    var label: some View {
        Text(title)
    }
}

#if os(iOS)

struct BluetoothCentralView: UIViewControllerRepresentable {

    func makeUIViewController(context: Context) -> CABTMIDICentralViewController {
 
        return CABTMIDICentralViewController()
    }

    func updateUIViewController(_ uiViewController: CABTMIDICentralViewController, context: Context) {

    }
}

#endif
Answered by DTS Engineer in 909800022

Thanks for posting the code, which reproduced the crash on the first try. Giving the view controller its table view before presenting it avoids the crash in my testing. Details and code are below.

On an iPhone running iOS 27.0 (24A427), presenting CABTMIDICentralViewController crashes with this exception:

*** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer bounds contains NaN: [0 0; 440 nan]. Layer: <CALayer: ...; delegate = <UITableView: ...>

The exception comes from -[CABTMIDICentralViewController loadView], which creates its table view with a height that isn't a number. It happens with your sheet, and also when I present the view controller with the UIKit present(_:animated:) method, without SwiftUI. On an iPhone running iOS 26.6.2, the same app presents the view controller, and it searches for MIDI devices.

The CABTMIDICentralViewController documentation says to "either present it modally or push it onto a UINavigationController view controller." It adds that "No other configuration of the object is necessary." Your code does exactly that. Please file a report in Feedback Assistant with your sample and the crash log, and post the FB number here. Just before the exception, the console logs "-[UIApplication statusBarHeightForOrientation:] API has been deprecated and is a no-op on 27.0 and later." Please include that line in the report too.

The workaround builds on documented behavior. The view documentation says that if you access the property "when its value is nil, the view controller automatically calls the loadView() method." When you set the table view first, the view controller's own loadView() doesn't run:

func makeUIViewController(context: Context) -> CABTMIDICentralViewController {
    let controller = CABTMIDICentralViewController()
    if #available(iOS 27, *) {
        controller.tableView = UITableView(frame: .zero, style: .grouped)
    }
    return controller
}

With that change on iOS 27.0, the view controller appears and searches for MIDI devices. I didn't have a Bluetooth MIDI peripheral to test discovery and pairing, so please confirm those with your hardware. This depends on an undocumented detail: the view controller doesn't need its own loadView(). The #available check keeps the standard behavior on earlier releases.

The workaround in the earlier reply replaces -[UIView initWithFrame:] for every view in the app. This one changes only the Bluetooth MIDI view controller.

If building your own discovery interface is an option, Core MIDI documents another route. MIDI Bluetooth explains that since iOS 16, "the system automatically reconnects Bluetooth Low Energy (BLE) MIDI peripherals when powered on, if the device supports pairing." It also describes how to connect other peripherals with Core Bluetooth and MIDIBluetoothDriverActivateAllConnections().

CABTWorkaround.mm

#import <objc/runtime.h>
#import <UIKit/UIKit.h>
#import <cmath>

static void swapMethods(Class c, SEL origSel, SEL swizSel)
{
    Method origMethod = class_getInstanceMethod(c, origSel);
    Method swizMethod = class_getInstanceMethod(c, swizSel);
    if (class_addMethod(c, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod))) {
        class_replaceMethod(c, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }
    else {
        method_exchangeImplementations(origMethod, swizMethod);
    }
}

@implementation UIView (CABTFix)

- (id)initWithFrame_CABTFIX_:(CGRect)frame
{
    if (std::isnan(frame.origin.y)) {
        frame.origin.y = 0.;
    }
    if (std::isnan(frame.size.height)) {
        frame.size.height = 0.;
    }
    return [self initWithFrame_CABTFIX_:frame];
}

@end

class CABTFix {
public:
    CABTFix() {
        if (@available(iOS 27, *)) {
            swapMethods([UIView class], @selector(initWithFrame:), @selector(initWithFrame_CABTFIX_:));
        }
    }
};

static CABTFix fix;

Thanks for posting the code, which reproduced the crash on the first try. Giving the view controller its table view before presenting it avoids the crash in my testing. Details and code are below.

On an iPhone running iOS 27.0 (24A427), presenting CABTMIDICentralViewController crashes with this exception:

*** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer bounds contains NaN: [0 0; 440 nan]. Layer: <CALayer: ...; delegate = <UITableView: ...>

The exception comes from -[CABTMIDICentralViewController loadView], which creates its table view with a height that isn't a number. It happens with your sheet, and also when I present the view controller with the UIKit present(_:animated:) method, without SwiftUI. On an iPhone running iOS 26.6.2, the same app presents the view controller, and it searches for MIDI devices.

The CABTMIDICentralViewController documentation says to "either present it modally or push it onto a UINavigationController view controller." It adds that "No other configuration of the object is necessary." Your code does exactly that. Please file a report in Feedback Assistant with your sample and the crash log, and post the FB number here. Just before the exception, the console logs "-[UIApplication statusBarHeightForOrientation:] API has been deprecated and is a no-op on 27.0 and later." Please include that line in the report too.

The workaround builds on documented behavior. The view documentation says that if you access the property "when its value is nil, the view controller automatically calls the loadView() method." When you set the table view first, the view controller's own loadView() doesn't run:

func makeUIViewController(context: Context) -> CABTMIDICentralViewController {
    let controller = CABTMIDICentralViewController()
    if #available(iOS 27, *) {
        controller.tableView = UITableView(frame: .zero, style: .grouped)
    }
    return controller
}

With that change on iOS 27.0, the view controller appears and searches for MIDI devices. I didn't have a Bluetooth MIDI peripheral to test discovery and pairing, so please confirm those with your hardware. This depends on an undocumented detail: the view controller doesn't need its own loadView(). The #available check keeps the standard behavior on earlier releases.

The workaround in the earlier reply replaces -[UIView initWithFrame:] for every view in the app. This one changes only the Bluetooth MIDI view controller.

If building your own discovery interface is an option, Core MIDI documents another route. MIDI Bluetooth explains that since iOS 16, "the system automatically reconnects Bluetooth Low Energy (BLE) MIDI peripherals when powered on, if the device supports pairing." It also describes how to connect other peripherals with Core Bluetooth and MIDIBluetoothDriverActivateAllConnections().

The workaround by DTS Engineer has an issue. No bluetooth devices appear. [CABTMIDICentralViewController loadView] is a method that needs to be called. Since it appears to perform some special processing, I think my workaround is better in that it only intervenes when the values are invalid. The workaround suggested by DTS Engineer never calls loadView at all.

Yes, the workaround posted by DTS Engineer doesn't work at all.

Thanks for testing that, and for pointing to loadView(). I tested with a Bluetooth LE MIDI adapter on iOS 27.0 (24A427), and the result agrees: the workaround in my earlier reply doesn't work. Setting tableView first skips the view controller's viewDidLoad() as well as its loadView(), and the view controller doesn't start its Bluetooth search. The list stays at "Searching for Bluetooth LE MIDI Devices..." and the adapter never appears.

Here's a corrected version. It adds one line, a call to viewDidLoad() after setting the table view:

func makeUIViewController(context: Context) -> CABTMIDICentralViewController {
    let controller = CABTMIDICentralViewController()
    if #available(iOS 27, *) {
        controller.tableView = UITableView(frame: .zero, style: .grouped)
        controller.viewDidLoad()
    }
    return controller
}

With this version, the adapter appeared in the list, connected, and delivered notes. It changes only this view controller, and it doesn't run the view controller's loadView(), where the crash occurs.

A UIView initWithFrame: NaN guard like yours also worked in the same test. It runs the view controller's own loadView(), and it applies to every view in the app. Both approaches depend on undocumented details of CABTMIDICentralViewController, so either one can stop working in a later release.

A report in Feedback Assistant is still how the crash itself gets fixed. If anyone files one, please post the FB number here.

Filed as FB25301268.

Confirmed on an iPad Pro running iOS 27.0.1 (24A446): presenting CABTMIDICentralViewController crashes in loadView with "CALayer bounds contains NaN: [0 0; 1032 nan]". Changing the presentation (SwiftUI sheet, full screen cover) makes no difference. The same code works on iOS 26.

The workaround from this thread (set tableView to a grouped UITableView(frame: .zero), then call viewDidLoad(), on iOS 27 only) works on my iPad Pro: the controller opens, BLE MIDI devices are listed, and I can connect a device and send MIDI from my app. I used it in a shipping app update.

This bug seems to be fixed in iOS 27.2.
Thanks everyone!