SwiftUI Cheat Sheet

0

Full article

This is a work in progress. It's still very early from being finished.

Table of content


Rationale

I've come to understand that creating apps programmatically is way more efficient than using the Interface Builder, therefore there is a need for memorization which I hate. With this guide, I try to simplify this process by analyzing the most fundamental parts of the code.

Resource

About SwiftUI

XCode help

  • Run the iOS simulator: Cmd+R
  • Run the canvas simulator: Opt+Cmd+P
  • Format code CTRL+I
  • Types of comments:

///```print(hello("World")) // Hello, World!```
/// - Warning: The returned string is not localized.
/// - Parameter subject: The subject to be welcomed.
/// - Returns: A hello string to the subject.
// MARK: A mark comment lives here.

Make to-do noticeable

UIKit equivalent in SwiftUI

Where is view controller in SwiftUI?

They are gone. Now views talk with others via the new reactive framework, Combine. This new approach work as a replacement for UIViewController which is just a way of communication.

UIKit SwiftUI
UIImageView Image
UITextField TextField
UITextView No equivalent (use Text/TextField)
UISwitch Toggle
UISlider Slider
UIButton Button
UITableView List
UICollectionView No equivalent (can be implemented by List)
UINavigationController NavigationView
UITabBarController TabView
UIAlertController with style .alert Alert
UIAlertController with style .actionSheet ActionSheet
UIStackView with horizontal axis HStack
UIStackView with vertical axis VStack
UISegmentedControl Picker
UIStepper Stepper
UIDatePicker DatePicker
NSAttributedString No equivalent (use Text)

View


Text


Image


Use the Image view to render images inside your SwiftUI layouts. These can load images from your bundle, from system icons, from a UIImage, and more, but those three will be the most common.

Load an image from your bundle

Reveal Code

Image("example-image")

Load icons from Apple’s San Francisco Symbol set

Reveal Code

Image(systemName: "cloud.heavyrain.fill")

Create an image view from an existing UIImage

Reveal Code

guard let img = UIImage(named: "example-image") else {
    fatalError("Unable to load image")
}

return Image(uiImage: img)

Shape


Layout


Relative sizes using GeometryReader

Although it’s usually best to let SwiftUI perform automatic layout using stacks, it’s also possible to give our views sizes relative to their containers using GeometryReader. For example, if you wanted two views to take up half the available width on the screen, this wouldn’t be possible using hard-coded values because we don’t know ahead of time what the screen width will be.

To solve this, GeometryReader provides us with an input value telling us the width and height we have available, and we can then use that with whatever calculations we need. So, to get two views taking up equal width, we could divide the available space in half, like this:

Screenshot
Reveal Code

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                Text("Left")
                    .frame(width: geometry.size.width / 2, height: 50)
                    .background(Color.yellow)
                Text("Right")
                    .frame(width: geometry.size.width / 2, height: 50)
                    .background(Color.orange)
            }
        }
    }
}

Note:GeometryReader doesn’t take into account any offsets or spacing further down in the view hierarchy, which is why there is no spacing on the HStack – if we allowed spacing in there, the views would be a little too large for the available space.

Background


Stacks


Stacks – equivalent to UIStackView in UIKit – come in three forms: horizontal (HStack), vertical (VStack) and depth-based (ZStack), with the latter being used when you want to place child views so they overlap. Example of 3 by 3 grid:

Screenshot
Reveal Code

HStack(spacing: 50){
            VStack(alignment: .leading, spacing: 50){
                HStack{
                   Text("1")
                }
                HStack{
                    Text("4")
                }
                HStack{
                    Text("7")
                }
            }
            VStack(alignment: .leading, spacing: 50){
                HStack{
                   Text("2")
                }
                HStack{
                    Text("5")
                }
                HStack{
                    Text("8")
                }
            }
            VStack(alignment: .leading, spacing: 50){
                HStack{
                   Text("3")
                }
                HStack{
                    Text("6")
                }
                HStack{
                    Text("9")
                }
            }
        }
        .background(Color.blue.frame(width: 200, height: 200).cornerRadius(50))

VStack

HStack

ZStack

Input


Toggle

SwiftUI’s toggle lets users move between true and false states.

For example, we could create a toggle that either shows a message or not depending on whether the toggle is enabled or not, but of course we don’t want to have to track the state of the toggle by hand – we want SwiftUI to do that for us.

Instead we should define a @State Boolean property that will be used to store the current value of our toggle. We can then use that to show or hide other views as needed.

For example:

Reveal Code

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

    var body: some View {
        VStack {
            Toggle(isOn: $showGreeting) {
                Text("Show welcome message")
            }.padding()

            if showGreeting {
                Text("Hello World!")
            }
        }
    }
}

Button


Buttons in SwiftUI can be made in two ways depending on how they should look.

Option 1

The simplest way to make a button is when it just contains some text: you pass in the title of the button, along with a closure that should be run when the button is tapped:

