demo

This post is about creating a confetti animation in SwiftUI with a customizable duration. I learned the technique from this Patreon tutorial.

In the original animation, the confetti pieces would change color while falling, which I considered a bug. I’ve since fixed it. Another optimization I considered was adding a fade-out effect to make the disappearance of the confetti smoother, but the result wasn’t satisfactory, so I have not included that change.

Let’s break down how to build this.

Step 1: Animate a Single Piece of Confetti

First, we’ll create the animation for a single piece of confetti. The code and the result are below:

confetti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ConfettiView: View {
@State private var animate = false
@State private var xSpeed = Double.random(in: 0.7...2.0)
@State private var zSpeed = Double.random(in: 1.0...2.0)
@State private var anchor = CGFloat.random(in: 0...1).rounded()
@State private var color: Color = [.orange, .green, .blue, .red, .yellow].randomElement() ?? .green

var body: some View {
Rectangle()
.fill(color)
.frame(width: 14, height: 14)
.onAppear { animate = true }
.rotation3DEffect(.degrees(animate ? 360 : 0), axis: (x: 1, y: 0, z: 0))
.animation(.linear(duration: xSpeed).repeatForever(autoreverses: false), value: animate)
.rotation3DEffect(.degrees(animate ? 360 : 0),
axis: (x: 0, y: 0, z: 1),
anchor: UnitPoint(x: anchor, y: anchor))
.animation(.linear(duration: zSpeed).repeatForever(autoreverses: false), value: animate)
}
}

This line of code is the key to fixing the color-changing issue:

@State private var color: Color = [.orange, .green, .blue, .red, .yellow].randomElement() ?? .green

The color used to change because the original code would assign a new random color during the animation process. By using @State to store the color, we ensure its value is preserved for the view’s lifetime, preventing the color from changing unexpectedly.

Step 2: Create the Confetti Container

Next, we’ll create a container to hold 50 confetti pieces and manage their positioning.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct ConfettiContainerView: View {
var count: Int = 50
@State private var ySeed: CGFloat = 0

var body: some View {
GeometryReader { geo in
ZStack {
ForEach(0..<count, id: \.self) { _ in
ConfettiView()
.position(
x: CGFloat.random(in: 0...geo.size.width),
y: ySeed == 0 ? 0 : CGFloat.random(in: 0...geo.size.height)
)
}
}
.ignoresSafeArea()
.onAppear {
ySeed = geo.size.height
}
}
}
}
A view filled with multiple, randomly positioned confetti pieces.

Step 3: Encapsulate as a View Modifier

Finally, to make the animation reusable and easy to apply, we’ll wrap it in a ViewModifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct ConfettiModifier: ViewModifier {
@Binding var isActive: Bool
@State private var opacity = 1.0

private let animationTime = 3.0
private let fadeTime = 1.6

func body(content: Content) -> some View {
content
.overlay(
ZStack {
if isActive {
ConfettiContainerView()
.opacity(opacity)
.allowsHitTesting(false)
}
}
)
.onChange(of: isActive) { _, newValue in
guard newValue else { return }
Task {
await sequence()
}
}
}

private func sequence() async {
do {
try await Task.sleep(nanoseconds: UInt64(animationTime * 1_000_000_000))

withAnimation(.easeOut(duration: fadeTime)) {
opacity = 0
}

try await Task.sleep(nanoseconds: UInt64(fadeTime * 1_000_000_000))
isActive = false
opacity = 1.0
} catch {}
}
}

extension View {
func displayConfetti(isActive: Binding<Bool>) -> some View {
modifier(ConfettiModifier(isActive: isActive))
}
}

// Usage Example
struct ContentView: View {
@State private var showConfetti = false

var body: some View {
VStack {
Text("You did it!").font(.title.bold())
Button("Celebrate") {
showConfetti = true
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.displayConfetti(isActive: $showConfetti)
}
}

Sometimes, you have to explore different paths to find where you truly belong. For a developer, that path is often paved with different languages, frameworks, and platforms. My own journey has taken me through the worlds of backend development, cross-platform apps with React Native and Flutter, and of course, native iOS. And after seeing what each has to offer, I’ve come to a clear and exciting realization: my true passion lies in building for native iOS.

TakeOffLight

A Journey Across the Stack

Every technology I’ve worked with has taught me something invaluable.

  • Building backend systems gave me a deep appreciation for data architecture, APIs, and the logic that powers applications from behind the scenes.
  • Working with React Native and Flutter was a fantastic lesson in efficiency and the challenge of creating a consistent experience across different operating systems. The promise of “write once, run anywhere” is compelling, and I learned a great deal about managing a single codebase for multiple targets.

This broad perspective is something I wouldn’t trade. It gave me a holistic view of how a product comes to life, from the database all the way to the user’s screen. But it also created a point of comparison that continually highlighted what makes native development, and specifically iOS, so special to me.

The Pull of Native iOS

There’s a certain elegance and satisfaction in iOS development that I kept coming back to. The seamless integration between the Swift language, powerful frameworks like SwiftUI, and the hardware itself allows for a level of polish and performance that is simply a joy to create. The pursuit of the perfect animation, the crispness of a native UI component, and the satisfaction of building something that feels completely at home on the device—that’s what excites me as a developer.

After reflecting on this, I felt a renewed surge of energy and inspiration. To channel it into something tangible, I decided to build a small, focused project that captures the kind of delightful interaction I love: a simple “lights-out” animation.

Project: The “Lights-Out” Animation

I wanted to create more than just a toggle. I wanted to build an experience. The idea was to mimic the satisfying, physical act of pulling a cord to turn a light on and off, complete with animated light beams, a draggable cord, and a crisp sound effect.

Here is a video of the final result!

This project, while small, was a great way to put SwiftUI’s strengths to the test, focusing on:

  • Declarative UI: Building complex views that react to state changes.
  • State Management: Using @State and @Binding to drive the entire UI from a single source of truth (isOn).
  • Animation: Leveraging withAnimation and animation modifiers to create fluid transitions and spring physics for the pull cord.
  • Gestures: Implementing a DragGesture to create an interactive and intuitive pull-cord mechanism.

A Look at the Code

For those interested in how it works, the full source code is available on my GitHub. But here are a few key pieces that bring the experience to life.

The core of the app is the ContentView, which manages the isOn state. This single boolean drives everything from the background color to the sound playback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct ContentView: View {
@State var isOn = true

var body: some View {
ZStack {
// The background's gradient changes based on the 'isOn' state
LightBeamBackground(isOn: self.$isOn)
.ignoresSafeArea()

// The main lightbulb button that can also toggle the state
Button(action: {
withAnimation {
self.isOn.toggle()
}
}) {
// ... button content
}
}
.overlay(alignment: .topTrailing) {
// The interactive pull cord is an overlay
PullCord(isOn: self.$isOn)
.padding(.top, -40)
}
.onChange(of: self.isOn) { _, newValue in
// Play a sound effect whenever the state changes
SoundPlayer.shared.play(newValue ? .lightOn : .lightOff)
}
}
}

The most interactive piece is the PullCord view. It uses a DragGesture to track the user’s finger and provides physical feedback by stretching. When the drag is released, it decides whether to toggle the light based on how far it was pulled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct PullCord: View {
@Binding var isOn: Bool
@State private var dragOffsetY: CGFloat = 0
private let maxDragDistance: CGFloat = 140

var body: some View {
ZStack(alignment: .top) {
// ... visual components for the cord and handle
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
let dy = max(0, value.translation.height)
dragOffsetY = min(dy, maxDragDistance)
}
.onEnded { value in
let shouldToggle = value.translation.height > maxDragDistance * 0.6
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
dragOffsetY = 0
if shouldToggle {
isOn.toggle()
}
}
}
)
}
}

This project reaffirmed my belief in the power and elegance of SwiftUI for creating these kinds of delightful, polished user experiences.

