Learn Swift Generics in 15 Minutes or Keep Writing Duplicate Code Forever
Stop writing the same function 10 different ways and learn the generic approach that’ll save you hours of repetitive coding
Swift Essentials

🔄 The Duplicate Code Problem
Alright so… I’m gonna show you some code from when I was still figuring Swift out. It’s kinda painful to look at now.
I had this task — needed to swap values around. Easy stuff. Except I needed it for integers in one place, strings in another, and then some custom objects elsewhere. My brain went “okay, just write three functions then.”
Yeah.
func exchangeIntegers(_ first: inout Int, _ second: inout Int) {
let temporary = first
first = second
second = temporary
}
func exchangeStrings(_ first: inout String, _ second: inout String) {
let temporary = first
first = second
second = temporary
}
func exchangeDoubles(_ first: inout Double, _ second: inout Double) {
let temporary = first
first = second
second = temporary
}You see it right? Same exact logic. Three times. Only difference is the type.
And yeah, every single time I needed to swap a new type? Copy, paste, rename. My code reviews must’ve made senior devs cringe.
Now check out what happens when you actually know generics:
func exchangeValues<T>(_ left: inout T, _ right: inout T) {
let backup = left
left = right
right = backup
}
// That's it. Works with literally anything.
var playerScore = 5, enemyScore = 10
exchangeValues(&playerScore, &enemyScore) // Int? Sure.
var currentUser = "Alice", previousUser = "Bob"
exchangeValues(¤tUser, &previousUser) // Strings? Yep.
var startPosition = CGPoint(x: 0, y: 0), endPosition = CGPoint(x: 100, y: 100)
exchangeValues(&startPosition, &endPosition) // Custom types? Why not.One function. Done.
I know what you’re thinking — those angle brackets <T> look weird and kinda intimidating. Trust me, give it 15 minutes and you'll wonder why you ever thought they were complicated.
📺 I’m actually putting together a video walking through generics with real app examples. Check it out on Swift Pal when it drops: _https://youtube.com/@swift-pal_
**Swift Pal** _👨💻 Welcome to Swift Pal - Your iOS Dev Companion! Whether you're just starting your journey with Swift and SwiftUI…_youtube.comhttps://youtube.com/@swift-pal
✨ Your First Generic Function
Let’s actually build one from scratch so you see how this works.
Pretend you need to print the first thing in an array. Without generics, you end up doing this:
func printFirstInt(_ array: [Int]) {
if let first = array.first {
print(first)
}
}
func printFirstString(_ array: [String]) {
if let first = array.first {
print(first)
}
}
// *sigh* not this again...With generics though:
func printFirstElement<T>(_ items: [T]) {
if let firstItem = items.first {
print(firstItem)
}
}
// Works with whatever
printFirstElement([42, 99, 7]) // Int array
printFirstElement(["apple", "banana"]) // String array
printFirstElement([true, false, true]) // Bool arraySo what’s happening here?
That <T> thing after the function name? It's telling Swift "hey, T is gonna be some type, but I don't know which yet."
When you actually call printFirst([1, 2, 3]), Swift goes "ah, T must be Int" and makes it work. When you call printFirst(["a"]), Swift's like "okay so T is String this time."
The letter T is arbitrary btw. You could write:
func printFirstElement<DataType>(_ items: [DataType]) { ... }
func printFirstElement<GenericElement>(_ items: [GenericElement]) { ... }But everyone uses T (stands for "Type"). Sometimes E for "Element." Or K and V for dictionary keys and values. Just convention stuff.
Here’s another one:
func buildArray<T>(withItem item: T, repeatedTimes count: Int) -> [T] {
return Array(repeating: item, count: count)
}
let zeroArray = buildArray(withItem: 0, repeatedTimes: 5) // [0, 0, 0, 0, 0]
let greetings = buildArray(withItem: "hey", repeatedTimes: 3) // ["hey", "hey", "hey"]See how the return type is [T]? Whatever you pass in for item, that's the type the array holds. Swift's compiler is smart enough to figure it all out.
🏗️ Generic Types (Structs, Classes, Enums)
Functions are just the start. You can make entire types generic.
Generic Struct:
struct Stack<Element> {
private var storage: [Element] = []
mutating func push(_ newElement: Element) {
storage.append(newElement)
}
mutating func pop() -> Element? {
guard !storage.isEmpty else { return nil }
return storage.removeLast()
}
func peek() -> Element? {
return storage.last
}
var count: Int {
return storage.count
}
var isEmpty: Bool {
return storage.isEmpty
}
}
// Now use it with whatever types you need
var navigationStack = Stack<String>()
navigationStack.push("HomeView")
navigationStack.push("ProfileView")
print(navigationStack.pop()) // Optional("ProfileView")
var undoStack = Stack<Int>()
undoStack.push(100)
undoStack.push(200)
print(undoStack.pop()) // Optional(200)One Stack implementation. Works with any type. Beautiful.Generic Class:
class Wrapper<T> {
var content: T
init(wrapping value: T) {
self.content = value
}
func getValue() -> T {
return content
}
func updateValue(_ newValue: T) {
content = newValue
}
}
let scoreWrapper = Wrapper(wrapping: 42)
print(scoreWrapper.getValue()) // 42
let messageWrapper = Wrapper(wrapping: "Hello")
print(messageWrapper.getValue()) // "Hello"
// Even works with optionals
let maybeWrapper = Wrapper<Int?>(wrapping: nil)
print(maybeWrapper.getValue()) // nilGeneric Enum (This one’s cool):Generic Enum (this one’s actually pretty cool):
enum Outcome<Success, Failure> {
case worked(Success)
case failed(Failure)
}
// API responses
let apiResponse: Outcome<Data, Error> = .worked(Data())
// Login validation
let loginCheck: Outcome<String, String> = .failed("Wrong password buddy")
// You can use different types for success vs failure
func calculateDivision(_ numerator: Double, dividedBy denominator: Double) -> Outcome<Double, String> {
guard denominator != 0 else {
return .failed("Cannot divide by zero, obviously")
}
return .worked(numerator / denominator)
}
let mathResult = calculateDivision(10, dividedBy: 2)
switch mathResult {
case .worked(let answer):
print("Got: \(answer)")
case .failed(let errorMsg):
print("Nope: \(errorMsg)")
}This pattern is literally how Swift’s built-in \Result\ type works. You’ve probably been using it without even thinking about the generics.
Multiple Type Parameters:
struct Tuple<Left, Right> {
let leftValue: Left
let rightValue: Right
}
let point = Tuple(leftValue: 10, rightValue: 20) // Tuple<Int, Int>
let userInfo = Tuple(leftValue: "Username", rightValue: 42) // Tuple<String, Int>
let arraysCombo = Tuple(leftValue: [1, 2], rightValue: ["x", "y"]) // Tuple<[Int], [String]>You can have as many type parameters as you need. Though honestly, if you’re using more than 3–4, your code’s probably getting too complex.You can have as many type parameters as you need. Though honestly, if you’re using more than 3–4, your code’s probably getting too complex.
🎯 Type Constraints Made Simple
Alright so here’s where generics get slightly tricky. Sometimes you need your generic type to actually DO something specific.
Like what if you wanna compare two values?
// This breaks
func findLarger<T>(_ firstValue: T, _ secondValue: T) -> T {
return firstValue > secondValue ? firstValue : secondValue // ❌ Nope! Swift doesn't know if T can use >
}Swift’s sitting there like “bro, T could be literally anything. How do I know it supports the > operator?”
And yeah — it could be an Int, a String, or some random struct you made that has no idea what comparison means.
This is where type constraints save you:
func findLarger<T: Comparable>(_ firstValue: T, _ secondValue: T) -> T {
return firstValue > secondValue ? firstValue : secondValue
}
print(findLarger(5, 10)) // 10
print(findLarger("zebra", "aardvark")) // "zebra"The : Comparable part says "T can be whatever, but it HAS to conform to Comparable." Now Swift knows > operator works because Comparable types are guaranteed to support it.
Some common ones you’ll use:
// Equatable - can use == and !=
func checkIfSame<T: Equatable>(_ itemA: T, _ itemB: T) -> Bool {
return itemA == itemB
}
// Numeric - supports math stuff
func addNumbers<T: Numeric>(_ firstNum: T, _ secondNum: T) -> T {
return firstNum + secondNum
}
print(addNumbers(5, 3)) // 8
print(addNumbers(2.5, 1.5)) // 4.0
// Collection - anything array-like
func showCount<T: Collection>(_ items: T) {
print("Total items: \(items.count)")
}
showCount([1, 2, 3]) // Arrays work
showCount("Hello") // Strings are Collections!
showCount(Set([1, 2, 3])) // Sets tooMultiple constraints:
// T needs to be BOTH Comparable AND Hashable
func checkMatch<T>(_ itemA: T, _ itemB: T) -> Bool where T: Comparable, T: Hashable {
return itemA == itemB
}
// Same thing, different syntax
func checkMatch<T: Comparable & Hashable>(_ itemA: T, _ itemB: T) -> Bool {
return itemA == itemB
}Both ways work. First one’s more readable when you have complex constraints IMO.
Constraining to a specific type:
// Only works with arrays of Equatable stuff
func removeDupes<T: Equatable>(_ list: [T]) -> [T] {
var uniqueItems: [T] = []
for item in list {
if !uniqueItems.contains(item) {
uniqueItems.append(item)
}
}
return uniqueItems
}
let numbers = [1, 2, 2, 3, 3, 3, 4]
print(removeDupes(numbers)) // [1, 2, 3, 4]
let tags = ["swift", "ios", "swift", "generics"]
print(removeDupes(tags)) // ["swift", "ios", "generics"]🔗 Protocols + Generics
This is where things get interesting. You can combine protocols with generics in powerful ways.
Associated Types in Protocols:
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(index: Int) -> Item { get }
}
// Implement it with a specific type
struct IntContainer: Container {
typealias Item = Int // Explicit, but Swift can infer this
private var items: [Int] = []
var count: Int {
return items.count
}
mutating func append(_ item: Int) {
items.append(item)
}
subscript(index: Int) -> Int {
return items[index]
}
}
// Or make it generic!
struct GenericContainer<T>: Container {
typealias Item = T
private var items: [T] = []
var count: Int {
return items.count
}
mutating func append(_ item: T) {
items.append(item)
}
subscript(index: Int) -> T {
return items[index]
}
}
var stringContainer = GenericContainer<String>()
stringContainer.append("Hello")
print(stringContainer[0]) // "Hello"Generic Functions with Protocol Constraints:
// Function that works with any Container
func printAll<C: Container>(_ container: C) where C.Item == String {
for i in 0..<container.count {
print(container[i])
}
}
var names = GenericContainer<String>()
names.append("Swift")
names.append("Pal")
printAll(names) // Works because Item is StringProtocol Extensions with Generics:
extension Array where Element: Numeric {
func sum() -> Element {
return reduce(0, +)
}
}
let numbers = [1, 2, 3, 4, 5]
print(numbers.sum()) // 15
let doubles = [1.5, 2.5, 3.0]
print(doubles.sum()) // 7.0
// Won't work on String arrays - good!
// let strings = ["a", "b"]
// strings.sum() // Error: doesn't conform to NumericThis is how you add functionality to generic types conditionally. Super powerful.
🚀 Advanced Patterns
You’ve got the basics. Here are some advanced tricks you’ll see in the wild.
Opaque Return Types (**some**)
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double { return .pi * radius * radius }
}
struct Square: Shape {
let side: Double
func area() -> Double { return side * side }
}
// Instead of this (which exposes implementation)
func makeCircle() -> Circle {
return Circle(radius: 5)
}
// Use opaque return type (hides concrete type)
func makeShape() -> some Shape {
return Circle(radius: 5)
}
// Caller knows it's a Shape, but not which kind
let shape = makeShape()
print(shape.area())The some keyword says "this returns something that conforms to Shape, but I'm not telling you what." Useful for hiding implementation details.
Generic Subscripts
struct JSON {
private var data: [String: Any]
init(_ data: [String: Any]) {
self.data = data
}
subscript<T>(key: String) -> T? {
return data[key] as? T
}
}
let json = JSON([
"name": "Alice",
"age": 30,
"isActive": true
])
let name: String? = json["name"] // "Alice"
let age: Int? = json["age"] // 30
let active: Bool? = json["isActive"] // true
// Type inference handles the castingConditional Conformance
// Make Array Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
static func == (lhs: Array, rhs: Array) -> Bool {
guard lhs.count == rhs.count else { return false }
for i in 0..<lhs.count {
if lhs[i] != rhs[i] { return false }
}
return true
}
}
// Now this works
let arr1 = [1, 2, 3]
let arr2 = [1, 2, 3]
print(arr1 == arr2) // true
// But this won't compile (good!)
struct NonEquatable {}
// let arr3 = [NonEquatable()]
// arr3 == arr3 // Error: can't compareType Erasure (Advanced, but important)
Sometimes you need to hide generic type information. That’s type erasure:
// Problem: Can't have arrays of different generic types
protocol Animal {
associatedtype Food
func eat(_ food: Food)
}
// Can't do this:
// var animals: [Animal] = [] // Error: protocol has associated type
// Solution: Type eraser
struct AnyAnimal<Food> {
private let _eat: (Food) -> Void
init<A: Animal>(_ animal: A) where A.Food == Food {
_eat = animal.eat
}
func eat(_ food: Food) {
_eat(food)
}
}
// Now you can have arrays
var animals: [AnyAnimal<String>] = []This is how Swift’s AnySequence, AnyPublisher, etc. work. It's a workaround for protocol limitations.
🎬 Wrapping This Up
If you made it this far, congrats — you now know more about Swift generics than most developers.
Here’s what we covered:
- Generic functions and types let you write code once for many types
- Type constraints (
<T: Comparable>) let you require specific capabilities - Protocols with associated types combine protocols and generics
- Real-world uses: networking, caching, validation, data sources
- Don’t overuse generics — sometimes concrete types are better
- Advanced patterns: opaque types, conditional conformance, type erasure
The key insight? Generics aren’t about making your code clever. They’re about eliminating duplication while maintaining type safety. That’s it.
Start simple. Write a generic function that replaces some duplicate code in your current project. Then maybe a generic struct. Build from there.
And remember — every time you’re about to copy-paste a function and just change the type? Stop. Think. “Could this be generic?”
Usually, the answer is yes.
📺 Want to see these patterns in a real iOS app? I’m building a complete project using generics throughout — network layer, storage, UI components, the works. Subscribe to Swift Pal at https://youtube.com/@swift-pal to catch it!
Now go delete some duplicate code. Your future self will thank you. 🚀
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read