🖥 Swift Programming for macOS

There are plenty of books, videos, and online resources for developing iOS apps. Despite the fact that iPhone and iPad apps require a Mac for code development, there is little information about actually creating native Mac applications. The examples provided below demonstrate various aspects of Mac app development using the latest versions of Swift and SwiftUI. Hopefully these examples will provide a useful resource for Mac developers. The code repository for this website is located on GitHub at wigging/swift-macos.

Questions, comments, and other feedback can be sent via Email or Twitter. Or submit an Issue on the GitHub repository at wigging/swift-macos. If you would like to support this project, donations can be made to Gavin Wiggins using GitHub Sponsors, Patreon, Buy Me A Coffee, or PayPal. Thank you 😄.

Contents

Getting started

The first step (other than getting a Mac) is to download Xcode from the Mac App Store.

If you’ve been using Xcode for a while, you should remove old simulators that are no longer supported to clear up some space. Use the following terminal command to delete the old simulators:

$ xcrun simctl delete unavailable

AppStorage

The @AppStorage property wrapper reads and writes values from UserDefaults. The example below saves a fruit name (a string) to the “fruit” key in UserDefaults. When the app is relaunched, the saved fruit name will be displayed in the text label. Enter a fruit in the text field then click the “Save fruit” button to save a new fruit to the “fruit” key in UserDefaults.

app storage property

import SwiftUI

struct ContentView: View {

    @State private var thefruit = ""
    @AppStorage("fruit") var fruit = ""

    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter fruit", text: $thefruit)
                .multilineTextAlignment(.center)
                .frame(maxWidth: 200)
            Button("Save fruit") {
                fruit = thefruit
            }
            Text("Saved fruit: \(fruit)")
        }
        .frame(width: 400, height: 100)
        .padding()
    }
}

Blur effect

The blur modifier can be used to blur a view.

import SwiftUI

struct ContentView: View {

    @State private var blurRadius: CGFloat = 0.0

    var body: some View {
        VStack(spacing: 40) {
            Text("Hello 😁")
                .font(.title)
                .blur(radius: blurRadius)

            Button("My Button"){}
                .blur(radius: blurRadius)

            Slider(value: $blurRadius, in: 0...20, minimumValueLabel: Text("0"), maximumValueLabel: Text("20")) {
                Text("Blur radius")
            }
            .padding()
        }
        .frame(width: 400, height: 300)
    }
}

blur effect

blur effect

Button styles

Several built-in button styles are available for macOS such as the PlainButtonStyle, LinkButtonStyle, and BorderlessButtonStyle. The BorderedButtonStyle is also the default button style. To create a custom appearance for a button, use the ButtonStyle protocol. To fully customize the button’s appearance and behavior, use the PrimitiveButtonStyle protocol.

button styles

import SwiftUI

struct RoundedButtonStyle: ButtonStyle {

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundColor(.black)
            .padding()
            .background(Color.yellow.cornerRadius(12))
            .scaleEffect(configuration.isPressed ? 0.95 : 1)
    }
}

struct BorderButtonStyle: ButtonStyle {

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundColor(configuration.isPressed ? .blue : .red)
            .padding(8)
            .overlay(
                RoundedRectangle(cornerRadius: 10)
                    .stroke(Color.purple, lineWidth: 3)
            )
            .onHover { hover in
                hover ? NSCursor.pointingHand.push() : NSCursor.pop()
            }
    }
}

struct DoubleTapButtonStyle: PrimitiveButtonStyle {

