130 Widgets

Semi-random thoughts and tales of tinkering

1. Your First iOS App — The Empty Canvas iOS

Before we touch anything related to audio or DSP, we need a running iOS app. This section walks you through creating an Xcode project, understanding the SwiftUI entry point, learning the basics of SwiftUI layout, and building a static placeholder UI for the VU meter we'll bring to life later.

If you've spent years in Visual Studio writing C# and XAML, you already know 80% of the concepts here — they just wear different clothes. Let's translate.

Creating the Xcode Project

Open Xcode and choose File → New → Project (or hit ⌘⇧N). Select the iOS → App template and click Next. Fill in the following:

Field Value
Product Name VUMeter
Team Your Apple ID (or "None" for now)
Organization Identifier Something like com.yourname
Interface SwiftUI
Language Swift

Click Next, choose a location on disk, and hit Create. Xcode generates a project with a handful of files. Let's map this to what you already know.

Project Structure — The Rosetta Stone

Here's how the Xcode project maps to a typical Visual Studio / .NET solution:

Xcode / iOS Visual Studio / .NET What It Does
VUMeter.xcodeproj VUMeter.sln The project/solution file. Contains build settings, targets, file references. You rarely edit it by hand.
VUMeter/ folder A C# project folder Contains your source code, assets, and configuration files — the actual "project."
VUMeterApp.swift Program.cs + App.xaml The app's entry point. Defines what launches when the app starts.
ContentView.swift MainWindow.xaml Your initial view — the first screen the user sees.
Assets.xcassets Resources folder App icons, colors, images. A structured asset catalog.
Info.plist (auto-generated) App.config / launchSettings.json App metadata: permissions, display name, supported orientations.
Tip

Unlike Visual Studio, Xcode uses a single window for everything: code editor, Interface Builder, debugger, simulator management, and provisioning. It takes a day or two to stop reaching for separate windows. Let it happen.

One key difference: there's no .csproj equivalent that you manually edit. Xcode manages build configuration through its GUI. You can dig into the underlying project.pbxproj file, but you'll almost never need to — and if you do, something has gone wrong.

VUMeterApp.swift — The Entry Point

Open VUMeterApp.swift. Xcode generated this for you:

import SwiftUI

@main
struct VUMeterApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Six lines. Let's unpack every one of them, because each carries weight.

import SwiftUI

This is Swift's equivalent of using in C#. It pulls in Apple's declarative UI framework. SwiftUI is to iOS what WPF/MAUI is to .NET — a declarative, data-driven way to define your user interface.

@main

This attribute marks the app's entry point — exactly like static void Main(string[] args) in a C# console app. There can only be one @main in the entire project. Swift doesn't have a Main method in the traditional sense; instead, @main tells the compiler "start here."

The App Protocol

VUMeterApp conforms to the App protocol. In C# terms, think of a protocol as an interface. The App protocol requires a single property: body, which returns a Scene. This is the root of your application's view hierarchy.

In WPF, your App.xaml defines the StartupUri that points to your main window. Here, the body property does the same job — it tells SwiftUI what to display.

Scene and WindowGroup

A Scene is a container for your app's content. WindowGroup is the most common scene type — it creates a window (on Mac) or a full-screen view (on iPhone). On iPad, it supports multiple windows.

Inside the WindowGroup, we instantiate ContentView() — our first and (for now) only screen. This is the SwiftUI equivalent of setting your WPF startup window.

iOS Concept

SwiftUI uses a struct for the app, not a class. In Swift, structs are value types (same as C#), but SwiftUI leans heavily on them for views. This is a deliberate design choice — views are lightweight descriptions of UI, not long-lived objects. SwiftUI recreates them constantly, and structs make that cheap.

SwiftUI Fundamentals for C# Developers

Before we build anything, let's cover the three concepts that'll feel new-but-familiar coming from WPF or MAUI.

Layout Stacks: VStack, HStack, ZStack

SwiftUI has three fundamental layout containers:

SwiftUI WPF/MAUI Equivalent What It Does
VStack StackPanel Orientation="Vertical" Stacks children top to bottom
HStack StackPanel Orientation="Horizontal" Stacks children left to right
ZStack Grid with overlapping cells / Canvas Stacks children on top of each other (back to front)

You nest these freely. A VStack inside an HStack inside a ZStack is completely normal. Unlike WPF's Grid with its row/column definitions, SwiftUI prefers composition over configuration — you build complex layouts by nesting simple containers.

Modifiers — The Fluent API

In WPF, you set properties on elements: <TextBlock FontSize="24" Foreground="Red" />. In SwiftUI, you chain modifiers:

Text("Hello")
    .font(.title)
    .foregroundStyle(.red)
    .padding()
    .background(.black)

If you've ever used a fluent builder pattern in C# — like LINQ chains or StringBuilder — this will feel natural. Each modifier returns a new view wrapping the previous one. Order matters: .padding() before .background() means the background includes the padding. Reverse them, and the padding sits outside the background.

Tip

A common beginner mistake is thinking modifiers mutate the view. They don't. Each modifier wraps the view in a new container. Think of it like new Padding(new Background(new Text("Hello"), .black), 16) — a Russian nesting doll of lightweight structs.

@State — Reactive State

In MVVM with WPF, you'd implement INotifyPropertyChanged (or use [ObservableProperty] with the CommunityToolkit) to make the UI react to data changes. SwiftUI has a built-in equivalent: @State.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.largeTitle)

            Button("Increment") {
                count += 1
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

When count changes, SwiftUI automatically re-renders the view. No manual PropertyChanged calls, no dispatcher, no binding syntax. You mutate the variable, and the UI updates. That's it.

@State is for simple, view-local state — like a private field in a ViewModel that only one view cares about. We'll encounter @ObservedObject and @StateObject later for shared state across views, but @State is enough for now.

iOS Concept

The body property is a computed property, not a stored one. SwiftUI calls it whenever state changes to get a fresh description of the UI. The framework then diffs the old and new descriptions and only updates what changed on screen — similar to how React's virtual DOM works, or WPF's dependency property system. You describe what the UI should look like; the framework handles the transitions.

Building the Placeholder UI

Now let's build something real. Open ContentView.swift and replace its contents with this:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 40) {
            Text("VU Meter")
                .font(.largeTitle)
                .bold()

            RoundedRectangle(cornerRadius: 12)
                .fill(Color.green)
                .frame(width: 80, height: 300)

            Text("-∞ dB")
                .font(.title2.monospacedDigit())
                .foregroundStyle(.secondary)

            Button("Start") { }
                .buttonStyle(.borderedProminent)
                .tint(.green)
        }
        .padding(40)
    }
}