What’s Next

This journey of exploration across the tech stack has been invaluable, but now I know where I want to build my future. I’m currently based in Calgary and am actively seeking my next role as an iOS Developer. I’m looking for a team where I can contribute my diverse experience, my passion for Apple’s ecosystem, and my drive to build beautiful, high-performing applications.

If you’re looking for a passionate iOS developer with a broad technical perspective, I would love to connect. You can find my LinkedIn profile here.

Good night, Calgary. Hopeful for new opportunities and interviews tomorrow!

Advanced Techniques with Binding: Transforming and Adapting State

While the standard property wrappers handle most state management needs, you will often encounter situations where the shape of your state doesn’t perfectly match the requirements of a SwiftUI view. For example, a view might need a Binding<String>, but your model provides a Binding<String?>. Or a view needs to bind to the associated value of an enum case.

SwiftUI’s Binding type is incredibly powerful, and with a few extension methods, we can transform bindings to fit our exact needs. Let’s use the following data model for our examples:

1
2
3
4
5
6
7
8
9
10
struct Item: Hashable, Identifiable {
let id = UUID()
var color: Color?
var status: Status

enum Status: Hashable {
case inStock(quantity: Int)
case outOfStock(isOnBackOrder: Bool)
}
}

Given a state variable like @State var item: Item, a binding to its status, $item.status, would have the type Binding<Item.Status>. Let’s see how we can manipulate this and other bindings.

Drilling Down into Bindings with Key Paths

SwiftUI has built-in support for creating bindings to the properties of a bound value. When you write $item.status, Swift is transparently applying a transform to map the Binding<Item> to a Binding<Item.Status>. This is conceptually achieved through a map function that uses a WritableKeyPath:

1
2
3
4
5
extension Binding {
func map<LocalValue>(_ keyPath: WritableKeyPath<Value, LocalValue>) -> Binding<LocalValue> {
self[dynamicMember: keyPath]
}
}

You rarely need to call this map function directly, as the . syntax ($item.status) is convenient shorthand for the same operation. It’s the simplest way to transform a Binding<A> into a Binding<B>.

Handling Optionals: The unwrap Extension

A very common scenario is dealing with optional state. For instance, our item.color property is a Color?, making $item.color a Binding<Color?>. However, SwiftUI’s ColorPicker view requires a Binding<Color>. It cannot work with optionals.

To bridge this gap, we can create a handy unwrap extension that transforms an optional binding Binding<A?> into a non-optional binding Binding<A>?. The result is itself an optional: it will be nil if the original state is nil, or a valid, non-optional Binding if the state has a value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Binding {
/// Transforms a `Binding<Wrapped?>` into a `Binding<Wrapped>?`.
/// If the original binding's value is `nil`, this returns `nil`.
func unwrap<Wrapped>() -> Binding<Wrapped>? where Value == Wrapped? {
// If the wrapped value is nil, we can't create a binding.
guard let value = self.wrappedValue else { return nil }

// Create a new binding that gets the non-optional value
// and sets the optional value on the original source.
return Binding<Wrapped>(
get: { value },
set: { self.wrappedValue = $0 }
)
}
}

Usage:

You can use this with an if let statement to conditionally show a view that requires a non-optional binding.

1
2
3
4
5
6
7
8
9
10
11
12
struct ItemEditorView: View {
@Binding var item: Item

var body: some View {
// Only show the ColorPicker if a binding to a non-optional Color can be created.
if let colorBinding = $item.color.unwrap() {
ColorPicker("Item Color", selection: colorBinding)
} else {
Text("This item has no color set.")
}
}
}

A More General Solution for Enums: matching with CasePaths

The unwrap function is actually a specific version of a broader problem: how do we bind to the associated value of an enum case? An Optional is just an enum with two cases: .none and .some(Wrapped). unwrap effectively extracts the associated value from the .some case.

To create a more generic solution for any enum, we can leverage the excellent CasePaths library. A CasePath is like a “key path for an enum case,” allowing you to reliably extract associated values from and embed them back into an enum.

Building on this, we can create a matching function that returns a binding to an associated value if and only if the binding’s value is currently in that specific case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Requires the 'CasePaths' library: https://github.com/pointfreeco/swift-case-paths
import CasePaths

extension Binding {
/// Returns a new binding focused on a specific case of an enum.
/// If the original binding's value does not match the case, this returns `nil`.
func matching<Case>(
_ casePath: CasePath<Value, Case>
) -> Binding<Case>? {
// Attempt to extract the associated value from the current state.
guard let `case` = casePath.extract(from: self.wrappedValue) else { return nil }

// Create a new binding.
return Binding<Case>(
// The getter returns the extracted associated value.
get: { `case` },
// The setter embeds the new value back into the case and updates the original source.
set: { `case` in self.wrappedValue = casePath.embed(`case`) }
)
}
}

Usage:

This powerful extension allows you to build UI that adapts to the state of an enum. For our Item.Status, we can show completely different controls for each case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ItemStatusView: View {
@Binding var status: Item.Status

var body: some View {
VStack {
// This UI will only appear and work if status is .inStock
if let quantityBinding = $status.matching(/Item.Status.inStock) {
Stepper("Quantity: \(quantityBinding.wrappedValue)", value: quantityBinding)
}

// This UI will only appear and work if status is .outOfStock
if let onBackOrderBinding = $status.matching(/Item.Status.outOfStock) {
Toggle("Is on backorder", isOn: onBackOrderBinding)
}
}
}
}

These techniques for transforming Binding are essential for writing clean, decoupled SwiftUI code. They allow your views to remain simple and focused on their specific data requirements, while your data models can remain complex and robust.

A Comprehensive Guide to State Management in SwiftUI

In SwiftUI, managing the state of your application—the data that drives your UI—is a fundamental concept. SwiftUI provides a set of powerful property wrappers that handle view updates automatically when your data changes. This guide explores the core tools: @State, @StateObject, @ObservedObject, and @Published.

Core Property Wrappers at a Glance

The following table provides a quick reference for the most common state management property wrappers in SwiftUI. Note that @StateObject, @ObservedObject, and @Published are all integral parts of the Combine framework, which SwiftUI uses for reactive programming.

Property Wrapper Purpose Data Type Ownership & Lifecycle Primary Use Case
@State Manages simple, private state within a single view. Value types (e.g., Struct, Enum, Int, String, Bool). Owned and managed by the view. SwiftUI manages its storage. It may be re-initialized if the view’s identity changes in the view hierarchy. It is view-local. Controlling the local state of UI components, such as a Toggle‘s on/off state, a TextField‘s input, or whether an alert is shown.
@StateObject Manages a complex, private state object within a single view. Reference types (Class) that must conform to ObservableObject. Owned & Persisted by the View. SwiftUI ensures the object’s instance persists for the lifetime of the view’s identity, even across redraws. It can be passed to other views. Creating and managing an instance of a complex data model (like a ViewModel) within the view that owns it.
@ObservedObject Subscribes to an existing observable object from an external source. Reference types (Class) that must conform to ObservableObject. The view does not own the object; it merely “borrows” or “observes” it. Its lifecycle is managed externally. Receiving and responding to a shared data model in a subview, where the model is managed by a parent view or another part of the app.
@Published Automatically publishes notifications when a property’s value changes. Any type. Its lifecycle is tied to the ObservableObject instance it belongs to. Marking properties within a ViewModel or shared data model that should trigger UI updates whenever they are modified.

A key distinction to remember is that a view’s @State can be destroyed and recreated if the view is removed and re-added to the view hierarchy. In contrast, @StateObject is designed to survive view redraws as long as the view maintains its identity.

Understanding the Nature of SwiftUI Views