    @State private var tapped = false

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundColor(tapped ? .yellow : .white)
            .padding(10)
            .background(Color.red.cornerRadius(8))
            .onTapGesture(count: 2, perform: {
                tapped.toggle()
                configuration.trigger()
            })
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {

            // Default style
            Button("Default Style") {}

            // Plain button style
            Button("Plain Style") {}.buttonStyle(PlainButtonStyle())

            // Link button style
            Button("Link Style") {}.buttonStyle(LinkButtonStyle())

            // Borderless button style
            Button("Borderless Style") {}.buttonStyle(BorderlessButtonStyle())

            // Bordered button style
            Button("Bordered Style") {}.buttonStyle(BorderedButtonStyle())

            // Group button style
            VStack {
                Button("Button 1") {}
                Button("Button 2") {}
            }
            .buttonStyle(LinkButtonStyle())

            // Custom border button style
            Button("Custom Style") {}.buttonStyle(BorderButtonStyle())

            // Custom rounded button style
            Button("Rounded Style") {}.buttonStyle(RoundedButtonStyle())

            // Double tap button style
            Button("Double Tap Style") {}.buttonStyle(DoubleTapButtonStyle())
        }
        .frame(width: 400, height: 500)
    }
}

Credits

The About window in a Mac application is viewed by selecting the app’s name in the top menu bar, then choosing the About item in the menu. The image below displays Safari’s About window.

app credits safari

To add credits to the About window, add a file named Credits.rtf to the Xcode project. The text in the RTF file will appear in the About window below the app version and copyright. For example, the following text in Credits.rtf will appear under the version number in the About window:

My Credits

These are the credits for this app. They go in a rich text file named
"Credits" in the Xcode project. See below for an example of a list.

    •   First item is here
    •   Second item here
    •   Third item goes here
    •   Fourth items goes here
    •   Fifth item is here
    •   Six item is the last

app credits

Cursor

The mouse pointer (or cursor) is represented by an arrow on the screen. Different cursors are available in macOS to indicate actions that the user can take with the mouse. See the NSCursor documentation for a list of all the available cursors.

cursors

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 25) {
            Text("Hover over each button to change cursor.")

            // i-beam cursor
            Button(" i-beam ") {}
                .onHover(perform: { hovering in
                    hovering ? NSCursor.iBeam.push() : NSCursor.pop()
                })

            // crosshair cursor
            Button(" crosshair ") { }
                .onHover(perform: { hovering in
                    hovering ? NSCursor.crosshair.push() : NSCursor.pop()
                })

            // close-hand cursor
            Button(" closed-hand ") { }
                .onHover(perform: { hovering in
                    hovering ? NSCursor.closedHand.push() : NSCursor.pop()
                })

            // open-hand cursor
            Button(" open-hand ") { }
                .onHover(perform: { hovering in
                    hovering ? NSCursor.openHand.push() : NSCursor.pop()
                })

            // pointing-hand cursor
            Button(" pointing-hand ") { }
                .onHover(perform: { hovering in
                    hovering ? NSCursor.pointingHand.push() : NSCursor.pop()
                })
        }
        .frame(width: 400, height: 300)
    }
}

Grid lines

A GeometryReader can be used to equally space lines in a view even when that view changes size. This is accomplished by using the width and height of the container view to determine the spacing of the lines.

geometryreader grid lines

import SwiftUI

struct ContentView: View {

    let xSteps = 5  // purple lines for x-axis grid
    let ySteps = 4  // black lines for y-axis grid

    var body: some View {
        ZStack(alignment: .top) {
            GeometryReader { geometry in
                Rectangle()
                    .fill(Color.gray)

                // x-axis grid shown as purple lines
                ForEach(0..<self.xSteps+1) {
                    Rectangle()
                        .fill(Color.purple)
                        .frame(width: 3)
                        .offset(x: geometry.size.width / CGFloat(self.xSteps) * CGFloat($0), y: 0.0)
                }

                // y-axis grid shown as black lines
                ForEach(0..<self.ySteps+1) {
                    Rectangle()
                        .fill(Color.black)
                        .frame(height: 3)
                        .offset(x: 0.0, y: geometry.size.height / CGFloat(self.ySteps) * CGFloat($0))
                }
            }
        }
        .frame(minWidth: 400, minHeight: 300)
        .padding()
        .background(Color.secondary)
    }
}

Image

An Image view is used to display an image. For the example below, an image named “homer” is added to the Assets catalog then the image is displayed in the window while preserving its aspect ratio.

image of homer

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image("homer")
                .resizable()
                .aspectRatio(contentMode: .fit)
        }
        .padding()
        .frame(width: 400, height: 300)
    }
}