Let's break down what each piece does, because this small example demonstrates most of the SwiftUI patterns you'll use throughout the project.

The VStack Container

VStack(spacing: 40) creates a vertical stack with 40 points of spacing between each child. The spacing parameter is like setting Margin between children in a WPF StackPanel — but more convenient because it's uniform and declared once.

The .padding(40) modifier on the entire VStack adds 40 points of inset on all sides, keeping the content away from the screen edges. This is equivalent to setting Padding="40" on a WPF panel.

The Title

Text("VU Meter") is the SwiftUI equivalent of a TextBlock. The .font(.largeTitle) modifier uses one of Apple's dynamic type sizes — it automatically adjusts based on the user's accessibility settings. .bold() does what you'd expect.

The Meter Bar (Placeholder)

RoundedRectangle(cornerRadius: 12) creates a shape — think of it like a Rectangle in WPF with RadiusX/RadiusY set. The .fill(Color.green) modifier fills it with green, and .frame(width: 80, height: 300) constrains its size.

Right now this is a static green bar. In later sections, we'll replace it with an animated bar whose height responds to audio input. The green block is our visual anchor — a promise of what's to come.

The dB Label

.font(.title2.monospacedDigit()) chains two font modifications: size (.title2) and digit style (.monospacedDigit()). Monospaced digits prevent the label from jittering when numbers change rapidly — each digit occupies the same width. This matters a lot for audio meters where values update many times per second.

.foregroundStyle(.secondary) applies the system's secondary text color — a muted gray that works in both light and dark mode.

The Start Button

Button("Start") { } creates a button with a label and an empty action closure. The closure ({ }) is where we'll eventually start audio capture. .buttonStyle(.borderedProminent) gives it a filled, rounded appearance — Apple's recommended style for primary actions. .tint(.green) overrides the default accent color.

Tip

Xcode's Canvas Preview (right side of the editor) shows your UI live as you type. If you don't see it, press ⌘⌥P to toggle it. It's like the XAML designer in Visual Studio, but it actually works reliably. You can interact with it, rotate the device, and even change accessibility settings — all without running the simulator.

Running in the Simulator

Time to see it run. At the top of the Xcode window, you'll see a device selector (it might say "iPhone 16" or "Any iOS Device"). Click it and choose a simulator — iPhone 16 is a good default.

Press ⌘R (or click the play button). Xcode will:

  1. Compile your Swift code (this takes a few seconds the first time).
  2. Boot the iOS simulator (a few more seconds the very first time).
  3. Install and launch your app inside the simulator.

You should see a centered green rectangle with "VU Meter" above it, a dB label below, and a green "Start" button at the bottom. The button doesn't do anything yet — tap it, confirm nothing happens, and feel good about that. We'll wire it up in Section 3.

iOS Concept

The iOS Simulator runs actual compiled ARM code (on Apple Silicon Macs) or x86 code (on Intel Macs). It's not an emulator — it runs your app natively on your Mac's CPU, just with an iOS-like environment wrapping it. This makes it very fast compared to the Android emulator. However, it can't simulate hardware sensors like the microphone with real audio input. For audio work, you'll eventually need a physical device. For now, the simulator is perfect for building our UI.

Common Issues

A few things that trip up developers coming from Visual Studio:

What We Built

Let's take stock. In this section you:

The app doesn't do anything yet, and that's the point. We have a clean, running canvas. In Section 2, we'll step away from code entirely and build the conceptual foundation: what sound actually is, how it becomes numbers, and how we'll measure it. Then in Section 3, we'll come back to Xcode and make this meter move.

Checkpoint

You should have a running iOS app in the simulator showing a static green bar, a title, a dB label, and a Start button. If you see that, you're ready for Section 2.