SwiftUI Cheat Sheet
Bookmark this page so you can have it handy 🤓
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, this is an EPIC page full of guides needed in the process of any app.
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.
Generate comments for documentation: Option+Cmd+/
Make to-do noticeable (give you a warning when compiling)
How to take screenshots of the phone Simulator
For more Tips checkout this video.
Submit an App to the App Store
How to create a bundle id for an app
Before you can submit your app using Xcode, you need to follow some steps to create your bundle id.
View
Demo
To switch between views in SwiftUI like the demo above, go to this page, it includes more ways to do it like sheets and more.
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
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:
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:
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
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.
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
.
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
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.
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. Here's an example:
Tap
Gesture
List
The job of List
is to provide a scrolling table of data. In fact, it’s pretty much identical to Form
, except it’s used for presentation of data rather than requesting user input. Don’t get me wrong: you’ll use Form
quite a lot too, but really it’s just a specialized type of List
.
Section
Reveal Code
Section(header: Text("How much tip do you want to leave?")) { some code ... }
Menus
Example 1 (Top navigation menu)
Screenshot
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 menu)
Screenshot
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")) }
Navigation Views
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.
Sheets & Full Screen Sheets
DemoSwiftUI’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.
Navigation Link
DemoThis is excellent to provide the user an easy way to go back and forth.
TabView (bottom menu)
DemoWhen 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.
Sidebar Menu (iPadOS & macOS)
DemoSidebars are really useful for navigating on bigger screen such as iPad's and the Mac's. We are going to take a look at how we can implement sidebars into a multi-platform SwiftUI app.
Group
Group creates several views to act as one, also to avoid Stack's 10 View maximum limit.
Screenshot
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
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.
You can find how to implement them here.
Notifications
Local Notifications
Screenshot
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) }
More here
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.
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:
let now = Date() let soon = Date().addingTimeInterval(5000) now == soon now != soon now < soon now > soon
Time
Time delay (using DispatchQueue)
Screenshot
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
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.
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
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
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
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
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
@AppStorage (UserDefaults) and @SceneStorage
You can read all about it here.
CoreData
More Here
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:
NSManagedObject
conforms to theObservableObject
protocol, which means we can bind any object to part of our user interface.- There’s a
managedObjectContext
key in the environment designed to store our active Core Data managed object context. - Xcode’s template then injects that context into the initial content view inside the scene delegate.
- 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:
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.
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:
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 aDynamicProperty
. It represents a stored variable in aView
type that is dynamically updated from some external property of the view. These variables will be given valid values immediately beforebody()
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
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
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
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
- Select your project
- In the left side of the middle pane, select your app under "Targets"
- Select the tab "Build Settings"
- Search the following keywords: "info.plist" and "pch"
Change directory of preview content
- Select your project
- In the left side of the middle pane, select your app under "Targets"
- Select the tab "Build Settings"
- 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.
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:
#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
Style
Color appearance theme
Want to switch automatically color when the phone theme changes from light to dark? In this article I cover how to select any color appearance in Xcode without specifying the theme.
Miscellaneous
Recommend another App from the App Store
DemoSwiftUI gives us a dedicated modifier that can recommend other apps on the App Store, which is a great way to cross-sell to users – “if you liked this, you’ll also like that”, and so on.
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:
// 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:
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:
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()) } }