Reveal Code

Button("Tap me!") {
    print("Button was tapped")
}

Option 2

If you want something more, such as an image or a combination of views, you can use this alternative form

Example:

Screenshot
Reveal Code

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

    var body: some View {
        Button(action: {
            print("Alert! You are in edit mode!")
        }) {
            HStack{
                Image(systemName: "pencil")
                Text("Edit")
            }
        }
        .disabled(buttonDisabled)
    }
}

To disable a button you can add the .disabled(bool) modifier

TextField


Slider


When you create it there are a variety of parameters you can provide, but the ones you probably care about most are:

  • Value: What Double to bind it to.
  • In: The range of the slider.
  • Step: How much to change the value when you move the slider.
Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    @State var sliderValue = 0.0
    var minimumValue = 0.0
    var maximumvalue = 100.0
    
    var body: some View {
        VStack {
            HStack {
                Text("\(Int(minimumValue))")
                // 2.
                Slider(value: $sliderValue, in: minimumValue...maximumvalue, step: 5)
                Text("\(Int(maximumvalue))")
            }.padding()
            // 3.
            Text("\(Int(sliderValue))")
        }
    }
}

Picker


Date picker

SwiftUI gives us a dedicated picker type called DatePicker that can be bound to a date property. Yes, Swift has a dedicated type for working with dates, and it’s called – unsurprisingly – Date.

Screenshot
Reveal Code

struct ContentView: View {
    
    @State private var wakeUp = Date()
    
    // when you create a new Date instance it will be set to the current date and time
    let now = Date()

    // create a second Date instance set to one day in seconds from now
    let tomorrow = Date().addingTimeInterval(86400)
    
    var body: some View {
        
        VStack{
            DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
            
            DatePicker("Please enter a date", selection: $wakeUp, in: Date()...)
            .labelsHidden()
        }
        
    }
}

Multi-Component Picker

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    @State var data: [(String, [String])] = [
        ("One", Array(0...10).map { "\($0)" }),
        ("Two", Array(20...40).map { "\($0)" }),
        ("Three", Array(100...200).map { "\($0)" })
    ]
    @State var selection: [String] = [0, 20, 100].map { "\($0)" }
    
    var body: some View {
        VStack(alignment: .center) {
            Text(verbatim: "Selection: \(selection)")
            MultiPicker(data: data, selection: $selection).frame(height: 300)
        }
    }
    
}

struct MultiPicker: View  {
    
    typealias Label = String
    typealias Entry = String
    
    let data: [ (Label, [Entry]) ]
    @Binding var selection: [Entry]
    
    var body: some View {
        GeometryReader { geometry in
            HStack {
                ForEach(0..<self.data.count) { column in
                    Picker(self.data[column].0, selection: self.$selection[column]) {
                        ForEach(0..<self.data[column].1.count) { row in
                            Text(verbatim: self.data[column].1[row])
                                .tag(self.data[column].1[row])
                        }
                    }
                    .pickerStyle(WheelPickerStyle())
                    .frame(width: geometry.size.width / CGFloat(self.data.count), height: geometry.size.height)
                    .clipped()
                }
            }
        }
    }
}

Read value from segmented control

SwiftUI’s Picker can also be used to create segmented controls equivalent to UISegmentedControl from UIKit, although it needs to be bound to some state and you must ensure to give each segment a tag so it can be identified. Segments can be text or pictures; anything else will silently fail.

Screenshot
Reveal Code

struct ContentView: View {
    @State private var favoriteColor = 0
    var colors = ["Red", "Green", "Blue"]

    var body: some View {
        VStack {
            Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
                ForEach(0..<colors.count) { index in
                    Text(self.colors[index]).tag(index)
                }
            }.pickerStyle(SegmentedPickerStyle())

            Text("Value: \(colors[favoriteColor])")
        }
    }
}

Stepper


SwiftUI’s Stepper control lets users select values from a range we specify. As an example, this creates a stepper bound to an age property, letting users select values in the range 0 through 130 inclusive:

Screenshot
Reveal Code

struct ContentView: View {
    @State private var age = 18

    var body: some View {
        VStack {
            Stepper("Enter your age", onIncrement: {
                self.age += 1
                print("Adding to age")
            }, onDecrement: {
                self.age -= 1
                print("Subtracting from age")
            })

            Text("Your age is \(age)")
        }
    }
}

Tap


Gesture


List


Section

Reveal Code

Section(header: Text("How much tip do you want to leave?")) {
    some code ...
}

Containers


By default iOS allows us to place content anywhere on the screen, including under the system clock and the home indicator. This doesn’t look great, which is why by default SwiftUI ensures components are placed in an area where they can’t be covered up by system UI or device rounded corners – an area known as the safe area.

On an iPhone 11, the safe area spans the space from just below the notch down to just above the home indicator. A common way of fixing this is by placing a navigation bar at the top of the screen. Navigation bars can have titles and buttons, and in SwiftUI they also give us the ability to display new views when the user performs an action.