SF Symbols

Images can also be created using the system name of an SF Symbol. Use the SF Symbols app to look up the names of the available system images.

image sf symbol

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Images as SF Symbols")

            Image(systemName: "trash")

            Image(systemName: "trash.fill")

            Image(systemName: "gear")
                .font(.system(size: 32, weight: .bold))

            Image(systemName: "gear")
                .imageScale(.large)

            Image(systemName: "paperplane.circle.fill")
                .renderingMode(.original).font(.largeTitle)
        }
        .frame(width: 400, height: 300)
    }
}

System images

Named images specific to macOS can be displayed using the appropriate NSImage.Name string. See Apple’s documentation for a list of the available system image names.

image system images

import SwiftUI

struct ContentView: View {

    let prefs = NSImage(named: NSImage.preferencesGeneralName)!
    let user = NSImage(named: NSImage.userAccountsName)!
    let advanced = NSImage(named: NSImage.advancedName)!
    let computer = NSImage(named: NSImage.computerName)!
    let folder = NSImage(named: NSImage.folderName)!

    let caution = NSImage(named: NSImage.cautionName)!
    let group = NSImage(named: NSImage.userGroupName)!
    let guest = NSImage(named: NSImage.userGuestName)!
    let font = NSImage(named: NSImage.fontPanelName)!
    let info = NSImage(named: NSImage.infoName)!

    var body: some View {
        VStack {
            Text("System images available in macOS")
            HStack(spacing: 60) {
                VStack(spacing: 10) {
                    Image(nsImage: prefs)
                    Image(nsImage: user)
                    Image(nsImage: advanced)
                    Image(nsImage: computer)
                    Image(nsImage: folder)
                }
                VStack(spacing: 10) {
                    Image(nsImage: caution)
                    Image(nsImage: group)
                    Image(nsImage: guest)
                    Image(nsImage: font)
                    Image(nsImage: info)
                }
            }
        }
        .frame(width: 400, height: 300)
    }
}

NSPasteboard

The typical way to copy text is to select it with the mouse then press ⌘C. To do this in code, use the NSPasteboard class to transfer text to the pasteboard server.

copy text

import SwiftUI

struct ContentView: View {

    @State private var name = ""

    var body: some View {
        VStack(spacing: 20) {
            Text("Type some text in the text field, then copy it to the clipboard by clicking the Copy Text button.")

            TextField("enter some text", text: $name)
                .multilineTextAlignment(.center)

            Button("Copy Text") {
                let pb = NSPasteboard.general
                pb.clearContents()
                pb.setString(self.name, forType: .string)
            }
        }
        .padding(80)
        .frame(width: 400, height: 300)
    }
}

Picker control

The picker control selects an item from a set of values. The appearance of the picker can be changed by using different styles and modifiers.

picker control

import SwiftUI

struct ContentView: View {

    let bands = ["Nirvana", "Pearl Jam", "NIN"]
    @State private var selectedBand = 0

    @State private var selectedName = 0

    var body: some View {
        VStack(spacing: 20) {

            Picker("Band", selection: $selectedBand) {
                ForEach(0..<bands.count) {
                    Text(self.bands[$0])
                }
            }

            Picker("Band", selection: $selectedBand) {
                ForEach(0..<bands.count) {
                    Text(self.bands[$0])
                }
            }
            .pickerStyle(RadioGroupPickerStyle())

            Picker("Band", selection: $selectedBand) {
                ForEach(0..<bands.count) {
                    Text(self.bands[$0])
                }
            }
            .pickerStyle(SegmentedPickerStyle())

            Picker("Name", selection: $selectedName) {
                Text("Homer Simpson").tag(0)
                Text("Lisa Simpson").tag(1)
                Text("Bart Simpson").tag(2)
            }
            .fixedSize()

            Picker("Name", selection: $selectedName) {
                Text("Homer Simpson").tag(0)
                Text("Lisa Simpson").tag(1)
                Text("Bart Simpson").tag(2)
            }
            .labelsHidden()
            .fixedSize()

        }
        .padding()
        .frame(width: 400, height: 300)
    }
}

