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 ChoreoBuilder, 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.
struct Drawing {
var title: String
}
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"}
This section will grow if not forgotten.
frame -> background -> shadow -> corner radius won't show the shadow
frame -> background -> corner radius -> shadow
No comments on this post yet. Be the first to share your wisdom :).