Example 1 (Top navigation view)

Screenshot
Reveal Code

struct ContentView: View {
    
    init() {
        //Use this if NavigationBarTitle is with displayMode = .inline
        UINavigationBar.appearance().titleTextAttributes = [
            .foregroundColor: UIColor.red,
            .font : UIFont(name:"Georgia-Bold", size: 20)!
            
        ]
    }
    
    var body: some View {
        NavigationView {
            Text("Meal tracker reminders")

            .navigationBarTitle("Meal Tracker", displayMode: .inline)
            .navigationBarItems(trailing:
                Button(action: {
                    print("User icon pressed...")
                }) {
                    Image(systemName: "person.crop.circle").imageScale(.large)
                }
            )
        }
    }
}

Example 2 (Top navigation view)

Screenshot
Reveal Code

NavigationView{
            Text("Test")
                .navigationBarItems(leading: HStack {
                    Button(action: {}, label: {Image(systemName: "star.fill")})
                    Button(action: {}, label: {Text("Edit")})
                }, trailing: VStack {
                    Button(action: {}, label: {Image(systemName: "star.fill")})
                    Button(action: {}, label: {Text("Edit")})
                })
                .navigationBarTitle(Text("Notes"))
        }

Screenshot
Reveal Code

import SwiftUI

struct DetailView: View {
    
    @Environment(\.presentationMode) var presentationMode: Binding
    
    var body: some View {
       Text("View 2")
    }
}



struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView())
                { Text("Go to view 2") }
            }
            .navigationBarTitle("Back")
            .navigationBarHidden(true)
        }
    }
}

TabView


When you want to show two separate views with SwiftUI, the easiest and most user-intuitive approach is with a tab bar across the bottom of our app. In our case, that means we’ll put our menu view in one tab and the active in another. SwiftUI gives us a TabView for just this purpose, and it works much like a UITabBarController with radio-style selection control which determines which View is presented.

Example 1 (Bottom navigation view)

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    var body: some View {
        TabView {
            ContentView()
                .tabItem {
                    Image(systemName: "list.dash")
                    Text("Menu")
                }

            OrderView()
                .tabItem {
                    Image(systemName: "square.and.pencil")
                    Text("Order")
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static let order = Order()

    static var previews: some View {
        ContentView().environmentObject(order)
    }
}

Group


Group creates several views to act as one, also to avoid Stack's 10 View maximum limit.

Screenshot
Reveal Code

VStack {
    Group {
        Text("Hello")
        Text("Hello")
        Text("Hello")
    }
    Group {
        Text("Hello")
        Text("Hello")
    }
}

Sounds, Alerts, Notifications and Action Sheets


Sounds

There are many free sounds you could get from websites for your project, here is one of them.

System Sounds

The number 1026 is the SystemSound id.

You can find the full list of ids here or here.

Reveal Code

import AVFoundation

AudioServicesPlaySystemSound(1026)

Custom Sounds

I'm going to divide this in 2 files to accomplish a simple sound at a button press.

Screenshot
ContentView

import SwiftUI
import AVFoundation

struct ContentView: View {
    
    var body: some View {
        Button(action: {
            playSound(sound: "piano-1", type: "mp3")
        }){
            Text("Play Sound")
        }
    }
}

PlaySound

import Foundation
import AVFoundation

var audioPlayer: AVAudioPlayer?

func playSound(sound: String, type: String) {
    if let path = Bundle.main.path(forResource: sound, ofType: type) {
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
            audioPlayer?.play()
        } catch {
            print("Couldn't find or play the audio file")
        }
    }
}

Alerts


If something important happens, a common way of notifying the user is using an alert – a pop up window that contains a title, message, and one or two buttons depending on what you need.

Screenshot
Reveal Code

                                    Button(action: {
                                        self.showingAlert = true
                                    }) {
                                        Text("Click Alert")
                                            .foregroundColor(Color.white)
                                    }
                                    .padding()
                                    .background(Color.blue)
                                    .alert(isPresented: self.$showingAlert) { () -> Alert in
                                        Alert(title: Text("arturofm.com"), message: Text("SwiftUI Alert."), primaryButton: .default(Text("Okay"), action: {
                                            print("Okay Clicked")
                                        }), secondaryButton: .default(Text("Dismiss")))
                                    }

Notifications


Local Notifications

Screenshot
Reveal Code

import SwiftUI
import UserNotifications

struct ContentView: View {
    
    var body: some View {
        VStack {
            Button("Request Permission") {
                RequestNotificationsAccess()
            }

            Button("Schedule Notification") {
                ScheduleNotification()
            }
        }
    }
}

func RequestNotificationsAccess() {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
        if success {
            print("All set!")
        } else if let error = error {
            print(error.localizedDescription)
        }
    }
}