ProgressView

A ProgressView represents completion of a task or the occurance of an activity with an unknown completion time. A default value of 1.0 is used for the total value when tracking completion progress. As the example demonstrates below, different configurations of a progress view are possible.

progress view

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 30) {
            ProgressView()

            ProgressView("Loading...")

            ProgressView(value: 0.3)

            ProgressView(value: 40, total: 100)

            ProgressView(value: 0.55) {
                Label("Loading", systemImage: "bolt.fill")
            }

            ProgressView(value: 0.25, label: {
                Label("Loading", systemImage: "bolt.fill")
            }, currentValueLabel: {
                Text("0.25")
            })

            ProgressView(value: 10.0, total: 100.0, label: {
                Label("Download", systemImage: "icloud.and.arrow.down")
            }, currentValueLabel: {
                HStack {
                    Text("0")
                    Spacer()
                    Text("50")
                    Spacer()
                    Text("100")
                }
            })
        }
        .padding()
        .frame(width: 400, height: 500)
    }
}

The next example uses a state variable x to update the progress view by 10 when the “Add 10” button is clicked. A progress view is often associated with a background task; consequently, the progress view must be updated on the main thread. This is demonstrated with the “Add 20 bg” button in the example shown below.

progress view 2

import SwiftUI

struct ContentView: View {

    @State private var x = 0.0

    var body: some View {
        VStack {
            ProgressView(
                "Downloading... \(String(format: "%.0f", x))%",
                value: x,
                total: 100.0
            )

            // Increase progress bar by 10
            Button("Add 10") {
                if x < 100.0 {
                    x += 10.0
                }
            }

            // Increase progress bar by 20 using value from background thread
            Button("Add 20 bg") {
                DispatchQueue.global(qos: .background).async {
                    let z = 10.0
                    DispatchQueue.main.async {
                        if x < 100.0 {
                            x += 10.0 + z
                        }
                    }
                }
            }
        }
        .padding()
        .frame(width: 400, height: 300)
    }
}

ScrollView

The scroll view displays its content within a scrollable area of the window.

scroll view

import SwiftUI

struct ContentView: View {
    @State private var toggled = true

    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                Text("Scroll up and down").font(.headline)

                Toggle(isOn: $toggled) { Text("Toggle 1") }

                Toggle(isOn: $toggled) { Text("Toggle 2") }

                Toggle(isOn: $toggled) { Text("Toggle 3") }

                Button("Button 1") { }

                Button("Button 2") { }

                Button("Button 3") { }

                Text("Last Item in scroll view")
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .frame(width: 300, height: 200)
    }
}

A sidebar can be used to select a destination view in a navigation-based app. This is accomplished by wrapping the views in a NavigationView and using NavigationLink to display the different views. In the example below, an AppStorage property remembers the selected view.

sidebar navigation

import SwiftUI

struct DetailView: View {

    var selected: String?
    @AppStorage("selectedStore") private var selectedStore = ""

    var body: some View {
        switch selected {
        case "One":
            selectedStore = "One"
            return Text("1️⃣ \(selectedStore) View").font(.title).frame(minWidth: 200)
        case "Two":
            selectedStore = "Two"
            return Text("2️⃣ \(selectedStore) View").font(.title).frame(minWidth: 200)
        case "Three":
            selectedStore = "Three"
            return Text("3️⃣ \(selectedStore) View").font(.title).frame(minWidth: 200)
        case "Four":
            selectedStore = "Four"
            return Text("4️⃣ \(selectedStore) View").font(.title).frame(minWidth: 200)
        default:
            return Text("Default View").font(.title).frame(minWidth: 200)
        }
    }
}

struct Sidebar: View {

    @State private var selection: String? = nil
    @AppStorage("selectedStore") private var selectedStore = ""

