Skip to main content
iPlug2 supports building plugin UIs with Apple’s SwiftUI framework, providing native macOS and iOS interfaces with declarative syntax.
SwiftUI support requires macOS 10.15+ or iOS 13+. It works with all plugin formats on Apple platforms (AU, AUv3, VST3, CLAP).

Getting Started

1

Copy the example project

Start by duplicating the SwiftUI example:
./duplicate.py IPlugSwiftUI MyPlugin
2

Project structure

Your project will have:
MyPlugin/
├── MyPlugin.h              # Plugin header (C++)
├── MyPlugin.mm             # Plugin implementation (Objective-C++)
├── config.h                # Plugin configuration
└── UI/                     # SwiftUI views
    ├── ContentView.swift
    ├── MyPluginState.swift
    ├── MyPluginViewController.swift
    ├── Param.swift
    └── ParamSliderView.swift
3

Configure your plugin

Edit config.h to set your plugin details.

Plugin Implementation

Plugin Header (C++)

Your plugin class should inherit from Plugin and use CocoaEditorDelegate:
#pragma once

#include "IPlug_include_in_plug_hdr.h"
#include "ISender.h"

using namespace iplug;

constexpr int SCOPE_BUFFER_SIZE = 512;

class MyPlugin final : public Plugin
{
public:
  MyPlugin(const InstanceInfo& info);

  void* OpenWindow(void* pParent) override;
  void OnParentWindowResize(int width, int height) override;
  bool OnHostRequestingSupportedViewConfiguration(int width, int height) override;

  void OnIdle() override;
  void ProcessBlock(sample** inputs, sample** outputs, int nFrames) override;
  bool OnMessage(int msgTag, int ctrlTag, int dataSize, const void* pData) override;

  using OscilloscopeSender = IBufferSender<1, 1, SCOPE_BUFFER_SIZE>;
  OscilloscopeSender mScopeSender {-100.0};
};

Plugin Implementation (Objective-C++)

The .mm file bridges C++ and Swift:
#include "MyPlugin.h"
#include "IPlug_include_in_plug_src.h"

#ifdef FRAMEWORK_BUILD
#import <AUv3Framework/MyPlugin-Swift.h>
#import <AUv3Framework/MyPlugin-Shared.h>
#else
#import <MyPlugin-Swift.h>
#endif

const NSInteger kScopeBufferSize = SCOPE_BUFFER_SIZE;

MyPlugin::MyPlugin(const InstanceInfo& info)
: iplug::Plugin(info, MakeConfig(kNumParams, kNumPresets))
{
  GetParam(kParamGain)->InitDouble("Gain", 100., 0., 100.0, 0.01, "%");
  MakeDefaultPreset();
}

void* MyPlugin::OpenWindow(void* pParent)
{
  PLATFORM_VIEW* platformParent = (PLATFORM_VIEW*) pParent;
  auto* vc = [[MyPluginViewController 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;
}

void MyPlugin::OnIdle()
{
  // Send data from audio thread to UI
  mScopeSender.TransmitData(*this);
}

void MyPlugin::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 visualizer
  mScopeSender.ProcessBlock(outputs, nFrames, kCtrlTagScope);
}

bool MyPlugin::OnMessage(int msgTag, int ctrlTag, int dataSize, const void* pData)
{
  if (msgTag == kMsgTagHello)
  {
    DBGMSG("Message received from UI\n");
    return true;
  }
  
  return CocoaEditorDelegate::OnMessage(msgTag, ctrlTag, dataSize, pData);
}

SwiftUI Implementation

State Object

Create an observable state object to manage parameters:
import SwiftUI

class MyPluginState: 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()
  }
}

View Controller

Bridge between C++ and SwiftUI:
import SwiftUI

@objc class MyPluginViewController: IPlugCocoaViewController {
  lazy var state = MyPluginState(
    beginEdit: self.beginInformHostOfParamChangeFromUI,
    doEdit: self.sendParameterValueFromUI,
    endEdit: self.endInformHostOfParamChangeFromUI,
    sendMsg: {
      self.sendArbitraryMsgFromUI(msgTag: kMsgTagHello, ctrlTag: kCtrlTagButton, msg:nil);
    })
  