Before diving deeper, it’s crucial to understand what a SwiftUI View is. The struct you define (e.g., struct MyView: View) is not the persistent object you see on screen. Instead, it’s a lightweight “blueprint” or “description” of your UI.

  • View Structs are Ephemeral: Every time SwiftUI needs to update the UI (perhaps because a @State variable changed), it re-creates your view struct and calls its body property to get a new blueprint. Creating and destroying these structs is extremely fast and low-cost.
  • “Redraw” = “Re-evaluating body“: When we say a “view redraws,” it’s more accurate to say that “the view’s body property is re-evaluated,” which often results in new view structs being created.

The Magic of @StateObject: Separating State from the View Struct

@StateObject was introduced to solve the problem of state being reset during view redraws. Its mechanism works as follows:

  1. View Identity: SwiftUI uniquely identifies a view by its position and type within the View Tree. For example, “the first UserProfileView inside the VStack in ContentView.”

  2. First-Time Creation & Storage: The very first time a view with this specific identity appears, SwiftUI sees the @StateObject property wrapper. It then:

    • Executes your initialization code (e.g., _viewModel = StateObject(wrappedValue: UserViewModel())) to create an instance of your ObservableObject.
    • SwiftUI then takes this newly created instance and stores it in a special, managed memory area associated with that specific view identity.
  3. Subsequent Redraws: Later, when a parent view’s state changes and your view’s body is re-evaluated, a new view struct is created. However:

    • SwiftUI again sees the @StateObject property wrapper.
    • This time, it checks its internal storage and finds that an object is already associated with this view’s identity.
    • It skips your initialization code and simply connects the property to the pre-existing instance from its managed memory.

Practical Example: The Lifecycle of @State

The following code demonstrates how @State is tied to the view instance’s lifetime within the view hierarchy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import SwiftUI

struct CounterView: View {
// A @State variable, initialized to 0
@State private var count = 0

init() {
// Log when this view is created
print("✅ CounterView has been initialized.")
}

var body: some View {
VStack {
Text("Counter Value: \(count)")
.font(.title)
.padding()

Button("Increment Count") {
count += 1
}
}
.padding()
.border(Color.blue, width: 2)
// This is called when the view is removed from the view tree
.onDisappear {
print("❌ CounterView has disappeared.")
}
}
}

struct ContainerView: View {
@State private var showCounter = true

var body: some View {
VStack(spacing: 30) {
Toggle("Show/Hide Counter", isOn: $showCounter.animation())
.padding()

if showCounter {
// When showCounter is true, CounterView exists in the view tree
CounterView()
} else {
// When showCounter is false, CounterView is completely removed
// A placeholder Text shows the structural change
Text("Counter is hidden")
.foregroundColor(.gray)
}

Spacer()
}
.navigationTitle("State Lifecycle Demo")
}
}

How to Run and Observe

  1. Run the ContainerView. You will see the counter, and the console will print: “✅ CounterView has been initialized.”
  2. Click the “Increment Count” button a few times to increase the count (e.g., to 5).
  3. Now, tap the “Show/Hide Counter” Toggle to turn it off.
    • The CounterView will disappear from the screen.
    • The console will print: “❌ CounterView has disappeared.” This confirms the view instance was destroyed.
  4. Tap the Toggle again to turn it back on.
    • CounterView reappears on the screen.
    • The console will again print: “✅ CounterView has been initialized.” This proves that SwiftUI has created a brand new CounterView instance.
    • You will notice that the counter’s value has reset to 0, not the 5 you left it at.

This behavior perfectly illustrates that @State‘s storage is tied to the lifecycle of its containing view in the view hierarchy. If the view is removed, its state is lost. This is precisely the scenario where @StateObject should be used if you need the state to persist as long as the view’s identity remains the same.

I am ready to systematically review the iOS knowledge, so I have this simple Swift 6 Programming study notes.

Part 1: Core Concepts

1. Value vs. Reference Types

Types in Swift are divided into value types (struct, enum) and reference types (class). The fundamental difference lies in how their data is stored and passed.

  • Value Types: Each instance keeps a unique copy of its data. When you pass a value type, it is copied.
  • Reference Types: Instances share a single copy of their data. When you pass a reference type, a reference (or pointer) to the instance is passed.

Copy-on-Write

To optimize performance, many of Swift’s standard library value types (like Array and Dictionary) use a technique called Copy-on-Write. This means a copy is only made when the data needs to be modified; otherwise, multiple instances share the same data storage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import Foundation

// A backing class for demonstration
class BackendQueue<T> {
private var items = [T]()
public init() {}
private init(_ items: [T]) { self.items = items }
public func addItem(item: T) { items.append(item) }
public func getItem() -> T? {
if items.count > 0 { return items.remove(at: 0) }
else { return nil }
}
public func count() -> Int { return items.count }
public func copy() -> BackendQueue<T> { return BackendQueue<T>(items) }
}

// A struct wrapper to implement Copy-on-Write
struct Queue {
private var internalQueue = BackendQueue<Int>()

public mutating func addItem(item: Int) {
checkUniquelyReferencedInternalQueue()
internalQueue.addItem(item: item)
}

public func count() -> Int {
return internalQueue.count()
}

mutating private func checkUniquelyReferencedInternalQueue() {
if !isKnownUniquelyReferenced(&internalQueue) {
internalQueue = internalQueue.copy()
print("Making a copy of internalQueue")
} else {
print("Not making a copy of internalQueue")
}
}
}

print("Start")
var queue1 = Queue()
var queue2 = queue1 // No copy happens here

print("ADD")
queue1.addItem(item: 1) // Modifying queue1 triggers a copy
queue1.addItem(item: 2)
print(queue1.count())
print("Done")

/*
Console Output:
Start
ADD
Making a copy of internalQueue
Not making a copy of internalQueue
2
Done
*/

Noncopyable Types (~Copyable)

Swift 6 introduces noncopyable types to represent unique resources like file handles or network sockets, ensuring they are not accidentally duplicated.

1
2
3
4
struct Person: ~Copyable {
var firstName: String
var lastName: String
}

Two key concepts related to noncopyable types are borrowing and consuming.

  • Borrowing (borrowing): Grants temporary, read-only access to a noncopyable value without transferring ownership. Borrowed values are thread-safe.
  • Consuming (consuming): Transfers ownership of a noncopyable value, and the original variable becomes invalid. A consuming method ends the object’s lifetime upon its return. Global instances cannot be consumed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The `borrowing` keyword indicates the function temporarily borrows `user` without taking ownership
func sendEmail(_ user: borrowing Person) {
print("Sending Email to \(user.firstName)")
}

// The `consuming` keyword indicates the function consumes `user`, taking ownership
func consumeUser(_ user: consuming Person) {
print("Consuming User \(user.firstName)")
}

func userFunction() {
let user = Person(firstName: "Jon", lastName: "Hoffman")
sendEmail(user) // user is borrowed
consumeUser(user) // user is consumed, and the variable becomes invalid afterward
}

We can also create consuming methods that invalidate the instance once executed.

1
2
3
4
5
6
7
8
9
struct SecrectMessage: ~Copyable {
private var message: String
init(_ message: String) { self.message = message }

// After this consuming method is executed, the instance is destroyed
consuming func read() {
print("\(message)")
}
}

2. Enumerations

