Senior iOS Softwareentwickler

Köln, Deutschland

A Clear Path to Swift Types

Sprache: Dieser Artikel ist auf Englisch verfasst.

In Swift, the type system is powerful, expressive, and strict. Choosing the right type is critical for code clarity, performance, safety, and maintainability. Swift offers a range of type categories, from simple scalar values to reference-based types with complex behavior. This post gives you a concise strategy for selecting the right type and explains the reasoning behind it.

TL;DR

When choosing Swift types, start simple and move up the complexity ladder only when needed: scalar types like Int and String for basic values, tuples for temporary groupings, enums for state modeling, and structs for most data types. Prefer struct over class because structs offer value semantics, static dispatch, stack allocation, and better concurrency safety without ARC overhead. Only reach for class or actor when you truly need reference semantics, inheritance, or shared mutable state—and consider final class if you want reference semantics without the performance cost of dynamic dispatch.

Type Hierarchy: From Simple to Complex

When defining your own types, prefer the simplest form that fulfills your needs. This is the recommended order:

1. Scalar Types

Use built-in types like Int, String, Bool, or Double if they are sufficient. They are lightweight and highly optimized.

2. Tuple

Use for grouping a few values locally. Tuples are anonymous, lack method support, and are best suited for temporary, local constructs. The empty tuple () is equivalent to Void.

3. Enum

Use when a value can be in one of a fixed set of states or variants. Enums support methods and properties and act as namespaces. A special case: an enum with no cases is uninstantiable and corresponds to the Never type.

4. Struct

Use for structured, named data types with multiple values. Structs offer value semantics, methods, computed properties, and initializers. They are ideal for modeling immutable or copyable data.

5. Class or Actor

Only use when reference semantics, shared mutable state, or class-specific features (like inheritance or identity) are truly required. Actors should be used when data needs to be protected from concurrent access.

Prefer struct over class

When deciding between struct and class, prefer struct unless a reference type is clearly necessary.

Reasons to prefer struct:

  • Value semantics: predictable behavior, safe by default
  • No ARC overhead: avoids retain/release tracking
  • Stack allocation: struct instances are often stored on the stack (or optimized as such), which is fast and automatically managed. In contrast, class instances are always heap-allocated, which involves dynamic memory management and reference counting overhead.
  • Clear ownership: no aliasing or shared references
  • Static method dispatch: direct function calls without vtable lookup overhead
  • Better compiler optimizations
  • Sendable by default: easier to use with concurrency

Important: class types are not Sendable by default. This means instances cannot be safely transferred between concurrent tasks unless you explicitly ensure thread safety and mark them Sendable. Doing so often requires low-level synchronization, which adds complexity and risk.

Understanding Method Dispatch in Swift

In Swift, how a method call is executed—known as method dispatch—depends on the type involved. This affects performance, optimization, and behavior at runtime. Let’s look at how dispatch works differently for struct, class, and final class.

struct: Always Static Dispatch

Methods on a struct are dispatched statically. That means the compiler knows at compile time exactly which function will be called. The method call is resolved directly, without any indirection. This enables aggressive compiler optimizations like inlining and dead code elimination. Because structs don’t support inheritance or method overriding, there’s no ambiguity, and dispatch is fast and predictable.

class: Dynamic Dispatch by Default

In contrast, methods on a regular class are dispatched dynamically using a virtual method table (vtable). At runtime, Swift must look up the actual implementation of a method because the class could be subclassed, and the method could be overridden. This indirection introduces runtime overhead and limits optimization opportunities, since the compiler cannot always determine the concrete implementation in advance.

final class: Static Dispatch with Reference Semantics

A final class is a class that cannot be subclassed. Since Swift knows the class hierarchy is closed, it can statically resolve method calls just like it does with structs. This allows the compiler to optimize method calls with the same performance benefits as structs—direct calls, no runtime lookup, and better inlining. You retain reference semantics but avoid the cost of dynamic dispatch.

Named Types vs Compound Types in Swift

Swift categorizes types into two main kinds: named types and compound types.

A compound type is a type without a name, defined in the Swift language itself. There are two compound types: function types and tuple types. A compound type may contain named types and other compound types. For example, the tuple type (Int, (Int, Int)) contains two elements: The first is the named type Int, and the second is another compound type (Int, Int).

Swift Documentation: Types

enum, struct, class, and actor are named types (also called nominal types).

In Swift, a type is considered a nominal type if it has been explicitly named by a declaration somewhere in code. Examples of nominal types include classes, structures and enumerations. Nominal types are an important concept in Swift because they may conform to protocols, be extended, and have values created using the initializer syntax MyType().

Swift Documentation: Nominal Types

This makes them the foundation for building well-structured, reusable components in Swift. By contrast, types like tuples or function types are structural – they are defined purely by their layout, not by name.

Note: While tuples are structural by default, you can make them reusable by defining a typealias.

Summary

Start simple. Use scalar types or tuples if they work for your needs. Reach for enum to model state, and prefer struct for most data models. Only introduce class or actor when there’s a concrete need for reference semantics or concurrency isolation.

This approach leads to code that is safer, faster, and easier to reason about—especially in concurrent environments.