All articles
iOS6 min read

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

K
Karan Pal
Author

Swift Essentials

Image Generated by AI
Image Generated by AI

🔄 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(&currentUser, &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 array

So 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 too

Multiple 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 String

Protocol 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 Numeric

This 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 casting

Conditional 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 compare

Type 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:

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! 🚀

● The newsletter

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