Skip to main content

Swift Generics Explained – A Practical Guide


If you've written Swift for even a short time, you've already used Generics—whether you realized it or not. Types like Array, Dictionary, Set, and even Optional are all built using generics.

Generics are one of Swift's most powerful language features because they help us write reusable, type-safe, and maintainable code.

Let's understand them step by step.


Why Do We Need Generics?

Imagine writing separate functions for every data type.

func printInt(_ value: Int) { ... }
func printString(_ value: String) { ... }
func printDouble(_ value: Double) { ... }

The logic is identical, but we're repeating code simply because the data types are different.

One option is to use Any.

func printValue(_ value: Any) {
    print(value)
}

While this works, it comes with a drawback—you lose compile-time type safety and often need runtime type casting (as? or as!).

Generics solve this problem by allowing us to write code once and reuse it with different data types without sacrificing type safety.

Advantages of Generics over Any:

  • Strong compile-time type safety

  • No unnecessary runtime downcasting

  • Cleaner and more reusable code

  • Better compiler optimizations


What Are Generics?

Generics are placeholders for types that allow us to write flexible code that works with multiple data types.

The placeholder type is called a Type Parameter.

For example:

func printMe<T>(a: T) {
    print(a)
}

Here, T is the type parameter.

Swift automatically determines the actual type when the function is called.

printMe(a: 1)
printMe(a: "iPhone")

Output:

1
iPhone

Generic Functions

Generic functions let us write one implementation that works for many different types.

Instead of creating multiple versions of the same function, we simply introduce a type parameter.

func printMe<T>(a: T) {
    print(a)
}

The compiler replaces T with the appropriate type during compilation.


Generic Types

Generics are not limited to functions.

Classes, structures, and enums can also be generic.

A common example is a Stack.

struct Stack<Element> {

    var items = [Element]()

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element {
        items.removeLast()
    }
}

Using the stack:

var stackOfStrings = Stack<String>()

stackOfStrings.push("uno")
stackOfStrings.push("dos")

Here, Element is the type parameter for the entire structure.


Understanding Type Parameters

A Type Parameter is simply a placeholder for a real type.

In our examples:

  • T

  • Element

are both type parameters.

Swift's standard library uses descriptive names for type parameters.

Examples:

Array<Element>

Dictionary<Key, Value>

Set<Element>

Optional<Wrapped>

Notice how these names clearly describe what each type represents.


Collections Are Generic

One interesting fact is that Swift collections are themselves generic implementations.

Examples include:

Array<Element>

Dictionary<Key, Value>

Set<Element>

This means the same implementation works for:

Array<Int>

Array<String>

Array<User>

without duplicating code.


Generic Functions Inside Generic Types

When writing a generic function inside a generic type, you don't need to declare the generic parameter again.

For example:

extension Stack {

    var topItem: Element? {
        items.isEmpty ? nil : items[items.count - 1]
    }
}

Notice that Element is already available because Stack itself is generic.


Multiple Type Parameters

A generic type can have multiple type parameters.

class AnotherGenericClass<TypeOne, TypeTwo, TypeThree> {

}

Swift places no restriction on how many type parameters you can define.


Extending Generic Types

Extending a generic type works just like extending any normal type.

There is no need to specify the generic parameter again.

extension Stack {

    var topItem: Element? {
        items.isEmpty ? nil : items.last
    }
}

Since Stack already defines Element, the extension automatically has access to it.


Type Constraints

Sometimes we don't want a generic type to accept every possible type.

Instead, we want to restrict it to types that conform to a protocol or inherit from a specific class.

This is called a Type Constraint.

For example:

class MyGenericClass<Type: Equatable> {

}

Now only Equatable types can be used.

Swift uses this concept extensively.

For example, the Dictionary type requires its key to conform to Hashable.

Dictionary<Key: Hashable, Value>

This allows Swift to efficiently find values using keys.


Why This Generic Function Doesn't Compile

Consider the following function:

func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {

    for (index, value) in array.enumerated() {

        if value == valueToFind {
            return index
        }
    }

    return nil
}

This won't compile.

Why?

Because Swift doesn't know whether T supports the == operator.

The solution is to add a type constraint.

func findIndex<T: Equatable>(
    of valueToFind: T,
    in array: [T]
) -> Int? {

    ...
}

Now Swift knows every possible T conforms to Equatable, making equality comparison valid.


Generic Where Clause

Sometimes a single constraint isn't enough.

Swift allows multiple constraints using a where clause.

func doSomething<T>(
    first: T,
    second: T
)
where T: Comparable, T: Hashable {

    guard first.hashValue == second.hashValue else {
        return
    }

    if first == second {
        print("\(first) and \(second) are equal.")
    }
}

The same constraints can also be written before Swift 5's preferred syntax.

func doSomething<T where T: Comparable, T: Hashable>(
    first: T,
    second: T
) {
    ...
}

Generic Subscripts

Just like functions and types, Swift also supports Generic Subscripts.

They allow subscripts to work with generic types while maintaining compile-time type safety.

Although less commonly used, they follow the same generic principles discussed so far.


Associated Types

Associated types are closely related to generics and are commonly used inside protocols.

Apple defines an associated type as:

"An associated type gives a placeholder name to a type that's used as part of the protocol."

Example:

protocol Container {

    associatedtype Item

    mutating func append(_ item: Item)

    var count: Int { get }

    subscript(i: Int) -> Item { get }
}

Every type conforming to Container decides what Item should be.

The protocol simply defines the placeholder.