    var body: some View {
        List {
            NavigationLink(destination: DetailView(selected: selection), tag: "One", selection: $selection) {
                Text("One View")
            }
            NavigationLink(destination: DetailView(selected: selection), tag:"Two", selection: $selection) {
                Text("Two View")
            }
            NavigationLink(destination: DetailView(selected: selection), tag:"Three", selection: $selection) {
                Text("Three View")
            }
            NavigationLink(destination: DetailView(selected: selection), tag:"Four", selection: $selection) {
                Text("Four View")
            }
        }
        .onAppear {
            selection = selectedStore
        }
        .listStyle(SidebarListStyle())
        .toolbar {
            Button(action: toggleSidebar, label: {
                Image(systemName: "sidebar.left").help("Toggle Sidebar")
            })
        }
        .frame(minWidth: 150)
    }
}

private func toggleSidebar() {
    NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}

struct ContentView: View {

    var body: some View {
        NavigationView {
            Sidebar()
            DetailView()
        }
        .frame(width: 500, height: 300)
    }
}

A sidebar view can be displayed or hidden using the toggleSidebar() feature from an NSSplitViewController. At the time of writing this article, SwiftUI does not have this feature but hopefully an upcoming WWDC will offer a SwiftUI solution.

sidebar toggle

import SwiftUI

struct Sidebar: View {

    var body: some View {
        List {
            Label("Books", systemImage: "book.closed")
            Label("Tutorials", systemImage: "list.bullet.rectangle")
            Label("Video Tutorials", systemImage: "tv")
            Label("Contacts", systemImage: "mail.stack")
            Label("Search", systemImage: "magnifyingglass")
        }
        .listStyle(SidebarListStyle())
        .toolbar {
            Button(action: toggleSidebar, label: {
                Image(systemName: "sidebar.left").help("Toggle Sidebar")
            })
        }
        .frame(minWidth: 150)
    }
}

private func toggleSidebar() {
    NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}

struct ContentView: View {

    var body: some View {
        NavigationView {
            Sidebar()
            Text("Use button to toggle sidebar.")
                .frame(minWidth: 200)
        }
        .frame(width: 500, height: 300)
    }
}

The sidebar view can also be toggled with a keyboard shortcut using Option-Command-S represented by the symbols ⌥⌘S. This is enabled by adding SidebarCommands() to the main window group.

import SwiftUI

@main
struct SidebarToggleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            SidebarCommands()
        }
    }
}

Stepper

The stepper control increments and decrements a value. A closed range can be used to limit the applicable stepper values.

stepper

import SwiftUI

struct ContentView: View {

    @State private var age = 18
    @State private var hours = 4.0
    @State private var number = 1
    @AppStorage("setting") var setting = 2

    var body: some View {
        VStack(spacing: 20) {

            // Increment or decrement `age` in range of 10-50
            Stepper("Age: \(age)", value: $age, in: 10...50)

            // Increment or decrement `hours` in range of 1-10 using steps of 0.25
            Stepper("Hours: \(hours, specifier: "%g")", value: $hours, in: 1...10, step: 0.25)

            // Increment or decrement `number` then print value
            Stepper("Number: \(number)", onIncrement: {
                print("on increment")
                self.number += 1
            }, onDecrement: {
                print("on decrement")
                self.number -= 1
            })

            // Increment or decrement `number` then print value.
            // Also detect when editing begins and ends.
            Stepper("Another Number: \(number)", onIncrement: {
                print("on increment")
                self.number += 1
            }, onDecrement: {
                print("on decrement")
                self.number -= 1
            }, onEditingChanged: { edited in
                if edited {
                    print("edited")
                } else {
                    print("not edited")
                }
            })

            // Increment or decrement `setting` and save value to UserDefaults
            Stepper("Setting: \(setting)", value: $setting, in: 0...5)

        }.frame(width: 400, height: 300)
    }
}

Text

A Text view displays one or more lines of read-only text.

text

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            // Basic text view
            Text("Hello there")
        }
        .frame(width: 400, height: 300)
    }

Font

Use the font instance method to apply a specific font to an individual Text view.