func ScheduleNotification() {
    let content = UNMutableNotificationContent()
    content.title = "For more go to:"
    content.subtitle = "www.arturofm.com"
    content.sound = UNNotificationSound.default

    // show this notification five seconds from now
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)

    // choose a random identifier
    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

    // add our notification request
    UNUserNotificationCenter.current().add(request)
}

Sheets: New view

SwiftUI’s sheets are used to present new view controllers modally over existing ones. To use one, give it something to show (some text, an image, a custom view, etc), add a Boolean that defines whether the detail view should be showing, then attach it to your main view as a modal sheet.

Screenshot
Reveal Code

struct DetailView: View {
    var body: some View {
        Text("Detail")
    }
}

struct ContentView: View {
    @State var showingDetail = false

    var body: some View {
        Button(action: {
            self.showingDetail.toggle()
        }) {
            Text("Show Detail")
        }.sheet(isPresented: $showingDetail) {
            DetailView()
        }
    }
}

Events


Lifecycle events

SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.

Reveal Code

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView()) {
                    Text("Hello World")
                }
            }
        }.onAppear {
            print("ContentView appeared!")
        }.onDisappear {
            print("ContentView disappeared!")
        }
    }
}

struct DetailView: View {
    var body: some View {
        VStack {
            Text("Second View")
        }.onAppear {
                print("DetailView appeared!")
        }.onDisappear {
                print("DetailView disappeared!")
        }
    }
}

Date


Compare dates

Swift’s Date struct conforms to both Equatable and Comparable, which means you check two dates for equality and compare them to see which is earlier.

In practice, this means you can use <, >, and == to compare them just like you would do with strings and integers. Try this in a playground:

Reveal Code

let now = Date()
let soon = Date().addingTimeInterval(5000)

now == soon
now != soon
now < soon
now > soon

Time


Time delay (using DispatchQueue)

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            Button(action: {
                DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                   print("Time is up!")
                }
            }) {
                Text("Alert in 5 sec")
            }
        }
    }
}

Compare time

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    let now = Date()
    let soon = Date().addingTimeInterval(3600)

    var body: some View {
        
        VStack(alignment: .leading, spacing: 0) {
            Text("\(self.now)")
            Text("\(self.soon)")
            
            if(soon > now) {
                Text("Soon is greater than now")
            }
        }

    }
}

Countdown Example 1 (using Timer and onReceive)

If you want to run some code regularly, perhaps to make a countdown timer or similar, you should use Timer and the onReceive() modifier.

It’s important to use .main for the runloop option, because our timer will update the user interface. As for the .common mode, that allows the timer to run alongside other common events – for example, if the text was in a scroll view that was moving.

Screenshots
Reveal Code

import SwiftUI

struct ContentView: View {
    @State var timeRemaining = 10
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack {
            if(self.timeRemaining > 0) {
                Text("\(timeRemaining)")
                .onReceive(timer) { _ in
                    if self.timeRemaining > 0 {
                        self.timeRemaining -= 1
                    }
                }
            } else {
                Text("Time is up!")
            }
        }
    }
}

Countdown Example 2 ( using DispatchQueue.main.async )

Screenshots
Reveal Code

struct ContentView: View {

    @State private var showtimer = false
    
    var body: some View {
        VStack(spacing: 10) {
            VStack {
                if showtimer {
                    timer()
                }
            }
                .frame(height: 80)

            Button(self.showtimer ? "Hide": "Show") {
                self.showtimer.toggle()
            }
        }
    }
}

struct timer: View {
    @State private var counter = 0
    
    var body: some View {
        DispatchQueue.main.async {
            self.counter += 1
        }
        return Text("Computed Time\n\(counter)").multilineTextAlignment(.center)
    }
}

Countdown Example 3 ( using Time() )

Screenshots
Model

import Foundation

enum TimerMode {
    case running
    case paused
    case initial
}

func secondsToMinutesAndSeconds(seconds: Int) -> String {
    let minutes = "\((seconds % 3600) / 60)"
    let seconds = "\((seconds % 3600) % 60)"
    let minuteStamp = minutes.count > 1 ? minutes : "0" + minutes
    let secondStamp = seconds.count > 1 ? seconds : "0" + seconds
    
    return "\(minuteStamp) : \(secondStamp)"
}

View
import SwiftUI

struct ContentView: View {
    
    @ObservedObject var timerManager = TimerManager()
    
    @State var selectedPickerIndex = 0
    
    let availableMinutes = Array(1...59)
    