  override init!(editorDelegateAndBundleID editorDelegate: UnsafeMutableRawPointer!, 
                 _ bundleID: UnsafePointer<CChar>!) {
    super.init(editorDelegateAndBundleID: editorDelegate, bundleID)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
      
    // Load 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)
    state.bundleID = getBundleID()

    view.addSubview(hostingController.view)
    hostingController.view.pinToSuperviewEdges()
  }
  
  // Receive data from C++
  override func sendControlMsgFromDelegate(ctrlTag: Int, msgTag: Int, msg: Data!) {
    if (ctrlTag == kCtrlTagScope) {
      // Parse audio data and update state
      var floatArray: [Float] = []
      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)
        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;
  }
}

Content View

Create your SwiftUI interface:
import SwiftUI

struct ContentView: View {
  @EnvironmentObject var state: MyPluginState
  
  var body: some View {
    ZStack {
      LinearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing)
      
      VStack(spacing: 20) {
        Text("My Audio Plugin")
          .font(.title)
          .foregroundColor(.white)
        
        ParamSliderView(param: state.params[kParamGain])
          .frame(width: 50, height: 200)
        
        OscilloscopeView()
          .frame(height: 150)
        
        Button("Send Message") {
          state.sendMsg()
        }
        .buttonStyle(.borderedProminent)
      }
      .padding()
    }
    .edgesIgnoringSafeArea(.all)
  }
}

Parameter Control

Create reusable parameter controls:
import SwiftUI

struct ParamSliderView: View {
  @ObservedObject var param: Param
  @State private var isDragging = false
  
  var body: some View {
    VStack {
      Text(param.name)
        .font(.caption)
        .foregroundColor(.white)
      
      GeometryReader { geometry in
        ZStack(alignment: .bottom) {
          // Background
          Rectangle()
            .fill(Color.gray.opacity(0.3))
          
          // Value bar
          Rectangle()
            .fill(Color.blue)
            .frame(height: geometry.size.height * CGFloat((param.value - param.minValue) / (param.maxValue - param.minValue)))
        }
        .gesture(
          DragGesture(minimumDistance: 0)
            .onChanged { value in
              if !isDragging {
                // Begin gesture
                isDragging = true
              }
              
              // Calculate new value from drag position
              let ratio = 1.0 - (value.location.y / geometry.size.height)
              let newValue = param.minValue + ratio * (param.maxValue - param.minValue)
              param.value = max(param.minValue, min(param.maxValue, newValue))
            }
            .onEnded { _ in
              isDragging = false
            }
        )
      }
      
      Text(param.displayString())
        .font(.caption2)
        .foregroundColor(.white)
    }
  }
}

Communication Patterns

Sending Parameter Changes

The Param class handles parameter communication automatically:
// In Param.swift
class Param: ObservableObject {
  @Published var value: Double {
    didSet {
      // Automatically sends to C++
      state?.doEdit(id, toNormalized(value))
    }
  }
  
  func toNormalized(_ rawValue: Double) -> Double {
    return shape.valueToNormalized(rawValue, in: self)
  }
}

Receiving Audio Data

Use ISender classes to send data from DSP to UI:
// In ProcessBlock()
mScopeSender.ProcessBlock(outputs, nFrames, kCtrlTagScope);

// In OnIdle() - called on main thread
mScopeSender.TransmitData(*this);
// In ViewController
override func sendControlMsgFromDelegate(ctrlTag: Int, msgTag: Int, msg: Data!) {
  if ctrlTag == kCtrlTagScope {
    // Update published property
    state.waveform = parseFloatArray(from: msg)
  }
}

Best Practices

Use @Published for reactive updatesMark properties with @Published in your state object to automatically trigger view updates when values change.
Don’t call C++ from SwiftUI views directlyAlways route communication through the state object and view controller. This ensures thread safety and proper host notification.
Handle both macOS and iOSUse #if os(macOS) and #if os(iOS) to provide platform-specific code. Many SwiftUI components work on both platforms automatically.

AUv3 Support

For AUv3 plugins, handle view configuration requests:
bool MyPlugin::OnHostRequestingSupportedViewConfiguration(int width, int height)
{
#ifdef OS_MAC
  // Logic/GB offer 0,0 - allow that for proper scaling
  return ((width + height) == 0);
#else
  return true;
#endif
}

Example Project

See the complete example in Examples/IPlugSwiftUI/ which demonstrates:
  • Parameter controls with sliders
  • Real-time oscilloscope visualization
  • Bidirectional C++/Swift communication
  • macOS and iOS support
  • Custom SwiftUI controls