Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
178: Custom Collection Conformance
I've seen this happen a dozen times: a developer wants their custom data structure to feel like a first-class citizen in Swift, so they slap Collection conformance on it. They want to be able to use .map, .filter, and for-in loops without writing a manual wrapper. But then, the app crashes the moment they actually try to iterate over it.
Take a look at this PagedResults wrapper. It's intended to wrap an array of items coming back from an API, but it's crashing with an "Index out of range" error during a simple loop.
struct PagedResults<Element>: Collection {
let items: [Element]
typealias Index = Int
var startIndex: Index {
return 0
}
var endIndex: Index {
// The developer thinks endIndex should be the last valid index
return items.count - 1
}
subscript(position: Index) -> Element {
return items[position]
}
}
let results = PagedResults(items: ["Apple", "Banana", "Cherry"])
for fruit in results {
print(fruit)
}
// CRASH: Index out of range
The Off-By-One Boundary Error
The crash happens because of a fundamental misunderstanding of how Swift defines the endIndex. In a Swift Collection, the endIndex is not the index of the last element. It is the "past-the-end" position.
When you write for fruit in results, Swift starts at startIndex and continues until it reaches endIndex. However, the loop logic internally checks if the current index is less than the endIndex. By setting endIndex to items.count - 1, you've effectively told Swift that the collection ends one element earlier than it actually does. In many cases, this just means you miss the last element. But if your collection is empty, items.count - 1 becomes -1, and the whole thing falls apart instantly.
Aligning the Index Boundary
To fix this, we need to treat the endIndex as a sentinel value. It represents the boundary where the collection stops, not a piece of data you can actually access via the subscript. I usually tell people to think of it as a "Stop" sign at the end of the road.
struct PagedResults<Element>: Collection {
let items: [Element]
typealias Index = Int
var startIndex: Index {
return 0
}
var endIndex: Index {
// Correct: the count is exactly one past the last valid index
return items.count
}
subscript(position: Index) -> Element {
return items[position]
}
}
Now, the loop works perfectly. When items.count is 3, the valid indices are 0, 1, and 2. The loop runs while index < 3, so it hits 0, 1, and 2, then stops exactly when it hits 3. No crash, no missing elements.
Connecting the Subscript to the Index
Once you've nailed the boundaries, the subscript is where the actual work happens. Because we used Int as our Index, we can just pass that integer directly into the underlying array.
But here is a pro tip: if you are wrapping a more complex structure (like a Linked List or a custom Tree), don't use Int. Create a dedicated struct for your index. This prevents users of your collection from passing in random integers that might not make sense for your specific data layout. If you use a custom index type, you'll also need to conform that index to Comparable so the collection knows how to move from startIndex to endIndex.
The beauty of conforming to Collection is that you get a massive amount of functionality for free. By defining just those four things—startIndex, endIndex, index(after:) (which Int provides automatically), and the subscript—you suddenly have access to first, last, contains, isEmpty, and every single sequence operation in the Swift Standard Library.
📋 Practical Task
Implement a Restricted-Access Buffer Collection
You are building a system that manages a raw buffer of data, but for security reasons, only a specific "window" of that data should be accessible to the rest of the application. You need to create a SecureWindow collection that wraps a larger array but only allows iteration and access within a specified range.
Requirements:
- Create a struct
SecureWindow<Element>that conforms toCollection. - The struct should take an internal array
allData: [Element]and arange: Range<Int>. - The
startIndexmust be the lower bound of the range. - The
endIndexmust be the upper bound of the range. - The
subscriptmust return the element fromallDataat the given position. - Ensure that the collection only iterates over the elements within the specified range, ignoring everything else in the
allDataarray.
Test Case:
let data = [10, 20, 30, 40, 50, 60]
let window = SecureWindow(allData: data, range: 1...3) // Elements 20, 30, 40
print(window.count) // Should be 3
for item in window {
print(item) // Should print 20, 30, 40
}
There are no comments for now.