    var body: some View {
        
        NavigationView {
            VStack {
                Text(secondsToMinutesAndSeconds(seconds: timerManager.secondsLeft))
                    .font(.system(size: 80))
                    .padding(.top, 80)
                Image(systemName: timerManager.timerMode == .running ? "pause.circle.fill" : "play.circle.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 180, height: 180)
                    .foregroundColor(.red)
                    .onTapGesture(perform: {
                        if self.timerManager.timerMode == .initial {
                            self.timerManager.setTimerLength(minutes: self.availableMinutes[self.selectedPickerIndex]*60)
                        }
                        self.timerManager.timerMode == .running ? self.timerManager.pause() : self.timerManager.start()
                    })
                if timerManager.timerMode == .paused {
                    Image(systemName: "gobackward")
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 50, height: 50)
                        .padding(.top, 40)
                    .onTapGesture(perform: {
                        self.timerManager.reset()
                    })
                }
                if timerManager.timerMode == .initial {
                    Picker(selection: $selectedPickerIndex, label: Text("")) {
                        ForEach(0 ..< availableMinutes.count) {
                            Text("\(self.availableMinutes[$0]) min")
                        }
                    }
                    .labelsHidden()
                }
                Spacer()
            }
            .navigationBarTitle("Timer")
        }
        .environment(\.colorScheme, .dark)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ViewModel
import Foundation
import SwiftUI

class TimerManager: ObservableObject {
    
    @Published var timerMode: TimerMode = .initial
    
    @Published var secondsLeft = UserDefaults.standard.integer(forKey: "timerLength")
    
    var timer = Timer()
    
    func setTimerLength(minutes: Int) {
        let defaults = UserDefaults.standard
        defaults.set(minutes, forKey: "timerLength")
        secondsLeft = minutes
    }
    
    func start() {
        timerMode = .running
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
            if self.secondsLeft == 0 {
                self.reset()
            }
            self.secondsLeft -= 1
        })
    }
    
    func reset() {
        self.timerMode = .initial
        self.secondsLeft = UserDefaults.standard.integer(forKey: "timerLength")
        timer.invalidate()
    }
    
    func pause() {
        self.timerMode = .paused
        timer.invalidate()
    }
}

Modifying program state


There’s a saying among SwiftUI developers that our “views are a function of their state,” but while that’s only a handful of words it might be quite meaningless to you at first.

There are several ways to keep track of the state of our app while it runs:

@State

Reveal Code

struct ContentView: View {
    @State var tapCount = 0

    var body: some View {
        Button("Tap Count: \(tapCount)") {
            self.tapCount += 1
        }
    }
}

@ObservedObject

Use this if you want to use a class with your SwiftUI data – which you will want to do if that data should be shared across more than one view.

Screenshot
Reveal Code

import SwiftUI

class User: ObservableObject {
    @Published var firstName = "Arturo"
}

struct ContentView: View {
    @ObservedObject var user = User()

    var body: some View {
        VStack {
            Text("Your name is \(user.firstName).")
            TextField("First name", text: $user.firstName)
        }
    }
}

.onReceive

Adds an action to perform when this view detects data emitted by the given publisher.

In the example below, once you click on set future date, it changes to blue and waits 5 sec, even if the app is minimized, it'll check for the date to see if it's passed due 🤓

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
      
    @State var now = Date()
    @State var futuredate = Date()
      
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
      
    var body: some View {
          
        NavigationView {
            VStack {
                  
                Button("set future date") {
                    self.futuredate = self.now.addingTimeInterval(5)
                }
                  
                VStack {
                    if now < futuredate {
                        Button(action: {
                        }) {
                            Text("")
                        }
                        .padding()
                        .background(Color.blue)
                    } else {
                        Button(action: {
                        }) {
                            Text("")
                        }
                        .padding()
                        .background(Color.black)
                    }
                }
            }
        }
        .onReceive(timer) { _ in
            self.now = Date()
        }
    }
}

Data Storage


UserDefaults

UserDefaults allows us to store small amount of user data directly attached to our app. There is no specific number attached to “small”, but you should keep in mind that everything you store in UserDefaults will automatically be loaded when your app launches – if you store a lot in there your app launch will slow down. To give you at least an idea, you should aim to store no more than 512KB in there.

UserDefaults is perfect for storing user settings and other important data – you might track when the user last launched the app, which news story they last read, or other passively collected information.

The default of an int will be 0 and the boolean false.

It takes iOS a little time to write your data to permanent storage. They don’t write updates immediately because you might make several back to back, so instead they wait some time then write out all the changes at once. How much time is another number we don’t know, but a couple of seconds ought to do it.

As a result of this, if you tap the button then quickly relaunch the app from Xcode, you’ll find your most recent tap count wasn’t saved.

The key String is case-sensitive just like regular Swift strings, and it’s important – we need to use the same key to read the data back out of UserDefaults

Example 1 (Store tap count)

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    @State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")

    var body: some View {
        Button("Tap count: \(tapCount)") {
            self.tapCount += 1
            UserDefaults.standard.set(self.tapCount, forKey: "Tap")
        }
    }
}

Example 2  - (Store Integer - using a ViewModel class)

Screenshot
ContentView

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var eatTracker = TimeToEatTrackerViewModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Button(action: {
                self.eatTracker.currentMeal = self.eatTracker.currentMeal + 1
                print(String(self.eatTracker.currentMeal))
            }){
                Text(String(self.eatTracker.currentMeal))
            }
            
        }
    }
}