Enumerations define a common type for a group of related values.

  • Raw Values: Enum members can be prepopulated with a default value.

    1
    2
    3
    4
    5
    6
    enum Direction: String {
    case North = "N", South = "S", West = "W", East = "E"
    }
    enum Month: Int {
    case January = 1, February, March, April // 2, 3, 4 are inferred automatically
    }
  • Associated Values: Store custom values associated with an enum member.

    1
    2
    3
    4
    enum Product {
    case Book(Double, Int, Int) // price, year, pages
    case Puzzle(Double, Int) // price, pieces
    }
  • Pattern Matching: The switch statement makes it easy to handle different enum cases and extract their associated values.

    1
    2
    3
    4
    5
    6
    7
    let masterSwift = Product.Book(49.99, 2024, 394)
    switch masterSwift {
    case .Book(let price, let year, let pages):
    print("Mastering Swift was published in \(year) for \(price) and has \(pages) pages")
    case .Puzzle(let price, let pieces):
    print("A puzzle with \(pieces) pieces and sells for \(price)")
    }
  • Enum Iteration: By conforming to the CaseIterable protocol, you can iterate over all members of an enumeration.

    1
    2
    3
    4
    5
    6
    7
    enum DaysOfWeek: String, CaseIterable {
    case Monday = "Mon", Tuesday = "Tues", Wednesday = "Wed", Thursday = "Thur", Friday = "Fri", Saturday = "Sat", Sunday = "Sun"
    }

    for day in DaysOfWeek.allCases {
    print("-- \(day.rawValue)")
    }

3. Closures

Closures are self-contained blocks of functionality that can be passed around and used in your code.

  • Basic Syntax:

    1
    2
    3
    4
    let clos1 = { () -> Void in
    print("hello world")
    }
    clos1()
  • Shorthand Syntax: Swift provides several ways to simplify closure syntax.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // $0, $1 represent the first and second parameters
    guests.map { print("hello \($0)") }

    // If the closure is the only argument to a function, the parentheses can be omitted
    testFunction2(num: 5) {
    print("hello from \($0)")
    }

    // Single-expression closures can implicitly return their result
    let clos7 = { (first: Int, second: Int) -> Int in first + second }
    print(clos7(1, 2)) // Prints 3
  • Escaping Closures (@escaping): When a closure is called after the function it was passed to returns, it needs to be marked with the @escaping keyword. This typically happens when the closure is stored for later use or executed in an asynchronous operation.

    1
    2
    3
    4
    var handlers: [LogLevel: [logLevelHandler]] = [:]
    func registerHandler(for level: LogLevel, handler: @escaping logLevelHandler) {
    handlers[level, default: []].append(handler)
    }

4. Error Handling

Swift provides a powerful error handling model that allows you to represent and respond to recoverable errors.

  • Defining Errors: Create an enum that conforms to the Error protocol to represent different kinds of errors.

    1
    2
    3
    4
    5
    6
    enum PlayerNumberError: Error {
    case NumberTooHigh(description: String)
    case NumberTooLow(description: String)
    case NumberAlreadyAssigned
    case NumberDoesNotExist
    }
  • Throwing Errors (throws): Use the throw keyword to throw an error within a function. The function’s signature must be marked with throws.

    1
    2
    3
    4
    5
    6
    mutating func addPlayer(player: BaseballPlayer) throws {
    guard player.number < maxNumber else {
    throw PlayerNumberError.NumberTooHigh(description: "Max number is \(maxNumber)")
    }
    // ... other checks
    }
  • Catching Errors (do-catch): Use a do-catch statement to call a function that can throw an error.

    1
    2
    3
    4
    5
    6
    7
    8
    do {
    let player = try myTeam.getPlayerByNumber(number: 34)
    print("Player is \(player.firstName)")
    } catch PlayerNumberError.NumberDoesNotExist {
    print("No player has that number")
    } catch let error { // Catch all other errors
    print("An error occurred: \(error)")
    }

    You can also match multiple error patterns in a single catch clause:

    1
    2
    3
    catch PlayerNumberError.NumberTooHigh, PlayerNumberError.NumberTooLow {
    print("Number is out of range.")
    }
  • LocalizedError Protocol: Conforming to this protocol can provide richer, localized descriptions for your errors.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    enum PlayerNumberError: Error, LocalizedError {
    // ... cases
    var errorDescription: String? {
    switch self {
    case .NumberAlreadyAssigned:
    return "Player number already assigned"
    // ... other descriptions
    }
    }
    }
  • Defer Statement (defer): The code within a defer block is executed just before the current scope is exited, whether by normal completion or by throwing an error. This is very useful for resource cleanup.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func processFile() {
    let file = openFile()
    defer {
    closeFile(file) // Ensures the file is always closed
    print("File closed.")
    }
    // ... file processing code that might throw an error
    print("File processed.")
    }

5. Memory Management

Swift uses Automatic Reference Counting (ARC) to manage memory. ARC tracks the number of references to class instances. When the reference count for an instance drops to zero, the instance is deallocated, and its memory is freed.

1
2
3
4
5
6
7
8
9
10
class MyClass {
var name: String
init(name: String) { self.name = name; print("Initializing \(name)") }
deinit { print("Releasing \(name)") }
}

var ref1: MyClass? = MyClass(name: "One") // Reference count is 1
var ref2: MyClass? = ref1 // Reference count is 2
ref1 = nil // Reference count is 1
ref2 = nil // Reference count is 0, instance is deallocated

Strong Reference Cycles

If two class instances hold a strong reference to each other, they will never be deallocated, causing a memory leak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass1_Strong {
var class2: MyClass2_Strong?
// ...
}
class MyClass2_Strong {
var class1: MyClass1_Strong?
// ...
}

var class1: MyClass1_Strong? = MyClass1_Strong(name: "Class1")
var class2: MyClass2_Strong? = MyClass2_Strong(name: "Class2")
class1?.class2 = class2 // class1 holds class2
class2?.class1 = class1 // class2 holds class1 (cycle!)

class1 = nil
class2 = nil // Instances will not be released

Solutions:

  • Weak References (weak): Use when the referenced instance might become nil. A weak reference does not increase the reference count and must be an optional type.

    1
    2
    3
    class MyClass2_Weak {
    weak var class1: MyClass1_Weak? // Weak reference
    }
  • Unowned References (unowned): Use when you are certain the referenced instance will never be nil during the current instance’s lifetime. An unowned reference is not optional, and accessing a deallocated unowned reference will trigger a runtime error.

    1
    2
    3
    class MyClass1_Unowned {
    unowned let class2: MyClass2_Unowned // Unowned reference
    }

Part 2: Protocol-Oriented & Functional Programming

Swift is a multi-paradigm language with strong support for Protocol-Oriented Programming (POP) and Functional Programming.

1. Object-Oriented Programming (OOP)

The three pillars of OOP are Encapsulation, Inheritance, and Polymorphism.

  • Inheritance: Supported by reference types (class). While powerful, complex class hierarchies can increase code complexity and coupling, making modification and maintenance difficult.
  • Dynamic Dispatch: When a method on a class is called, the runtime uses a virtual table (VTable) to look up and call the correct implementation. This provides flexibility but is slightly slower than a direct call.

2. Protocol-Oriented Programming (POP)

POP is a core design philosophy in Swift. It emphasizes defining blueprints using protocols rather than relying on class inheritance.

  • Protocol Definition: A protocol defines a blueprint of methods, properties, and other requirements.
    1
    2
    3
    protocol Nameable {
    var firstName: String { get }
    }
  • Protocol Composition: A type can conform to multiple protocols, combining different functionalities.
    1
    2
    protocol Person: Nameable, Contactable { /* ... */ }
    struct Employee: Person, Occupation { /* ... */ }
  • Protocol Inheritance: Protocols can also inherit from other protocols, aggregating multiple requirements.

Compared to class inheritance, POP offers greater flexibility and modularity, helping to avoid bloated base classes.

3. Protocols and Protocol Extensions

  • Type Checking and Casting: You can use is and as? to check if an instance conforms to a protocol.

    1
    2
    3
    for person in people where person is SwiftProgrammer {
    print("\(person.firstName) is a Swift Programmer")
    }
  • Protocol Extensions: You can extend protocols to provide default implementations for methods. Types conforming to the protocol automatically gain this functionality.

    1
    2
    3
    4
    5
    6
    7
    8
    protocol Dog {
    var name: String { get }
    }
    extension Dog {
    func speak() -> String {
    return "Woof Woof"
    }
    }
  • Any vs any:

    • Any: Can represent a value of any type, including function and optional types.
    • any: Used to modify a protocol, representing an existential type. It allows you to store values of different types that conform to the same protocol in a container and supports dynamic dispatch.
  • Implicitly Opened Existentials: Swift 6 allows the compiler to implicitly open existential types, simplifying operations on protocol arrays.

    1
    2
    3
    4
    5
    6
    7
    protocol Drawable { func draw() }
    // No manual casting needed, protocol methods can be called directly
    func drawAll(_ items: [any Drawable]) {
    for item in items {
    item.draw()
    }
    }

