Skip to main content
The IPlugSwiftUI example demonstrates how to integrate SwiftUI for building modern, native plugin interfaces on Apple platforms. This approach provides access to the full SwiftUI ecosystem while maintaining bidirectional communication with your audio processing code.

What This Example Demonstrates

  • SwiftUI integration with iPlug2
  • Bidirectional parameter synchronization between SwiftUI and C++
  • Real-time audio visualization using Metal (via OscilloscopeView)
  • Custom SwiftUI controls (sliders, oscilloscope)
  • State management with @Published properties
  • Message passing between UI and audio code

Architecture Overview

C++ Plugin Layer

Audio processing, parameter definitions, DSP code (IPlugSwiftUI.mm)

View Controller

Bridge between C++ and SwiftUI (IPlugSwiftUIViewController.swift)

State Object

Observable state container (IPlugSwiftUIState.swift)

SwiftUI Views

UI components and layout (ContentView.swift)

C++ Plugin Implementation

Opening the SwiftUI Window

void* IPlugSwiftUI::OpenWindow(void* pParent)
{
  PLATFORM_VIEW* platformParent = (PLATFORM_VIEW*) pParent;
  auto* vc = [[IPlugSwiftUIViewController alloc] 
    initWithEditorDelegateAndBundleID: this : GetBundleID()];
  mViewController = vc;
  vc.view.frame = MAKERECT(0.f, 0.f, (float) PLUG_WIDTH, (float) PLUG_HEIGHT);
  [platformParent addSubview:vc.view];
  
  OnUIOpen();
  
  return vc.view;
}

Sending Data to SwiftUI

void IPlugSwiftUI::ProcessBlock(sample** inputs, sample** outputs, int nFrames)
{
  const double gain = GetParam(kParamGain)->Value() / 100.;
  const int nChans = NOutChansConnected();
    
  for (int s = 0; s < nFrames; s++) {
    for (int c = 0; c < nChans; c++) {
      outputs[c][s] = inputs[0][s] * gain;
    }
  }
  
  // Send audio data to SwiftUI for visualization
  mScopeSender.ProcessBlock(outputs, nFrames, kCtrlTagScope);
}

void IPlugSwiftUI::OnIdle()
{
  mScopeSender.TransmitData(*this);
}

Receiving Messages from SwiftUI

bool IPlugSwiftUI::OnMessage(int msgTag, int ctrlTag, int dataSize, const void* pData)
{
  if (msgTag == kMsgTagHello)
  {
    DBGMSG("MsgTagHello received on C++ side\n");
    return true;
  }
  else if(msgTag == kMsgTagRestorePreset)
  {
    RestorePreset(ctrlTag);
  }
  
  return CocoaEditorDelegate::OnMessage(msgTag, ctrlTag, dataSize, pData);
}

SwiftUI View Controller

The view controller bridges between C++ and SwiftUI:
@objc class IPlugSwiftUIViewController: IPlugCocoaViewController {
  lazy var state = IPlugSwiftUIState(
    beginEdit: self.beginInformHostOfParamChangeFromUI,
    doEdit: self.sendParameterValueFromUI,
    endEdit: self.endInformHostOfParamChangeFromUI,
    sendMsg: {
      self.sendArbitraryMsgFromUI(
        msgTag: kMsgTagHello, 
        ctrlTag: kCtrlTagButton, 
        msg: nil
      );
    }
  )
  
  override func viewDidLoad() {
    super.viewDidLoad()
      
    // Populate parameters from C++
    for idx in 0..<parameterCount() {
      state.params.append(Param(
        id: idx,
        name: getParameterName(idx),
        defaultValue: getParameterDefault(idx),
        minValue: getParameterMin(idx),
        maxValue: getParameterMax(idx),
        step: getParameterStep(idx),
        label: getParameterLabel(idx),
        group: getParameterGroup(idx)
      ))
    }
    
    let contentView = ContentView().environmentObject(state)
    let hostingController = PlatformHostingController(rootView: contentView)
    
    view.addSubview(hostingController.view)
    hostingController.view.pinToSuperviewEdges()
  }
  