Swift file #2
import Foundation

class TimeToEatTrackerViewModel: ObservableObject {
    
    @Published var currentMeal: Int = UserDefaults.standard.integer(forKey: "CurrentMeal") {
        didSet {
            UserDefaults.standard.set(self.currentMeal, forKey: "CurrentMeal")
        }
    }
    
}

Example 3  - (Store dictionary - using a ViewModel class)

This implementation is slightly different from the above since changing property wrapper content value does not trigger didSet, at least for now, Xcode 11.4, solution:

ContentView

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var eatTracker = TimeToEatTrackerViewModel()
    
    var body: some View {
        
        VStack {
            Button(action: {
                var tmp = self.eatTracker.mealAndStatus
                tmp["Breakfast"] = "done"
                self.eatTracker.mealAndStatus = tmp
            }){
                Text(String(self.eatTracker.mealAndStatus["Breakfast"]!))
            }
        }
    }
}

Swift file #2
import Foundation

class TimeToEatTrackerViewModel: ObservableObject {
    @Published var mealAndStatus: [String: String] =
        UserDefaults.standard.dictionary(forKey: "mealAndStatus") as? [String: String] ?? ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"] {
        didSet {
            UserDefaults.standard.set(self.mealAndStatus, forKey: "mealAndStatus")
        }
    }
}

CoreData


When you’re working with Core Data, please try to keep in mind that it has been around for a long time – it was designed way before Swift existed, never mind SwiftUI, so occasionally you’ll meet parts that don’t work quite as well in Swift as we might hope. Hopefully we’ll see this improve over the years ahead, but in the meantime be patient!

Be warned: Xcode really likes to ignore changes made in the Core Data editor, so I like to drive the point home by pressing Cmd+S before going to another file. Failing that, restart Xcode!

Useful links:

SwiftUI’s integration with Core Data points very strongly in one direction: create the Core Data container once when the app starts, inject its managed object context into the environment, then perform fetch requests directly on there.

Here are the four specific features that will help you see it:

  1. NSManagedObject conforms to the ObservableObject protocol, which means we can bind any object to part of our user interface.
  2. There’s a managedObjectContext key in the environment designed to store our active Core Data managed object context.
  3. Xcode’s template then injects that context into the initial content view inside the scene delegate.
  4. There’s a @FetchRequest property wrapper that uses the environment’s managed object context to perform fetch requests.

So, we create a managed object context when the app launches, attach it to the environment for our views, then use @FetchRequest to load data for the app to use.

Configure Core Data

If you created a new project and check both SwiftUI and Core Data skip this, since Xcode does a pretty good job of getting you towards a working configuration, but if selected CloudKit or nothing at all, then follow below.

IF you didn't select core data at the beginning, you can set it up like this:

1- Create a Core Data model by press Cmd+N to make a new file, then choosing Data Model. The name of this model matters, because it will be used in your code shortly. Once you have your model you can go ahead and create any entities you want to use in your app.

2-  Your need to load your Core Data model into the app in your app delegate, handling any errors as appropriate. Something like this in AppDelegate.swift ought to do the trick:

Code

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "CoreDataModelNameHere")
    container.loadPersistentStores { description, error in
        if let error = error {
            // Add your error UI here
        }
    }
    return container
}()

3- Add a saveContext() method to your app delegate so that it checks whether the context has changes and commits them if needed.

Code

func saveContext () {
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            // Show the error here
        }
    }
}

4- You need to inject the managed object context for your Core Data container into the SwiftUI environment. To use Apple’s code, open your SceneDelegate.swift file, look for where you create your initial ContentView instance, then modify it to this:

Code

guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
    fatalError("Unable to read managed object context.")
}

That will still crash your code if the typecast fails, but at least it gives us the chance to provide a descriptive error.

5- You should make your scenes trigger a Core Data save when they move to the background:

Code