You can also add constraints.

associatedtype Item: Equatable

Now every conforming type must use an Equatable item.


Swift Generics Are Invariant

One advanced concept worth knowing is that Swift generics are invariant.

This means that even if one type inherits from another, their generic wrappers are considered completely different types.

Example:

struct Container<T> {}

var swiftCourse: Container<SwiftOnTheServer>
    = Container<SwiftOnTheServer>()

var onlineCourse: Container<OnlineCourse> = swiftCourse

This produces a compiler error because:

Container<SwiftOnTheServer>

is not a subtype of

Container<OnlineCourse>

even if SwiftOnTheServer inherits from OnlineCourse.


Key Takeaways

  • Generics let you write reusable code that works with multiple types.

  • They provide compile-time type safety unlike Any.

  • Generic functions, classes, structures, and enums all use type parameters.

  • Swift collections such as Array, Dictionary, Set, and Optional are built using generics.

  • Type constraints help limit generic types to protocols such as Equatable or Hashable.

  • Associated types bring generic behavior to protocols.

  • Swift generics are invariant, meaning generic types don't automatically inherit subtype relationships.

Mastering generics is an important step toward writing clean, reusable Swift code. You'll encounter them everywhere—from the Swift Standard Library to modern frameworks, Swift Packages, and Clean Architecture implementations.

Popular posts from this blog

Animating label text update - choosing a better way

Recently I published a countdown app .  At one point of development - I have to show a timer on a UILabel which ticks on each seconds. As usual I started  setting text to a label object - self .timerLabel.text = someString Easy piece of cake right !?   But wait ... it won't take much user attention when timer ticks on every seconds. So I decided to make use of a simple animation while label gets text update. I found there are dozens of ways to animate a label. In this short article, I listed 3 best way you can animate text on a label. ( Spoiler Alert 👀- I decided to go with 3rd option)  1. Fade In - Fade out animation : CATransition class has got transition type `fade`. With timing function of CATransition - I was able to see the below result. let animation: CATransition = CATransition () animation.timingFunction = CAMediaTimingFunction (name: CAMediaTimingFunctionName .easeInEaseOut) animation.type = CATransitionType .fade animation.subtype = C...

Cached Async Image in SwiftUI

 SwiftUI’s AsyncImage is handy, but every time your view appears, it refetches the image—leading to flicker, delays, and unnecessary network use. What if you could fetch once, then reuse instantly? That's exactly what the Cached Async Image  delivers: a memory-powered caching layer that keeps SwiftUI image loading smooth, snappy, and resilient. First a simple in-memory cache without disk persistence. This will be thread-safe and auto-purges under memory pressure. A Singleton wrapping NSCache for URL → UIImage caching as follows : final class ImageCache {   static let shared = ImageCache()   private init() {}   private let cache = NSCache<NSURL, UIImage>()   func image(for url: URL) -> UIImage? {     cache.object(forKey: url as NSURL)   }   func insertImage(_ image: UIImage?, for url: URL) {     guard let image else { return }     cache.setObject(image, forKey: url as NSURL)   }   func clearAll() { ...

Implementing autocompletion OTP field in iOS

Long waiting is over. !!  iOS 12 brings Autofill for OTP text field which is close to Android provided a decade back. Previously in iOS we used to toggle between OTP text screen and message inbox.  Which was hard to remember and time consuming resulting a bad user experience. Personally, I have been asked from the client/customer couple of times to implement autocompletion for OTP field and took me a lot of time to convey that it is not possible in iOS. Why Autofill was not possible previously?  We all know that Apple gives at most care for user privacy. When we see iOS architecture, each individual app is like a separate island. There is no inter-app bridge between apps (exception for Keychain and URLSchemes APIs which gives very limited scope). Thus we cannot read message content from inbox. Where to start Autofilling? First of all, the target SMS need to have the OTP Code with prefix string "Code" or "Passcode"on its message content. Beware of OTP c...

Prevent Navigationbar or Tabbar overlapping Subview - solved for Card view

Recently, I started with a Card view added as a subview of UIView in a view-controller. When a view controller created along subviews, it tends to use entire screen bounds and also slips behind Tab bar or Navigation bar. In my current situation, it's second case. Casually new iOS developers will write a patch by additional value for coordinate y and subtracting bar height from its size. A lot of them posted in SO threads too : How to prevent UINavigationBar from covering top of view? View got hidden below UINavigationBar iOS 7 Navigation Bar covers some part of view at Top So, how I got solved ? self.edgesForExtendedLayout = [] This  will avoid all subviews in a view controller get behind any bars. Read full apple  documentation on here. Full Source code below :  //Simple view controller where its view layed-out as a card. class WidgetCardViewController : UIViewController { var containerView = UIView () //MARK:- View Controller Life Cyc...

UICollectionViewCell shows with wrong size on First time - Solved

We commonly use Collection view where its cell size calculated run time. The flow layout delegate is responsible to return individual cell sizes. BUT in most of the cases, delegate method `collectionView: layout sizeForItem:` expects cell size too early. Before generating actual cell size. extension YourViewController : UICollectionViewDelegateFlowLayout { func collectionView ( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize (width: externalWidth, height: externalHeight) } } For instance, if a cell size depends on external view and its frame is not yet ready - results with wrong (or outdated) cell size. Typically happens for the first time view controller laid out all views. You can find similar queries in StackOverflow community : Collection view sizeForItemNotWorking UICollectionViewCell content wrong size on first load How to refresh UICollec...