  // Receive control messages from C++
  override func sendControlMsgFromDelegate(ctrlTag: Int, msgTag: Int, msg: Data!) {
    if (ctrlTag == kCtrlTagScope) {
      msg.withUnsafeBytes { (pointer: UnsafeRawBufferPointer) in
        let intSize = MemoryLayout<Int32>.size
        let byteOffset = intSize * 3
        let floatPointer = pointer.baseAddress!.advanced(by: byteOffset)
        let floats = UnsafeRawBufferPointer(
          start: floatPointer, 
          count: pointer.count - byteOffset
        ).bindMemory(to: Float.self)
        let floatArray = Array(floats.prefix(kScopeBufferSize))
        
        state.waveform = floatArray
      }
    }
  }
  
  // Receive parameter changes from C++
  override func onParamChangeUI(_ paramIdx: Int, _ value: Double) {
    state.params[Int(paramIdx)].value = value;
  }
}

State Management

The state object uses SwiftUI’s @Published property wrapper:
class IPlugSwiftUIState: NSObject, ObservableObject {
  var params: [Param] = []
  @Published var bundleID = String("")
  @Published @objc dynamic var waveform: [Float] = Array(
    repeating: 0.0, 
    count: kScopeBufferSize
  )

  let beginEdit: @MainActor (Int) -> Void
  let doEdit: @MainActor (Int, Double) -> Void
  let endEdit: @MainActor (Int) -> Void
  let sendMsg: @MainActor () -> Void

  public init(
    beginEdit: @MainActor @escaping (Int) -> Void = {_ in },
    doEdit: @MainActor @escaping (Int, Double) -> Void = {_,_ in },
    endEdit: @MainActor @escaping (Int) -> Void = {_ in },
    sendMsg: @MainActor @escaping () -> Void = {}
  ) {
    self.beginEdit = beginEdit
    self.doEdit = doEdit
    self.endEdit = endEdit
    self.sendMsg = sendMsg
    super.init()
  }
}
The @Published property wrapper automatically triggers UI updates when values change, making it perfect for real-time audio visualization.

SwiftUI Content View

struct ContentView: View {
  @EnvironmentObject var state: IPlugSwiftUIState
  
  var body: some View {
    ZStack {
      MeshGradient(width: 2, height: 2, points: [
        [0, 0], [1, 0], [0, 1], [1, 1]
      ], colors: [.red, .orange, .blue, .yellow])
      
      VStack {
        Text("iPlug2 + SwiftUI + Metal")
          .font(.title)
          .padding()
        
        HStack(alignment: .center) {
          OscilloscopeView()
            .environmentObject(state)
        }
      }
      .padding()
    }
    .edgesIgnoringSafeArea(.all)
  }
}

Communication Patterns

1

C++ to SwiftUI: Parameters

Use onParamChangeUI() to receive parameter updates from the host or automation.
2

C++ to SwiftUI: Control Data

Use sendControlMsgFromDelegate() to send custom data (like audio buffers for visualization).
3

SwiftUI to C++: Parameters

Call beginInformHostOfParamChangeFromUI(), sendParameterValueFromUI(), and endInformHostOfParamChangeFromUI() for gesture-based parameter editing.
4

SwiftUI to C++: Messages

Use sendArbitraryMsgFromUI() to send custom messages, handled in OnMessage().

AUv3 View Configuration

bool IPlugSwiftUI::OnHostRequestingSupportedViewConfiguration(int width, int height)
{
#ifdef OS_MAC
  // Logic/GB offer one option with 0w, 0h
  // Allow that so our AUv3 has "our" size as its 100% setting
  return ((width + height) == 0);
#else
  return true;
#endif
}
This ensures proper sizing behavior in Logic Pro and GarageBand when hosting your plugin as an AUv3.

Advantages of SwiftUI

Native Performance

Metal-accelerated rendering and platform-optimized layout

Modern API

Declarative syntax, automatic state management, reactive updates

Apple Ecosystem

Full access to SF Symbols, system colors, dark mode, accessibility

Cross-platform

Same code works on macOS and iOS with minimal changes
SwiftUI is only available on Apple platforms. For cross-platform plugins, consider using IGraphics or web-based UIs.