text font

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {

            Group {
                // Large title font
                Text("Large Title").font(.largeTitle)

                // Title font
                Text("Title").font(.title)

                // Title 2 font
                Text("Title 2").font(.title2)

                // Title 3 font
                Text("Title 3").font(.title3)

                // Headline font
                Text("Headline").font(.headline)

                // Subheadline font
                Text("Subheadline").font(.subheadline)
            }

            Group {
                // Body font
                Text("Body").font(.body)

                // Callout font
                Text("Callout").font(.callout)

                // Caption font
                Text("Caption").font(.caption)

                // Caption 2 font
                Text("Caption 2").font(.caption2)

                // Footnote font
                Text("Footnote").font(.footnote)
            }
        }.frame(width: 400, height: 400)
    }
}

Styles

The Text view in SwiftUI provides several modifiers to customize the appearance and style of the text.

text styles

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            Group {
                Text("Bold text").bold()
                Text("Italic text").italic()
                Text("Ultralight text").fontWeight(.ultraLight)
                Text("Strikethrough text").strikethrough()
                Text("Strikethrough blue").strikethrough(color: .blue)
                Text("Underline text").underline()
                Text("Underline green").underline(color: .green)
            }
            Group {
                Text("Upper case").textCase(.uppercase)
                Text("Lower case").textCase(.lowercase)
                Text("Color red text").foregroundColor(.red)
                Text("Purple and ").foregroundColor(.purple) + Text("Blue").foregroundColor(.blue)
            }
        }
        .frame(width:400, height: 400)
    }
}

Vertical text

Vertical text can be accomplished by rotating a text view 90 degrees. To rotate the frame of the text view, the fixed size modifier must be implemented along with defining the frame size.

vertical text

import SwiftUI

struct ContentView: View {

    var body: some View {
        HStack {
            Text("Vertical text")
                .rotationEffect(.degrees(-90))
                .fixedSize()
                .frame(width: 20, height: 180)
            Circle()
                .frame(width: 200)
        }
        .frame(width: 400, height: 300)
    }
}

TextField

The TextField structure is a control that provides an editable text field. Various modifiers are available to customize the appearance and text alignment. Actions can be performed when editing begins and ends for the text field or when the return key is pressed.

text field

import SwiftUI

struct ContentView: View {
    @State private var text1 = ""

    var body: some View {
        VStack {
            TextField("Example 1", text: $text1)

            TextField("Example 2", text: $text1)
                .fixedSize()

            TextField("Example 3", text: $text1)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("Example 4", text: $text1)
                .multilineTextAlignment(.center)

            TextField("Example 5", text: $text1)
                .multilineTextAlignment(.trailing)

            TextField("Example 6", text: $text1)
                .foregroundColor(.red)

            TextField("Example 7", text: $text1, onEditingChanged: { editing in
                if editing {
                    print("is editing")
                } else {
                    print("not editing")
                }
            })

            TextField("Example 8", text: $text1, onCommit:  {
                print("on commit")
            })

        }
        .padding()
        .frame(width: 480, height: 300)
    }
}

ViewBuilder

The @ViewBuilder attribute can be used to build views from closures. To demonstrate, three views are defined as shown below:

struct ViewA: View {
    var body: some View {
        Text("View A")
            .font(.title)
            .frame(width: 200, height: 200)
            .foregroundColor(.black)
            .background(Color.purple)
    }
}

struct ViewB: View {
    var body: some View {
        Text("View B")
            .font(.title)
            .frame(width: 200, height: 200)
            .foregroundColor(.black)
            .background(Color.green)
    }
}

struct ViewC: View {
    var body: some View {
        Text("View C")
            .font(.title)
            .frame(width: 200, height: 200)
            .foregroundColor(.black)
            .background(Color.orange)
    }
}

In the main ContentView, a function is used to switch between the different views based on the selected picker item. Notice the use of @ViewBuilder allows the function to provide different child views. Without the @ViewBuilder attribute, the function would need to wrap each case’s view with AnyView.

struct ContentView: View {

    @State private var selectedView = 1

    var body: some View {
        VStack {
            Picker("Select view:", selection: $selectedView) {
                Text("View A").tag(1)
                Text("View B").tag(2)
                Text("View C").tag(3)
            }
            .fixedSize()

            getView(tag: selectedView)
        }
        .padding()
        .frame(width: 400, height: 300)
    }