func sceneDidEnterBackground(_ scene: UIScene) {
    (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}

Create

Saving Core Data Objects in SwiftUI in exactly the same way it works outside of SwiftUI: get access to the managed object context, create an instance of your type inside that context, then save the context when you’re ready.

For CRUD operations on Core Data I'm using a xcdatamodeld file with  an entity called ProgrammingLanguage that has two string attributes: “name” and “creator”.
Reveal Code

import SwiftUI

struct ContentView: View {
    
    // Anywhere you need to create Core Data objects you should add an @Environment property to ContentView to read the managed object context right out:
    @Environment(\.managedObjectContext) var managedObjectContext

    var body: some View {
        
        Button(action: {
            let language = ProgrammingLanguage(context: self.managedObjectContext)
            language.name = "Python"
            language.creator = "Guido van Rossum"
            
            // See whether your managed object context has any changes
            if self.managedObjectContext.hasChanges {
                // Save the context whenever is appropriate
                do {
                    try self.managedObjectContext.save()
                } catch {
                    // handle the Core Data error
                }
            }
        }) {
            Text("Insert example language")
        }
    }
}

Read

Retrieving information from Core Data is done using a fetch request – we describe what we want, how it should sorted, and whether any filters should be used, and Core Data sends back all the matching data. The property wrapper called @FetchRequest takes two parameters: the entity we want to query, and how we want the results to be sorted. It has quite a specific format.

@FetchRequest is a DynamicProperty. It represents a stored variable in a View type that is dynamically updated from some external property of the view. These variables will be given valid values immediately before body() is called.

Example:

For CRUD operations on Core Data I'm using a xcdatamodeld file with  an entity called ProgrammingLanguage that has two string attributes: “name” and “creator”.

Creating a fetch request requires two pieces of information: the entity you want to query, and a sort descriptor that determines the order in which results are returned. In my example setup we created a ProgrammingLanguages entity that had name and creator attributes, so we could create a fetch request for it like this:

Reveal Code

import SwiftUI

struct ContentView: View {
    
    @FetchRequest(
        entity: ProgrammingLanguage.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \ProgrammingLanguage.name, ascending: true),
            NSSortDescriptor(keyPath: \ProgrammingLanguage.creator, ascending: false)
        ]
    ) var languages: FetchedResults<ProgrammingLanguage>
    
    var body: some View {
        List(Array(languages.enumerated()), id: \.element) { index, language in
    Text(language.name ?? "Unknown")
    Text("\(index)")
}
    }
}

As you can see, the sortDescriptors parameter is an array, so you can provide as many sorting options as you need like.

Predicates (show details about data, while ignoring other data):

You can substitute the code above with the following to add the predicate:

Reveal Code

@FetchRequest(
    entity: ProgrammingLanguage.entity(),
    sortDescriptors: [
        NSSortDescriptor(keyPath: \ProgrammingLanguage.name, ascending: true),
    ],
    predicate: NSPredicate(format: "name == %@", "Python")
) var languages: FetchedResults<ProgrammingLanguage>

Update

Once you know how to read/create data, the update process is very simple:

For CRUD operations on Core Data I'm using a xcdatamodeld file with  an entity called ProgrammingLanguage that has two string attributes: “name” and “creator”.
Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    // Once you have your managed object context, make it available to your SwiftUI view as a property like this one:
    @Environment(\.managedObjectContext) var managedObjectContext
    
    // Create a fetch request that reads some data from your managed object context. In my example setup there’s a ProgrammingLanguage entity, so we can read out all items like this:
    @FetchRequest(
        entity: ProgrammingLanguage.entity(),
        sortDescriptors: []
    ) var languages: FetchedResults<ProgrammingLanguage>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(languages, id: \.self) { language in
                    Button(action: {
                        do {
                            language.creator = "Updated Value"
                            try self.managedObjectContext.save()
                        } catch {
                            // handle the Core Data error
                        }
                    }) {
                        Text("Creator: \(language.creator ?? "Anonymous")")
                    }
                    
                }
            }
        }
    }
}

or

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    // Once you have your managed object context, make it available to your SwiftUI view as a property like this one:
    @Environment(\.managedObjectContext) var managedObjectContext
    
    // Create a fetch request that reads some data from your managed object context. In my example setup there’s a ProgrammingLanguage entity, so we can read out all items like this:
    @FetchRequest(
        entity: ProgrammingLanguage.entity(),
        sortDescriptors: []
    ) var languages: FetchedResults<ProgrammingLanguage>
    
    var body: some View {
        VStack {
            Button("Update") {
                for data in self.languages {
                        do {
                            data.name = "Changed!"
                            try self.managedObjectContext.save()
                        } catch {
                            // handle the Core Data error
                        }
                    print(data.name!)
                }
            }
        }
    }
}

Delete

Deleting Core Data objects in SwiftUI is mostly the same as deleting them in UIKit.

For CRUD operations on Core Data I'm using a xcdatamodeld file with  an entity called ProgrammingLanguage that has two string attributes: “name” and “creator”.
Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    
    // Once you have your managed object context, make it available to your SwiftUI view as a property like this one:
    @Environment(\.managedObjectContext) var managedObjectContext
    
    // Create a fetch request that reads some data from your managed object context. In my example setup there’s a ProgrammingLanguage entity, so we can read out all items like this:
    @FetchRequest(
        entity: ProgrammingLanguage.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \ProgrammingLanguage.name, ascending: true),
        ]
    ) var languages: FetchedResults<ProgrammingLanguage>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(languages, id: \.self) { language in
                    Text("Creator: \(language.creator ?? "Anonymous")")
                }
                .onDelete(perform: removeLanguages)
            }
                
            .navigationBarItems(trailing: EditButton())
            
        }
    }
    
    // Add the removeLanguages() method to your SwiftUI view. This should accept an IndexSet, which is a collection of unique integer indexes that should be deleted:
    func removeLanguages(at offsets: IndexSet) {
        
        for index in offsets {
            let language = languages[index]
            managedObjectContext.delete(language)
        }
        
        // You might want to save your Core Data context at this point, in which case after the for loop finishes add something like this:
        do {
            try managedObjectContext.save()
        } catch {
            // handle the Core Data error
        }
    }
}