4. Generics

Generic code enables you to write flexible, reusable functions and types that can work with any type.

  • Generic Functions:
    1
    2
    3
    func swapGeneric<T>(a: inout T, b: inout T) {
    let tmp = a; a = b; b = tmp
    }
  • Associated Types (associatedtype): Used in a protocol as a placeholder for a type that is specified only when the protocol is adopted.
    1
    2
    3
    4
    5
    protocol Queue {
    associatedtype QueueType
    mutating func add(item: QueueType)
    mutating func getItem() -> QueueType?
    }
  • Conditional Extensions and Conformance: You can add extensions to a generic type that are only available if the generic parameter meets certain conditions.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Add the sum method only if T is a numeric type
    extension List where T: Numeric {
    func sum() -> T { items.reduce(0, +) }
    }

    // List conforms to Equatable only if T also conforms to Equatable
    extension List: Equatable where T: Equatable {
    static func == (l1: List, l2: List) -> Bool {
    // ... comparison logic
    }
    }

5. Functional Programming

Core principles of functional programming include:

  • Immutability: Prefer constants (let) to avoid direct state modification.
    1
    2
    let numbers = [1, 2, 3, 4, 5]
    let doubled = numbers.map { $0 * 2 } // The `numbers` array itself is not changed
  • Pure Functions: Always produce the same output for the same input and have no side effects.
    1
    2
    3
    func add(_ first: Int, _ second: Int) -> Int {
    return first + second
    }
  • Higher-Order Functions: Functions that take other functions as arguments or return them, such as map, filter, and reduce.
    1
    2
    3
    func performMathOperation(_ first: UInt, _ second: UInt, function: (UInt, UInt) -> UInt) -> UInt {
    return function(first, second)
    }

Advanced Techniques:

  • Function Composition: Combining multiple functions into a new one.
    1
    2
    3
    4
    5
    infix operator >>>
    func >>> <A, B, C>(lhs: @escaping (A) -> B, rhs: @escaping (B) -> C) -> (A) -> C {
    return { rhs(lhs($0)) }
    }
    let addOneToString = addOne >>> toString
  • Currying: Transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.
    1
    2
    3
    4
    5
    func curriedAdd(_ a: Int) -> (Int) -> Int {
    return { a + $0 }
    }
    let addTwo = curriedAdd(2)
    let result = addTwo(3) // result is 5
  • Recursion: A function calling itself to solve a problem.
    1
    2
    3
    4
    func factorial(_ n: Int) -> Int {
    if n <= 1 { return 1 }
    return n * factorial(n - 1)
    }

Part 3: Modern Concurrency

Swift provides a concurrency model ranging from the low-level GCD to the modern async/await structured approach.

1. Grand Central Dispatch (GCD)

GCD is a low-level C API that manages tasks via queues.

  • Concurrency: Multiple tasks starting, running, and completing in the same time period.
  • Parallelism: Multiple tasks running at the exact same moment, which requires a multi-core processor.

Queue Types:

  • Serial Queues: Tasks are executed one at a time in FIFO order. Often used to synchronize access to a shared resource.
  • Concurrent Queues: Tasks start in order but can run concurrently. The system determines the number of concurrent tasks.
  • Main Dispatch Queue: A globally available serial queue that executes tasks on the application’s main thread, typically used for UI updates.

Using Queues:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a concurrent queue
let cqueue = DispatchQueue(label: "cqueue.example", attributes: .concurrent)
// Create a serial queue
let squeue = DispatchQueue(label: "squeue.example")

// Execute a task asynchronously without blocking the current thread
cqueue.async {
performCalculation(tag: "async1")
}

// Switch from a background thread to the main thread to update the UI
squeue.async {
let resizedImage = image.resize()
DispatchQueue.main.async {
picView.image = resizedImage
}
}

Advanced Tools:

  • DispatchGroup: Coordinate the completion of multiple asynchronous tasks.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let group = DispatchGroup()
    group.enter()
    queue.async { /* task 1 */; group.leave() }
    group.enter()
    queue.async { /* task 2 */; group.leave() }

    group.notify(queue: .main) {
    print("All tasks are complete")
    }
  • Barrier: Create a synchronization point in a concurrent queue. All tasks submitted before the barrier complete before the barrier task executes. Tasks submitted after the barrier wait for it to finish.
    1
    2
    3
    queue.async(flags: .barrier) {
    // This task waits for previous tasks and blocks subsequent ones
    }
  • DispatchSemaphore: Control the number of concurrent accesses to a shared resource.
    1
    2
    3
    4
    5
    6
    let semaphore = DispatchSemaphore(value: 1) // Allow only one thread to access
    func accessSharedResource() {
    semaphore.wait() // Request access, wait if count is 0
    // ... access shared resource ...
    semaphore.signal() // Finished, release the resource
    }

2. Structured Concurrency (async/await)

Swift 6 emphasizes using structured concurrency to write safer, more readable asynchronous code, preventing issues like data races.

async and await

  • async: Marks a function as asynchronous, meaning it can be suspended during its execution.
  • await: Used to call an async function, indicating that the current task might pause here to wait for the result of the asynchronous function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func retrieveUserData() async -> String {
print("Retrieving user data")
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "User Data Retrieved"
}

// Serial execution
let data = await retrieveUserData()

// Parallel execution
async let userData = retrieveUserData()
async let imageData = retrieveImageData()
let results = await (userData, imageData)
print("User Data: \(results.0), Image Data: \(results.1)")

Tasks

A Task represents a unit of work that can be run asynchronously.

  • Creating a Task:
    1
    2
    3
    4
    Task {
    let data = await retrieveUserData()
    print("Data: \(data)")
    }
  • Detached Task (Task.detached): Creates a top-level task that does not inherit the context (like actor isolation) of its creation point.
  • Task Cancellation: Tasks can be cancelled externally. In a long-running loop, you should periodically check Task.isCancelled and exit gracefully.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func testCancelTask() async throws {
    for i in 0..<10 {
    if Task.isCancelled {
    print("Task was cancelled, cleaning up")
    throw CancellationError()
    }
    print("Loop \(i)")
    await retrieveUserData()
    }
    }

Task Groups

Used for creating a dynamic group of concurrent tasks and waiting for all of them to complete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func taskGroup() async -> [String] {
return await withTaskGroup(of: String.self) { group in
let users = ["Jon", "Heidi", "Kailey", "Kai"]
for user in users {
group.addTask {
return await retrieveUserData(user)
}
}

var data = [String]()
for await result in group {
data.append(result)
}
return data
}
}

Actors

Actors are a special kind of reference type that protect their mutable state from concurrent access, preventing data races. Access to an actor’s internal state is asynchronous and serialized.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
actor BankAccount {
private var balance: Double
init(_ balance: Double) { self.balance = balance }

func deposit(amount: Double) {
balance += amount
}

func getBalance() -> Double {
return balance
}
}

let account = BankAccount(5000)
await account.deposit(amount: 100)
let newBalance = await account.getBalance()
print("New Balance: \(newBalance)")

Sendable Types

The Sendable protocol marks types whose values can be safely passed between concurrency domains (e.g., from one actor to another).

  • Automatic Conformance: Swift’s core value types (Int, String, etc.), structs and enums containing only Sendable values, and actor types automatically conform to Sendable.
  • Manual Conformance: A class can conform to Sendable if it is final, all its properties are immutable constants (let), and the types of those properties also conform to Sendable.