    @ViewBuilder
    func getView(tag: Int) -> some View {
        switch tag {
        case 1:
            ViewA()
        case 2:
            ViewB()
        case 3:
            ViewC()
        default:
            ViewA()
        }
    }
}

view builder

WebView

A WKWebView from the WebKit framework is used to display web content in a window. The web view can be wrapped with NSViewRepresentable to make it usable with SwiftUI. Content for the web view can be loaded from an HTML file, from a string containing HTML, or from a URL representing a website address. Don’t forget to enable “Outgoing Connections” in the target’s “App Sandbox”; otherwise, the website will not display in the app.

Load an HTML file

<!-- page.html -->

<html>
<head>
<meta charset="utf-8">
<style>
    :root { color-scheme: light dark; }
</style>
</head>
<body>
    <h1>Hello again friend 😁</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    <p>Done.</p>
</body>
</html>
import SwiftUI
import WebKit

struct WebView: NSViewRepresentable {

    let htmlFile: String

    func makeNSView(context: Context) -> WKWebView {

        guard let url = Bundle.main.url(forResource: self.htmlFile, withExtension: "html") else {
            return WKWebView()
        }

        let webview = WKWebView()
        webview.loadFileURL(url, allowingReadAccessTo: url)

        return webview
    }

    func updateNSView(_ nsView: WKWebView, context: Context) { }
}
import SwiftUI

struct ContentView: View {
    var body: some View {
        WebView(htmlFile: "page")
            .padding()
            .frame(width: 480, height: 600)
    }
}

Load a string containing HTML

import SwiftUI
import WebKit

struct WebView: NSViewRepresentable {

    let content: String

    func makeNSView(context: Context) -> WKWebView {

        let webview = WKWebView()
        webview.loadHTMLString(self.content, baseURL: nil)
        return webview
    }

    func updateNSView(_ nsView: WKWebView, context: Context) { }
}
import SwiftUI

let htmlContent = """
<html>
<head>
<style>
    :root { color-scheme: light dark; }
</style>
</head>
<body>
    <h1>Hello friend!</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    <p>Done.</p>
</body>
</html>
"""

struct ContentView: View {
    var body: some View {
        WebView(content: htmlContent)
            .padding()
            .frame(width: 480, height: 600)
    }
}

Load from a URL

import SwiftUI
import WebKit

struct WebView: NSViewRepresentable {

    let url: String

    func makeNSView(context: Context) -> WKWebView {

        guard let url = URL(string: self.url) else {
            return WKWebView()
        }

        let webview = WKWebView()
        let request = URLRequest(url: url)
        webview.load(request)

        return webview
    }

    func updateNSView(_ nsView: WKWebView, context: Context) { }
}
import SwiftUI

struct ContentView: View {
    var body: some View {
        WebView(url: "https://www.apple.com")
            .padding()
            .frame(width: 480, height: 600)
    }
}

Window size

The window size is defined by the frame size of the containing view. In this example the VStack frame is set to a width of 500 and height of 300 which makes the window width 500 and height 300.

window size

import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
            Text("Window size is 500 by 300")
        }
        .frame(width: 500, height: 300)
    }
}

Menus in Mac apps are typically located at the top of the screen in the menu bar. Menu items can be added to an app’s menu using the commands modifier on the WindowGroup.

menu items

import SwiftUI

@main
struct MenuItemsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.commands {
            CommandMenu("My Menu") {
                Text("Some text")
                Button(action: {}, label: {
                    Image(systemName: "clock")
                    Text("Date & Time")
                })

                Divider()

                Button(action: {}, label: {
                    Text("1️⃣ Item 1")
                })
                Button(action: {}, label: {
                    Text("2️⃣ Item 2")
                })

                Divider()

                Menu("Sub Menu") {
                    Button(action: {}, label: {
                        Text("Sub Item 1")
                    })
                    Button(action: {}, label: {
                        Text("Sub Item 2")
                    })
                }
            }
        }
    }
}

Gavin Wiggins © 2021