Project structure


Define the first View on App launch

Your first view does not have to be named ContentView. You can name it whatever you want. Just make sure you refer to it properly in the scene delegate.

Tell Xcode where my info.plist and .pch files are

  1. Select your project
  2. In the left side of the middle pane, select your app under "Targets"
  3. Select the tab "Build Settings"
  4. Search the following keywords: "info.plist" and "pch"

Change directory of preview content

  1. Select your project
  2. In the left side of the middle pane, select your app under "Targets"
  3. Select the tab "Build Settings"
  4. Search the following keywords: "Preview Content"

MVVM design pattern


Group your views and models by functionality, not type.

Example Project Structure:

Customer /
-- CustomerViewModel.swift
-- CustomerModel.swift
-- Add/
---- AddCustomerViewModel.swift
---- AddCustomerView.swift
-- Edit/
---- EditCustomerViewModel.swift
---- EditCustomerView.swift
-- Delete/
---- DeleteCustomerViewModel.swift
---- DeleteCustomerView.swift

Invoice /
-- InvoiceViewModel.swift
-- InvoiceModel.swift
-- Add/
---- AddInvoiceViewModel.swift
---- AddInvoiceView.swift

Theme


Detect light and dark mode

SwiftUI lets us detect whether dark mode or light mode is currently enabled using the colorScheme environment key.

Reveal Code

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        Text(colorScheme == .dark ? "In dark mode" : "In light mode")
    }
}

ContentView_Previews in light and dark mode

Xcode allows you to preview your layouts in either color scheme by setting the \.colorScheme environment value in your preview. For example, this shows a preview using dark mode:

Reveal Code

#if DEBUG
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
      Group {
         ContentView()
            .environment(\.colorScheme, .dark)
      }
   }
}
#endif

Multiple previews light/dark mode

If you want to see both light and dark mode side by side, place multiple previews in a group, like this:

Reveal Code

#if DEBUG
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
      Group {
         ContentView()
            .environment(\.colorScheme, .light)

         ContentView()
            .environment(\.colorScheme, .dark)
      }
   }
}
#endif

Tutorials


Conditionally use view in SwiftUI

You may notice some odd behavior where your Swift code doesn’t quite work the same in SwiftUI. For example, if you are using an if statement with a simple boolean check to optionally show a view, everything works just fine. If you were to take the conditional check one step further to something familiar like evaluating an enum or to use the if let syntax to conditionally unwrap an optional, you will be left scratching your head. Here is an example of how you can overcome this until they implement something better:

  • You can use Ternary Conditional Operator:
Reveal Code

// That says: if meal.eaten is nil and meal.will_eat is also nil, then display "--", else display their respective values.
Text("\(meal.eaten == nil ? meal.will_eat == nil ? "--" : meal.will_eat! : meal.eaten!)")

// Or like this saying: if value not nil, display meal.eaten
Text("\(meal.eaten ?? "--")")

  • You can use if else statements:
Reveal Code

import SwiftUI

struct ContentView: View {
    
    func shouldShowDetailView(state: String) -> Bool {
        switch state {
        case "active": return true
        case "notSet": return false
        default:
            return false
        }
    }
        
        var body: some View {
            
            VStack {
                if shouldShowDetailView(state: "active") == true {
                    Text("Active view!")
                } else {
                    Text("Disabled view")
                }
            }
        }
}



// You can also create multiple if's inside the VStack, as long as they all return the same view, ex:
VStack {
                                    if meal.status! == "active" {
                                        Text("Active view!")
                                    } else if meal.status! == "active" {
                                        Text("Disabled view")
                                    } else if meal.status! == "active" {
                                        Text("Disabled view")
                                    } else if meal.status! == "active" {
                                        Text("Disabled view")
                                    }
                                }

Change Status Bar Text Color

You need to edit the UIHostingController call in the SceneDelegate. Create a new file called: HostingController and place the following code in it:

Reveal Code

import SwiftUI

final class HostingController: UIHostingController {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        .lightContent
    }
}

and then in the scene delegate changing the line:

window.rootViewController = UIHostingController(rootView: contentView)

to: window.rootViewController = HostingController(rootView: contentView)

Custom Modifiers

If you find yourself constantly attaching the same set of modifiers to a view – e.g., giving it a background color, some padding, a specific font, and so on – then you can avoid duplication by creating a custom view modifier that encapsulates all those changes. So, rather than say “make it red, make it use a large font” and so on, you can just say “make it look like a warning,” and apply a pre-made set of modifiers.

Reveal Code

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.red)
            .foregroundColor(Color.white)
            .font(.largeTitle)
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI")
            .modifier(PrimaryLabel())
    }
}


References:
hackingwithswift, Github, fuckingswiftui

Comments