Part 4: Advanced & Specialized Features

1. Property Observers and Wrappers

  • Property Observers (willSet/didSet): Execute code before (willSet) or after (didSet) a property’s value is set.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct MyStruct {
    var myProperty: String {
    willSet(newName) {
    print("Preparing to change from \(myProperty) to \(newName)")
    }
    didSet {
    if oldValue != myProperty {
    print("Value changed from \(oldValue) to \(myProperty)")
    }
    }
    }
    }
  • Property Wrappers (@propertyWrapper): Encapsulate the storage and logic of a property into a separate type to reduce code duplication.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @propertyWrapper
    struct MyPropertyWrapper<T> {
    private var value: T
    var wrappedValue: T {
    get { /* return the value */ }
    set { /* modify the value */ }
    }
    init(wrappedValue initialValue: T) {
    self.value = initialValue
    }
    }

    struct MyExample {
    @MyPropertyWrapper var number: Int
    }

2. Key Paths and Dynamic Member Lookup

  • Key Paths: Provide a type-safe way to reference a property of a type. The syntax is \TypeName.propertyName.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct BasketballTeam {
    var city: String
    }
    let cityKeyPath = \BasketballTeam.city
    var team = BasketballTeam(city: "Boston")
    let teamCity = team[keyPath: cityKeyPath] // "Boston"

    // Simpler syntax in map/filter
    let names = people.map(\.name)
    let adults = people.filter { $0.age > 17 } // Traditional way
    let adultsWithKeyPath = people.filter { $0[keyPath: \.age] > 17 }
  • Dynamic Member Lookup (@dynamicMemberLookup): Allows a type to access members dynamically using dot syntax, even if those members are not explicitly defined at compile time.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @dynamicMemberLookup
    struct BaseballTeam {
    let city: String
    let nickName: String

    subscript(dynamicMember key: String) -> String {
    switch key {
    case "fullname":
    return "\(city) \(nickName)"
    default:
    return "Unknown"
    }
    }
    }
    let team = BaseballTeam(city: "Boston", nickName: "Red Sox")
    print(team.fullname) // Prints "Boston Red Sox"

3. Custom Subscripting

Allows you to access instances of a type by index, similar to an array or dictionary.

  • Basic Subscript:
    1
    2
    3
    4
    5
    6
    7
    class MyNames {
    private var names = ["Jon", "Kailey", "Kai"]
    subscript(index: Int) -> String {
    get { return names[index] }
    set { names[index] = newValue }
    }
    }
  • Multi-dimensional Subscript:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct TicTacToe {
    var board = [["","",""],["","",""],["","",""]]
    subscript(x: Int, y: Int) -> String {
    get { return board[x][y] }
    set { board[x][y] = newValue }
    }
    }
    var board = TicTacToe()
    board[1, 1] = "x"
  • Static Subscript:
    1
    2
    3
    4
    5
    6
    struct Hello {
    static subscript(name: String) -> String {
    return "Hello \(name)"
    }
    }
    let greeting = Hello["Jon"]

4. Result Builders

Result builders are a special syntax transformation that lets you build a complex result from a sequence of statements, commonly used for creating Domain-Specific Languages (DSLs), like SwiftUI’s view builder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: " ")
}
}

func buildString(@StringBuilder _ components: () -> String) -> String {
return components()
}

let result = buildString {
"Hello,"
"Mastering"
"Swift!"
}
print(result) // "Hello, Mastering Swift!"

5. Reflection

Swift’s Mirror API allows you to inspect the properties, types, and values of an instance at runtime. Swift’s reflection is read-only, in keeping with its principle of type safety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = Person(firstName: "Jon", lastName: "Hoffman", age: 55)
let mirror = Mirror(reflecting: person)

for (label, value) in mirror.children {
print("Property: \(label ?? "Unknown"), Value: \(value)")
}

// Can be used to implement a generic serialization function
func serialize<T>(_ value: T) -> [String: Any] {
let mirror = Mirror(reflecting: value)
var result = [String: Any]()
for child in mirror.children {
if let propertyName = child.label {
result[propertyName] = child.value
}
}
return result
}

6. Regular Expressions

Swift provides modern and powerful support for regular expressions.

  • Literal Syntax:
    1
    2
    3
    4
    5
    6
    let pattern = /\b\w+\b/
    let text = "Hello from regex literal"
    let matches = text.matches(of: pattern)
    for match in matches {
    print("-- \(text[match.range])")
    }
  • RegexBuilder: Construct complex regular expressions in a declarative way.
    1
    2
    3
    4
    5
    6
    7
    8
    import RegexBuilder

    let pattern = Regex {
    Anchor.wordBoundary
    OneOrMore(.word)
    "@"
    // ... more components
    }
  • Capturing: You can define references to capture matched parts and perform type conversions.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let animalTypeRef = Reference(Substring.self)
    let ageRef = Reference(Int.self)

    let pattern = Regex {
    "I am a "
    Capture(as: animalTypeRef) { OneOrMore(.word) }
    " who is "
    TryCapture(as: ageRef) { OneOrMore(.digit) } transform: { Int($0) }
    // ...
    }

    if let match = str.firstMatch(of: pattern) {
    print("Animal Type: \(match[animalTypeRef])")
    print("Age: \(match[ageRef])")
    }

Part 5: Code Quality & Organization

1. Access Control

Access control restricts access to parts of your code from code in other source files and modules.

  • open: The most permissive. Allows access and subclassing from any module (for classes only).
  • public: Allows access from any module.
  • internal: The default level. Allows access only within the defining module.
  • fileprivate: Allows access only within the defining source file.
  • private: The most restrictive. Allows access only within the enclosing declaration (like a struct or class).

Best Practices:

  • Default to the most restrictive access level and relax it as needed.
  • Encapsulate implementation details and expose only the necessary API.
  • Maintain consistency by following a uniform access control strategy throughout your codebase.

2. Availability Checks

Use #available to check the operating system version and execute different code accordingly.

1
2
3
4
5
if #available(iOS 16.0, macOS 13.0, *) {
// Use new APIs for iOS 16+ and macOS 13+
} else {
// Provide a fallback for older versions
}

You can also mark an entire function or type with @available.

1
2
3
4
@available(iOS 16.0, *)
func newFeature() {
// This function is only available on iOS 16 and later
}

3. Swift Testing

Swift provides the Testing framework, a modern and expressive solution for testing.

  • @Test: Marks a function as a test function.

  • #expect and #require:

    • #expect: Checks if a condition is true. If it fails, the test continues to run.
    • #require: Checks if a condition is true. If it fails, the test terminates immediately.
    1
    2
    3
    4
    5
    @Test func validExpectation() throws {
    #expect(1 == 1)
    let one: Int? = 10
    let willSucceed = try #require(one) // Succeeds, test continues
    }
  • Test Suites (@Suite): Used to organize related tests. Any struct containing @Test functions automatically becomes a suite.

    1
    2
    3
    4
    @Suite("Calculator Tests")
    struct CalculatorTests {
    @Test func testAddition() { /* ... */ }
    }
  • Parameterized Tests: Provide multiple sets of inputs and expected outputs for a single test.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct TestValues { let first: Double, second: Double, answer: Double }

    @Test("Addition Tests", arguments: [
    TestValues(first: 2, second: 3, answer: 5),
    TestValues(first: 10, second: 11, answer: 21)
    ])
    func testAddition(_ values: TestValues) {
    #expect(Calculator.addition(values.first, values.second) == values.answer)
    }
  • @testable: Imports a module and allows test code to access its internal members.

    1
    @testable import MyApp

Today I start to prepare Duolingo English Test. I made honey sweet potato chips for the children today, and the children loved it.

honey sweet potato chips

Vim is a powerful and versatile text editor that offers a wide range of features and customization options. In this article, we will explore some useful tips and techniques from the book “Modern Vim” to help you become more efficient and productive in your Vim editing.

