This course is for people who finished the beginner iOS course, or who already build small apps and want to work at the level a team expects. We start with the language, because most advanced iOS problems turn out to be language problems: how to write code once that works for many types, how to hide a repeated rule behind a clean name, and how to keep code safe when it grows. Lesson 1 covers the four tools that Apple's own frameworks use everywhere, and that interviewers ask about: generics, protocols with associated types, protocol extensions and property wrappers.
Generics: one function, many types
A generic function or type works with a placeholder type, written in angle brackets, instead of one fixed type. Swift fills in the real type at each use and checks it fully, so you get reuse without losing safety. You have already used generics without noticing: [Expense] is Array<Expense>, and Optional<Int> is Int?.
func largest<T: Comparable>(in items: [T]) -> T? {
items.max()
}
print(largest(in: [3, 9, 4]) ?? 0) // 9
print(largest(in: ["pear", "apple"]) ?? "") // pear
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
var top: Element? { items.last }
}
extension Stack where Element: Equatable {
func contains(_ item: Element) -> Bool { items.contains(item) }
}
T: Comparable is a constraint: it says *T can be any type, as long as it can be compared*. Without it, the compiler would not let max() compile, because it cannot know that two Ts can be ordered. The where Element: Equatable extension adds contains only for stacks whose elements can be compared for equality, so a Stack<Int> gets it and a stack of non-equatable things does not.
Protocols with associated types
A plain protocol lists methods. A protocol can also name a placeholder type of its own, called an associated type. This is how you describe a *kind of thing that works with some type*, such as a repository that stores some kind of item.
protocol Repository {
associatedtype Item
func all() async throws -> [Item]
func save(_ item: Item) async throws
}
actor InMemoryRepository<Item: Sendable>: Repository {
private var items: [Item] = []
func all() -> [Item] { items }
func save(_ item: Item) { items.append(item) }
}
func countItems<R: Repository>(in repository: R) async throws -> Int {
try await repository.all().count
}
The same protocol can be met by an in-memory version for tests, a SwiftData version for the app and a network version for a server, and countItems works with all of them. This is the heart of the *depend on protocols* idea from the first course, now with types that vary.
some and any
some Repositorymeans *one specific type that I do not name, but the compiler knows*. SwiftUI'ssome Viewis the same idea. It is fast, because the real type is known at build time. A function that returnssomemust return one type from every path.any Repositorymeans *a box that can hold any type that meets the protocol*, and the real type is found at run time. It is more flexible, since you can put different types in one array, and a little slower. Swift 5.7 and later allowanywith protocols that have associated types.- As a habit: use generics or
somefirst, and useanywhen you really need to store different types together.
Protocol extensions: free behaviour
You can give every type that meets a protocol a ready-made method, by writing an extension on the protocol. Types may use it as it is, or write their own. Swift calls this style protocol-oriented programming. It is the reason that so much of the standard library comes with free helpers.
protocol Describable {
var name: String { get }
}
extension Describable {
var summary: String { "This is \(name)" }
}
struct Category: Describable { let name: String }
struct Account: Describable {
let name: String
var summary: String { "Account \(name) (custom)" }
}
print(Category(name: "Food").summary) // This is Food
print(Account(name: "Cash").summary) // Account Cash (custom)
Property wrappers
A property wrapper moves the get and set rules of a property into a reusable type. You have used several: @State, @Binding, @AppStorage, @Query. Writing your own lets you hide a repeated rule, such as keeping a value in a range.
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Review {
@Clamped(1...5) var stars = 3
}
var review = Review()
review.stars = 9
print(review.stars) // 5
A wrapper may also offer a projectedValue, which you reach with a dollar sign. That is why $title gives a binding for @State: the State wrapper projects a Binding. Another feature you meet through SwiftUI is the result builder, written @resultBuilder. It lets many lines inside braces become one value, which is how a VStack { ... } collects its children. You rarely write one, and it helps to know where the magic comes from.
Key paths
A key path is a reference to a property, without reading it, written with a backslash: \Expense.amount. They are used by @Query(sort:) and by many APIs, and they can stand in for a closure, as in expenses.map(\.amount).
Common mistakes
- Over-generic code. If only one type will ever use a function, a generic only makes it harder to read. Reach for generics when you have a second real use.
- Using
anyeverywhere out of habit, and losing the speed and clarity of generics. - A property wrapper that hides work a reader would be surprised by. Keep wrappers small and obvious.
Your task
- Write a generic
Stack<Element>withpush,popandtop, and test it withIntandString. - Add a
Repositoryprotocol and two types that meet it: an actor with an array, and one that stores to a dictionary by anid. - Write a
@Trimmedproperty wrapper that removes spaces at both ends of aStringevery time it is set. - Use
map(\.amount)andsorted { $0.amount > $1.amount }on an array of expenses, and write one sentence on what a key path is.
How to check it worked
largest(in:)works forIntandStringand returnsnilfor an empty array.- Setting
review.stars = 9ends at 5, and your@Trimmedproperty removes spaces. - You can say, in your own words, the difference between
someandany.