November 7, 2025 at 10:10 PM EST
Migrating more Swift/SwiftUI notes
November 7, 2025 at 10:10 PM EST
Migrating more Swift/SwiftUI notes
Following suit with Swift Fun, I’ve relocated more notes to this post. Recently, I’ve been posting with the intent of using my writings as a mental cache for the knowledge I acquire. One tip for you today is to give yourself time before compiling and correcting your code. You don’t have to be optimal on the first try and you don’t have to remember everything. If you forget some concepts and syntax, you can always revisit them in your projects, in the documentation or ask people for help when you’ve struggled enough.
You can use Shapes for creative overlays or backgrounds.
struct Diamond: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
path.move(to: CGPoint(x: width / 2, y: 0))
path.addLine(to: CGPoint(x: width, y: height / 2))
path.addLine(to: CGPoint(x: width / 2, y: height))
path.addLine(to: CGPoint(x: 0, y: height / 2))
path.closeSubpath()
return path
}
}
Transactions provide SwiftUI with the information it needs to animate changes between views correctly. Whichever view with an .animation modifier can be “intercepted” and customized using the .transaction modifier. Inside the transaction, you can modify the animation. As to what information it specifically holds and what calculations it does, I’d have to dig deeper, but I’m fine at the surface for now.
There are two types of animation:
Personally, the naming (implicit vs explicit) is confusing to me , so I shift my focus to grasp the functionality instead. For implicit animations, if you have a VStack of a Rectangle and Text, and you add .animation to the Rectangle, it’s only going to animate the Rectangle. However, if you attach it to the VStack containing the Rectangle and Text, it animates what’s inside the VStack hence both. For explicit animations, you can opt for withAnimation. When you toggle a Button controlling a @State variable wrapped in withAnimation, every element inside that View gets affected by the animation.
struct A: View -> some View {
@State private var isExpanded = false
var body: some View {
VStack {
// Only the rectangle gets animated
Rectangle()
.fill(.blue)
.frame(width: isExpanded ? 200 : 100, height: 100)
Text("Snail Rice")
Button("Toggle") {
isExpanded.toggle()
}
}
}
}
// B is the root
struct B: View -> some View {
@State private var isExpanded = false
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: isExpanded ? 200 : 100, height: 100)
Text("Snail Rice")
// No need for the .animation modifier
// Everything starts to get animated at B
Button("Toggle") {
withAnimation(.spring()) {
isExpanded.toggle()
}
}
}
}
}
You can read more in this solid article from Holy Swift.
This is a snippet straight from the SwiftUI Lab. All credits to Javier. In this code, we need to use ViewModifier to create custom transitions, because that’s what the static func modifier of the AnyTransition class requires the identity and active elements to be.
struct MyCustomModifier: ViewModifier {
let opacity: Double
func body(content: Content) -> some View {
content.opacity(opacity)
}
}
/*
static func modifier<E>(
active: E,
identity: E
) -> AnyTransition where E : ViewModifier
*/
extension AnyTransition {
static var myOpacity: AnyTransition {
AnyTransition.modifier(
active: MyOpacityModifier(opacity: 0),
identity: MyOpacityModifier(opacity: 1))
}
}
// The AnyTransition.myOpacity returns an AnyTransition
MyView()
.transition(
.asymmetric(
insertion: AnyTransition.myOpacity
.combined(with: .slide),
removal: .scale
)
)
Two-way connection between a source of truth and a view property. Think parent and child (@State and @Binding). You can use them in initializers.
// Binding in an initializer
init(food: Food) {
self.food = food
_calories = .init(initialValue: Double(food.calories))
}
Paul Hudson and Donny Walls explain this clearly. To paraphrase, this macro sniffs out changes in the properties of a class to trigger updates in the views that use them. @Bindable allows observed properties to be used in bindings. You’d use it when your observed class has a property that you want to bind to a TextField, TextEditor, Picker, etc.
In Segmance, tapping my play/pause button will have SwiftUI re-render the Image view, because it’s using the AudioPlayerModel’s isPlaying property. The model handles the functionality while the view does its magic to reflect its changes.
// AudioPlayerModel
@Observable
class AudioPlayerModel: NSObject, AVAudioPlayerDelegate {
private var audioPlayer: AVAudioPlayer?
var isPlaying: Bool = false
}
// AudioPlayerView
struct AudioPlayerView: View {
// AudioPlayerView owns the AudioPlayerModel
@State private var audioPlayerManager: AudioPlayerModel
var body: some View {
// SwiftUI rerenders the Image once I tap the play/pause button
// because it's using the observed isPlaying property.
Image(systemName: audioPlayerManager.isPlaying ? "pause.fill" : "play.fill")
.onTapGesture {
audioPlayerManager.isPlaying
? audioPlayerManager.pauseAudio()
: audioPlayerManager.playAudio()
}
}
}
The changes in the move’s details in the TextEditor will automatically update the Move’s details property.
@Observable
class Move {
var id: UUID = UUID()
var title: String = ""
var details: String
}
// MoveView
@Bindable var move: Move
var body: some View {
VStack {
TextField("Enter a move title", text: $move.title)
TextEditor(text: $move.details)
}
}
All of Swift’s basic value types conform to to Hashable. It allows types to be stored in dicts, sets and compared since Hashable also conforms to Equatable (HeightValue in K-Count) 1.. You need structs to conform to Hashable if you want to use them with navigationDestination. Also, using ForEach with id: \.self is possible thanks to Hashable. With Identifiable, you don’t need the id inside the ForEach. Think Hashable for lookup and equality and Identifiable for tracking uniqueness.
I always freeze for a second when I have to remember the difference between willSet and didSet:
struct Exercise {
var name: String
var repetitions: Int {
// A function that holds the old and new value
// Use this when you care about the previous value (show a change)
willSet {
print("Going from \(repetitions) to \(newValue)")
}
didSet {
print("\(name) completed \(repetitions)")
}
}
}
var flares = Exercise(name: "flares", repetitions: 0)
flares.repetitions = 2
flares.repetitions = 4
With get and set, there are some neat underlying behaviours, and it’s nice to see where they come from.
struct Rect {
var origin = Point()
var size = Size()
// You don't need set if it's read-only
var center: Point {
get {
Point(x: origin.x + (size.width/2),
y: origin.y + (size.height/2))
}
// You have access to newValue behind the scenes
// set(newCenter) is another way to write this
set {
origin.x = newValue.x - (size.width/2)
origin.y = newValue.y - (size.height/2)
}
}
}
Static Properties
struct VideoGame {
static var collectionSize = 0
var videogame: String
init(name: String) {
self.videogame = name
VideoGame.collectionSize += 1
}
}
let mario64 = VideoGame(name: "Super Mario Sunshine")
let mariorpg = VideoGame(name: "Super Mario RPG")
print(VideoGame.collectionSize) // 2
Structs are value types which means you’ll create a copy instead of accessing the same reference. They also come with memberwise initializer (no need to write init). You could, however, write your own initializer with a struct, but you’ll lose the synthetized one.
struct Drawing {
var title: String
}
// Structs have memberwise initializers (comes packed with an init)
var drawing1 = Drawing(title: "Cube")
var drawing2 = drawing1
drawing2.title = "Cylinder"
print(drawing1.title) // Cube
print(drawing2.title) // Cylinder
class Drawing {
var title: String
init(title: String) {
self.title = title
}
}
var drawing1 = Drawing(title: "cube")
var drawing2 = drawing1
drawing2.title = "Cones"
print(drawing1.title) // Cones
print(drawing2.title) // Cones
No explanations for now, only syntax.
You can check many conditions on the same line with optional binding.
if let a = optionalA, let b = optionalB, let c = optionalC {}
Optional chaining:
struct Shortcuts {
let shortcuts: [String]
var program: Program?
}
struct Program {
let name: String
}
var shortcuts = Shortcuts(shortcuts: ["Ctrl-A", "Ctrl-E"])
shortcuts.program = Program(name: "Terminal")
print(shortcuts.program!.name)
shortcuts.program = nil
if let terminalShortcuts = shortcuts.program?.name {
print(terminalShortcuts)
} else {
print("No shortcuts")
}
enum LifeParameters {
case sacrifice, consistency, adjustment
}
enum LifeParameters {
case values(Int, Int, Int, Int)
case code(String)
}
enum LifeParameters: String {
case sacrifice
case consistency
case adjustement
case trust
}
For negative indices, Swift’s % operator preserves the sign of the left-hand operand.
struct Note {
let name: String
let notes = ["C", "C#", "D", "D#", "E", "F", "F#","G", "G#", "A", "A#", "B"]
subscript(index: Int) -> String {
guard let startIndex = notes.firstIndex(of: name) else { return "?" }
let count = notes.count
// Make it work for negative indices
let normalizedIndex = (startIndex + index % count + count) % count
return notes[normalizedIndex]
}
}
let note = Note(name: "C#")
print(note[-1])
let imagery = { (human: String, object: String) in
print("\(human) is picturing a \(object) in their mind.")
}
// No return statement
imagery("Joe","guitar")
let imagery_return = { (human: String, object: String) -> String in
return "\(human) is picturing a \(object) in their mind."
}
let harmonica_image = imagery_return( "Joe", "harmonica")
print(harmonica_image)
// Mixing Void and String return types
func juggling(_ location: () -> Void, _ move: () -> String) {
print("Joe has started juggling at")
location()
let move = move()
print("With his special move: \(move)")
}
let location = { print("Vison") }
let move = { return "Behind the Neck" }
juggling(location, move)
// With trailing closure syntax
juggling(location) {
return "Backcrosses"
}
func draw(object: String, time: (Int) -> Void) {
// time is closure that takes an Int as an arguemnt
// 30 is being passed to the closure
time(30)
}
draw(object: "bridge") { (time: Int) in
// here
print("I spent \(time) minutes on it")
}
func travel() -> (String) -> Void {
return {
print("I'm going to \($0)")
}
}
travel()("Fukuoka")
// or
let result = travel()
result("Osaka")
func makeArray(size: Int, using generator: () -> Int) -> [Int] {
var numbers = [Int]()
for _ in 0..<size {
let newNumber = generator()
numbers.append(newNumber)
}
return numbers
}
let rolls = makeArray(size: 10) {
Int.random(in: 1...10)
}
struct VideoGame {
let title: String
let copiesSold: Int
let console: String
}
var v = [
VideoGame(title: "Mario 64", copiesSold: 140, console: "N64"),
VideoGame(title:"The Legend of Zelda: Majora's Mask", copiesSold: 244, console: "N64"),
VideoGame(title: "Paper Mario 64", copiesSold: 430, console: "N64"),
VideoGame(title: "Pokemon Platinum", copiesSold: 4000, console: "Nintendo DS")
]
// We could further simplify this with $0 and $1
let copiesSort = { (g1: VideoGame, g2: VideoGame) -> Bool in
return g1.copiesSold > g2.copiesSold
}
let consoleFilter = { (console: String) -> (VideoGame) -> Bool in
return { $0.console == console }
}
v.sort(by: copiesSort)
let N64Games = v.filter(consoleFilter("N64"))
let DSGames = v.filter { $0.console == "Nintendo DS"}
Referencing the capture list guide by Paul Hudson, and adding my twist to it. I first create a Dancer class.
class Dancer {
let name: String
let type: String
func dance() {
print("\(self.name) is busting \(self.type) moves")
}
init(name: String, type: String) {
self.name = name
self.type = type
}
}
Strong capture: Referenced object is kept alive after the function finishes execution.
func joeDance() -> () -> Void {
let joe = Dancer(name: "Joe", type: "breakdance")
// This closure is capturing Joe strongly
// It's keeping it alive after the function finishes running
let danceToSong = {
joe.dance()
}
return danceToSong
}
let danceFunction = joeDance()
danceFunction() // Joe is busting breakdance moves
Weak capture: Swift treats the captured objects as optionals, so you have to unwrap them. According to the documentation, the weak objects captured have a shorter lifetime. When the weakJoeDance function finishes, the joe Dancer is already deallocated, but the closure still exists. In the example below, by the time danceToSong is returned, the Dancer object is already gone.
func weakJoeDance() -> () -> Void? {
let joe = Dancer(name: "Joe", type: "Hip-hop")
// This closure doesn't hold Joe's hand tightly anymore
let danceToSong = { [weak joe] in
// joe.dance will give you a warning (sln: optional chain)
// joe!.dance will crash (sln: don't write this)
if let joe = joe {
joe.dance()
}
}
return danceToSong
}
let weakDanceFunction = weakJoeDance()
weakDanceFunction() // prints nothing, Joe was never retained (reference count: 0)
We can modify what’s above to keep joe alive (thank you) by creating another reference keepingJoeAlive to it.
func weakJoeDanceWithParameters() -> (String) -> Void? {
let joe = Dancer(name: "Joe", type: "Contemporary")
// Add this reference to joe
let keepingJoeAlive = joe
// DanceToSong Closure retains Joe because of keepingJoeAlive
let danceToSong = { [weak joe] (songName: String) in
// This prints because joe is kept alive by the keepingJoeAliveReference
joe?.dance()
print("\(songName) is playing in the background")
keepingJoeAlive.dance()
}
return danceToSong
}
let weakJoeDanceWithParametersFunction = weakJoeDanceWithParameters()
weakJoeDanceWithParametersFunction("Endless Summer Nights")
Unowned capture: Swift treats them like implicitly unwrapped optionals, which means you don’t need to unwrap them. Unowned is for referenced objects with the same or a longer lifetime.
func unownedJoeDance() -> () -> Void? {
let joe = Dancer(name: "Joe", type: "Locking")
let danceToSong = { [unowned joe] in
// No need to unwrap
// joe.dance will crash if joe has been deallocated
joe.dance()
}
return danceToSong
}
let unownedDanceFunction = unownedJoeDance()
unownedDanceFunction() // Crashes
Asynchronous tasks need to live inside an asynchronous context and get rowdy. This includes Task, .task or a function marked with async. Keep in mind that even in an asynchronous context, execution can still be synchronous.
await vs async let
You control execution order with await, it’s a suspension point.
let runConcurrently = true
if runConcurrently {
// This block could have any print order
// (123), (132), (312)
async let _: () = asyncWork()
async let _: () = otherAsyncWork()
} else {
// await introduces suspension points
// This block will always print (123)
await asyncWork()
await otherAsyncWork()
}
// Those functions provide asynchronous contexts
func asyncWork() async {
print("Step 1")
print("Step 2")
}
func otherAsyncWork() async {
print("Step 3")
}
Sleeping Tasks
Play with this code. Before running it, what do you think it prints? What happens if you change the sleep duration?
import Foundation
func asyncWork() async {
await MainActor.run {
print("Step 1: Started asyncWork on main thread:", Thread.current)
}
// returns nil if it fails (sleep can throw)
try? await Task.sleep(nanoseconds: 1_000_000_000)
await MainActor.run {
print("Step 2: Resumed asyncWork on main thread:", Thread.current)
}
}
func doOtherWork() {
print("Doing other work. isMainThread:", Thread.isMainThread)
}
struct App {
static func main() async {
await MainActor.run {
print("Main started on main thread:", Thread.current)
}
await asyncWork()
Task {
// And if you change this to 200_000_000
try? await Task.sleep(nanoseconds: 50_000_000)
print("Be patient, Joe!")
}
doOtherWork()
try? await Task.sleep(nanoseconds: 100_000_000)
// FINISH:
// At this point, execution is finished.
// Unfinished sleeping tasks get cancelled after reaching this point.
}
}
await App.main()
This section will grow if not forgotten.
frame -> background -> shadow -> corner radius won't show the shadow
frame -> background -> corner radius -> shadow
If a view is getting pushed down because of another, add a frame.
If you see: “The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions”, check your initializers.
No comments on this post yet. Be the first to share your wisdom :).