img

Introduction

Vim has a unique set of keyboard shortcuts and commands that can enhance your editing experience. Here are some key concepts and shortcuts to keep in mind:

  • <C-p> represents pressing the Control key followed by the letter “p.”

  • Operators are commands used to perform actions on text. Some common operators include:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    `c` for change
    `d` for delete
    `y` for yank (copy)
    `~` for swapping case
    `gu` for making text lowercase
    `gU` for making text uppercase
    `!` for filtering text through an external program
    `=` for text formatting
    `gq` for text formatting with line wrapping
    `>` for shifting text right
    `<` for shifting text left
    `zf` for defining a fold
    `g@` for calling a function set with the 'operatorfunc' option
  • The Meta key:

    • On macOS, it refers to the Option key.
    • On Windows, it refers to the Alt key.

Getting Modern Vim

To get the most out of Vim, it’s recommended to use Neovim and the neovim-remote tool developed by Marco Hinz. This tool allows remote control of Neovim processes.

Installing Plugins

Plugins are a great way to extend Vim’s functionality. Here are some insights on managing plugins:

  • Understanding Scripts, Plugins, and Packages:

    • You can manually load a script using the :source {path} command, where {path} is the location of the script.
    • Vim automatically sources scripts located in specific locations on disk when it starts up.
    • Your vimrc file is one of the first scripts to be loaded, making it an ideal place to configure your startup settings.
    • Prior to recent versions of Vim, managing the runtimepath to include plugins was not convenient. However, you can now use the :set runtimepath+=$VIMCONFIG/arbitrary/demo-plugin command to add a plugin to the runtimepath.
    • Pressing <C-]> will jump to the specified anchor in Vim’s documentation, and you can use <C-o> to quickly jump back to the previous location. These commands allow you to navigate Vim’s documentation similar to interacting with a web page.
    • After installing a new plugin, you only need to run :helptags once. Vim will then use the generated tags file to find the documentation for that plugin.
  • Installing Plugins to Your Package:

    • Note that if you install a new plugin into the start directory while Vim is running, you won’t be able to use it immediately. Restarting Vim will add the new plugin to the runtimepath and make it available.
    • The unimpaired plugin comes with documentation, but Vim doesn’t know where to find the appropriate files. You can fix this issue by running the :helptags ALL command (:help :helptags).
    • You can suppress error messages by running :silent helptags ALL.
    • By default, optional plugins are not loaded. Use the :packadd command to activate a plugin (e.g., :packadd vim-scriptease).
  • Managing Plugins with minpac:

    • Typing :call minpac#update() can be cumbersome. You can create custom commands to make it more convenient:
      • command! PackUpdate call minpac#update()
      • command! PackClean call minpac#clean()

Opening Files

Efficiently opening and navigating files is crucial for an effective editing workflow. Let’s explore two techniques:

  • Finding Files Using Fuzzy Path Matching:

    • You can use <C-x>, <C-v>, or <C-t> to open a file in a horizontal split, vertical split, or new tab, respectively.
    • The rg --files command (Ripgrep) filters out files ignored by Git, Mercurial, and Subversion repositories.
  • Finding Files Semantically:

    • Open files in separate windows using the -O flag. For example:

      • vim -O file1.txt file2.txt
    • You can define file-to-type mappings in a .projections.json file. For instance:

      • "app/models/*.js": { "type": "model" }
    • Vim provides navigation commands specific to file types, such as:

      • ```
        :Etype - Opens the specified type in the current window
        :Stype - Opens the specified type in a horizontal split
        :Vtype - Opens the specified type in a vertical split
        :Ttype - Opens the specified type in a new tabpage
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        		
        - These navigation commands are my preferred way to navigate codebases, and you can add navigation commands as needed for different file types.

        - Jumping to an Alternate File:
        - The Projectionist plugin allows you to create links between related files. Once you specify the relationship between a file and its alternate file, you can follow the link by running the `:A` command.

        ## Working with the Quickfix List

        The quickfix list is a powerful feature in Vim that allows you to efficiently handle build errors, linting results, and file search results. Let's explore some techniques:

        - Running a Build and Navigating Failures:
        - The Dispatch plugin, introduced in 2013, provides asynchronous command execution in Vim when it didn't natively support it. Make sure to check out the plugin's release and the "dispatch.vim" screencast.
        - Linting the Current File:
        - Learn about the Asynchronous Linting Engine (ALE), a powerful plugin for linting code in Vim.
        - You can use the `]w` and `[w` mappings to quickly navigate between warnings. Error messages are displayed at the bottom of the screen as you access each warning.
        - Neomake is another linting plugin that runs asynchronously. It supports running commands across the entire project, not just on individual files.
        - Searching Files with Grep-Alikes:
        - The `:Grepper` command provides a powerful way to search for patterns in files. For example, running `:Grepper -cword` with the word "Waldo" under the cursor will prompt you for search options.

        ## Neovim's Built-In Terminal Emulator

        Neovim comes with a built-in terminal emulator that allows you to interact with programs running in the terminal. Let's dive into some terminal-related techniques:

        - Grokking Terminal Mode:
        - Neovim introduces a new mode called Terminal mode, where you can interact with programs running in the built-in terminal emulator.
        - Use the `:terminal` command to open a terminal buffer.
        - When you create a terminal buffer, you start in normal mode. Pressing `i` switches to terminal mode, indicated by the

        `-- TERMINAL --` prompt in the bottom left corner. Press `<C-><C-n>` to switch back to normal mode.
        - Running Programs in a Terminal Buffer:
        - Use the `:read !{cmd}` command to capture the output of a command in an existing buffer.
        - The `:terminal {cmd}` command is a new feature in Neovim. It runs the specified command in a new terminal buffer. You can abbreviate it as `:te {cmd}`.
        - To switch between a terminal buffer and a regular buffer, use `<C-^>` (`:h ctrl-^`).
        - Try stopping a process in the terminal buffer using the `:bwipeout!` command (e.g., `:5bwipeout` to stop the top process).
        - When you exit Neovim, any running processes in terminal buffers are also closed.
        - Note that if you suspend Neovim (`<C-z>`), all processes running in terminal buffers will be suspended as well. They will resume when you resume Neovim.

        - Managing Windows That Contain Terminal Buffers:
        - Opening a terminal buffer with the `:terminal` command takes over the current window and hides the buffer that was previously displayed. This behavior is similar to the `:edit {file}` command.
        - If you use `:te`, it creates a buffer. However, `:te` does not create a buffer.
        - Use the `tnoremap` command to create mappings that work only in terminal mode. With these mappings, you can switch to another window by pressing `<M-h>`, `<M-j>`, `<M-k>`, or `<M-l>` regardless of whether you are in normal mode or terminal mode.

        - Using Normal Mode Commands in a Terminal Buffer:
        - You can use the `yi`` command to copy the text within backticks to Vim's unnamed register and then paste it using `p` in the terminal at the cursor position.
        - You can prepend a named register (e.g., `"a`) or use special registers like `"*` to reference the system clipboard when using yank and put commands.
        - The terminal buffer is now hidden, but you can quickly switch back to it using `<C-^>` (`:h ctrl-^`).

        - Sending Commands to a Terminal Buffer:
        - Activate the window containing the terminal buffer running the web server and run the following command:
        - `:echo b:terminal_job_id`
        - This tells us that the job ID is 1, which we can use as the first argument when calling `jobsend({job}, {data})`.
        - To restart the web server, run the command:
        - `:call jobsend(1, "\<C-c>npm run server\<CR>")`

        ## Sessions

        Sessions in Vim allow you to save and restore your editing environment. Let's explore session-related techniques:

        - Saving and Restoring Sessions:
        - After opening the `app.js` and `app-test.js` files in adjacent windows, use the `:mksession!` command (`:h :mksession`) to save the session.
        - Restart Vim with the `-S` flag to load the session:
        - `vim -S`
        - If you like the idea of automatically recording sessions, consider installing Tim Pope's Obsession plugin. You can install it in your bundle directory:
        - `cd $VIMCONFIG/pack/bundle/start`
        - `git clone https://github.com/tpope/vim-obsession.git`

        - Making Undo Persist Between Sessions:
        - By default, undo history is not preserved between sessions. However, you can use autocommands to disable the undofile for files matching specific patterns. For example, the following is an example script that disables persistent undo in all files in the `/tmp` directory:
        - `--forget-undo-in-tmpfile.vim`
        - ```
        augroup vimrc
        autocmd!
        autocmd BufWritePre /tmp/* setlocal noundofile
        augroup END
  • Restarting Terminal Processes When Resuming a Session:

    • You can rename a terminal buffer using the :file {name} command (:help :file_f). Activate the window containing the terminal buffer running the web server and run:
      • :file term://PORT=3001 npm run server

Configuring Vim

Customizing Vim’s behavior can greatly enhance your editing experience. Here’s a technique to respond to events using autocommands:

  • Using Autocommands to Respond to Events:
    • Vim triggers the BufReadPost command (:h BufReadPost) after reading a file into a buffer. If the file path matches the pattern defined in our autocommand, Vim executes the specified {command}. Since we use a wildcard * in this example, the autocommand applies to all buffers.
    • Sometimes, there might be more suitable events. For such cases, you can achieve similar results by listening to the FileType event (:h FileType).
      • autocmd BufWritePre /tmp/* setlocal noundofile
    • The autocommand we defined is triggered by the User event with the pattern ProjectionistActivate. The User event doesn’t trigger automatically, but you can trigger such events yourself:
      • :doautocmd User ProjectionistActivate

In this article, we’ve explored various tips and techniques from “Modern Vim” to help you enhance your Vim editing skills. By incorporating these techniques into your workflow, you can become a more efficient and productive Vim user.

什么是Keystone?

Keystone是一个强大的Node.js内容管理系统,它是建立在Express和Mongoose ODM上的Web App框架。Mongoose ODM是面向文档映射,为那些存储到MongoDB数据库中文档,其提供了面向基于模式的解决方案来为数据和关系建模。Keystone扩展了Mongoose基于模式模型。Keystone列表利用智能字段类型帮助你构建漂亮的Admin界面。

Keystone的目标是让你更容易的构建复杂网站和应用,而不限制你自定义功能。你可以引入你自己的视图引擎,设计你想要的路由,并且修改你的数据结构来适应你的需求。

先决条件

确保你安装好了Node.js JavaScript运行环境。Keystone 4主要是在Node 6上进行测试,但是应当是和最新正式版Node是兼容的。我们推荐使用Node LTS(长期支持)版本,因为更长的支持周期和稳定性。(译者注:LTS 版本更注重稳定性和扩展支持,通常将支持 30 个月。)

你也要可以访问一个MongoDB数据库,不管是安装在本地还是部署在远程服务器上。Keystone 4兼容最新的MongoDB正式版。

你需要有一定的JavaScript、Node.js和npm的知识来使用Keystone。了解数据库概念和MongoDB则更有帮助。

从哪里开始?

快速开始指导

如果你想要在本地快速运行一个项目,我们推荐你从Keystone Yeoman生成器开始。Keystone Yeoman生成器提供了一个提示命令行来帮助你生成完整的项目,可以通过npm来定制一些特性如基础的博客,反馈表单,相册功能。

从脚手架开始设置

如果你更喜欢从脚手架编写你的代码,产看我们第四部分的设置教程。这个教程让你从核心的文件和设置来开始Keystone。

接下来去哪?

如果你想要学习更多的Keystone设置选项,请查阅我们的文档。数据库配置是一个好的起点,同样Keystone通用设置选项也是不错的选择。

给定一组单词,返回所有字母都在同一行美国键盘上的单词。

例子:

Input: ["Hello", "Alaska", "Dad", "Peace"]
Output: ["Alaska", "Dad"]

注意:

  1. 可以使用同一个键盘上字母多次
  2. 假设输入有且只有字母

解决思路有两种。最简单的方法是利用正则表达式来判断字母是否满足正则表达式,如果满足则表示单词有效。这里面有几点需要注意的。

  1. 需要一个正确的正则表达式。
  2. 判断之前需要将单词转换成小写的。
  3. 将有效的单词收集并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {    
func findWords(_ words: [String]) -> [String] {
// 利用正则表达式来判断
let pattern = "^([qwertyuiop]*|[asdfghjkl]*|[zxcvbnm]*)$"
var validateWords = [String]()
for word in words {
let lowerWord = word.lowercased()

let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex?.matches(in: lowerWord, options: .reportProgress, range: NSMakeRange(0, lowerWord.characters.count))
if let m = matches, m.count > 0 {
validateWords.append(word)
}
}
return validateWords
}
}

第二种方法就麻烦一点了。

  1. 首先将单词设置成有效
  2. 找到第一个字母在键盘的那一行。
  3. 接着判断所有字母是否是在那一行,如果不在则单词无效。
  4. 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
func findWords(_ words: [String]) -> [String] {
// 1. 判断单词属于num行
// 2. 标示单词有效
// 3. 如果单词不属于num行
// 4. 标示单词无效
let lines: [String] = ["qwertyuiop", "asdfghjkl", "zxcvbnm"];
var validateWords = [String]()
for word in words {
let lowerWord = word.lowercased()
validateWords.append(word)

for line in lines {
if !line.characters.contains(lowerWord[lowerWord.startIndex]) {
continue
}

for c in lowerWord.characters {
if !line.characters.contains(c) {
validateWords.popLast()
break
}
}
}
}
return validateWords
}
}

给定一个正数,输出它的“补数”。求的方法是把二进制位置的数进行取反。

注意:

  1. 给定的正数在32位内
  2. 正数前面没有补0。比如2(B10),在它的前面没有0。

例子1:

输入: 5
输出: 2
解释: 5的二进制数是101 (前面不补充0), 它的补数是010。所以输出是2。

例子2:

输入: 1
输出: 0
解释: 1的二进制是1 (前面不补充0), 它的补数是0。因此输出是0。

思路

假设输入数为input, 输出为output, 设mask的二进制位数与input相等,且每一位都为1。
这里求input有两种方法。这里你可以用上面的例子数据代进去验证一下。

  1. output = input ^ mask
  2. output = mask - input

所以这个问题的关键是如何得到mask。

还有第三种思路是这样的。每次将input分别与其对应二进制位为1的数进行异或操作,就能得到output。

1
2
3
4
5
6
7
class Solution(object):
def findComplement(self, num):
i = 1
while num >= i:
num ^= i
i <<= 1
return num

计算mask方法一:

1
2
3
4
5
6
var flipMask: UInt32 = ~0
let numUInt32 = UInt32(num)
while ((numUInt32 & flipMask) != 0) {
flipMask <<= 1
}
let mask = ~flipMask;

将全是’1’的UInt32数不断的左移,直到(numUInt32 & flipMask) == 0停止,然后再对flipMask取反就能得到要求的mask

计算mask方法二:

1
2
3
4
5
6
7
int mask = 0;
int j = 0;
while (mask < num)
{
mask += Math.pow(2, j);
j++;
}

从右往左不断的增加1的位数,直到mask >= num

计算mask方法三:

1
2
3
4
5
6
int mask = num;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;
mask |= mask >> 16;

每次与右移后的值进行与操作,移动的位数每次能扩大2倍。这样就能保证mask的所有二进制位都是1。

计算mask方法四:

1
mask = ((2<<int(math.log(num, 2)))-1)

利用数学函数能得到input的位数(int(math.log(num, 2))+1), 这样进1位减去1就得到了mask值。

0%