Leveraging the Power of Generic Protocols in Swift
In an era where drugs and vaccines are of paramount importance maybe, we should start thinking about generics. 💉
A while back, I wrote about how to avoid property/method naming conflicts by using Namespaces in the article down below. I mentioned that the implementation could be further improved by using Generics which we’ll tackle in this article.
What are Generics? 🤔
First, let’s talk about Generics. What the hell are generics? According to Swift’s Documentation:
Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.
We can see Generics being used under the hood all around the Swift Language, you only have to be on the lookout of these symbols <
and >
. No, I don’t mean greater than and less than symbols, you will notice them after a class’ name declaration like this class Foo<Bar>
. As an example, we have the Swift Array declared like so:
@frozen struct Array<Element>
Normally you don’t notice this because Arrays are usually declared with a shorthand syntax, example:
let array: [Int] = [1, 2, 3, 4, 5]
But actually this data type is being recognized by the compiler as:
let array: Array<Int> = [1, 2, 3, 4, 5]
There are other data types with this format as well such as:
- Optional<Wrapped> — which can be used with any nullable declarations such as
Int?
,String?
, etc. - Dictionary<Key, Value> — which translates to any dictionary such as
[String: Any]
,[String: String]
, etc. - And many more
Creating Generic Types 🛠
Let’s dive deeper into creating our own generic types. Here’s a simple example of a generic container class:
class Container<T> {
private var items: [T] = []
func add(_ item: T) {
items.append(item)
}
func get(at index: Int) -> T? {
guard index < items.count else { return nil }
return items[index]
}
}
Now we can use this container with any type:
let numberContainer = Container<Int>()
numberContainer.add(42)
let stringContainer = Container<String>()
stringContainer.add("Hello Generics!")
Generic Protocols with Associated Types 💪
This is where things get interesting! We can create protocols with associated types to define generic requirements:
protocol DataStore {
associatedtype DataType
var items: [DataType] { get set }
func add(_ item: DataType)
func get(at index: Int) -> DataType?
}
Now we can implement this protocol with different types:
class StringStore: DataStore {
typealias DataType = String
var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func get(at index: Int) -> String? {
guard index < items.count else { return nil }
return items[index]
}
}
Combining Protocols, Associated Types, and Generics 🔄
Swift lets us combine protocols with both associated types and generic parameters. Here’s a practical example:
// Protocol with an associated type
protocol DataProcessor {
associatedtype Input
func process(_ input: Input) -> String
}
// Protocol with a generic type parameter
protocol Transformer<T> {
func transform(_ input: T) -> T
}
// Class combining both approaches
class AdvancedProcessor<T>: DataProcessor, Transformer<T> {
typealias Input = T
func process(_ input: T) -> String {
let transformed = transform(input)
return String(describing: transformed)
}
func transform(_ input: T) -> T {
return input
}
}
// Example implementation
class StringProcessor: AdvancedProcessor<String> {
override func transform(_ input: String) -> String {
return input.uppercased()
}
}
// Usage
let processor = StringProcessor()
print(processor.process("hello")) // Outputs: "HELLO"
This pattern is particularly useful for building flexible components that need both protocol conformance and generic type capabilities, like networking layers or data transformation systems.
Combining Generics with Namespaces 🚀
Remember our namespace implementation from the previous article? Let’s enhance it with generics:
struct Namespace<Base> {
let base: Base
init(_ base: Base) {
self.base = base
}
}
protocol NamespaceCompatible {
associatedtype CompatibleType
static var ns: Namespace<CompatibleType>.Type { get }
var ns: Namespace<CompatibleType> { get }
}
extension NamespaceCompatible {
static var ns: Namespace<Self>.Type {
return Namespace<Self>.self
}
var ns: Namespace<Self> {
return Namespace(self)
}
}
Now we can create type-safe extensions:
extension UIImageView: NamespaceCompatible { }
extension Namespace where Base: UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
// Implementation using your preferred networking library
}
}
Usage becomes clean and type-safe:
let imageView = UIImageView()
imageView.ns.setImage(from: imageURL, placeholder: placeholderImage)
Type Constraints and Where Clauses ⛓
Sometimes we need to restrict what types can be used with our generic code. We can do this using type constraints:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
return array.firstIndex(of: valueToFind)
}
We can also use where clauses for more complex constraints:
extension Container where T: Comparable {
func sorted() -> [T] {
return items.sorted()
}
}
Best Practices 🎯
- Keep It Simple: Don’t make things generic just because you can. Use generics when you need to reuse code across different types.
- Use Meaningful Names: Instead of
T
, use names that describe the purpose likeElement
,Key
,Value
. - Consider Type Constraints: Add constraints when you need specific functionality from the generic types.
- Document Your Generic Code: Generic code can be confusing — add clear documentation explaining the purpose and requirements.
Conclusion 🎉
Generics are a powerful feature in Swift that allows us to write flexible, reusable code while maintaining type safety. By combining generics with protocols and namespaces, we can create elegant, type-safe APIs that are a joy to use.
Remember, with great power comes great responsibility — use generics judiciously where they add value and improve code reuse, but don’t over-complicate your code just for the sake of making it generic.
If you’re reading this, then you’ve reached the end. Cheers! 🍻
If you have any comments, suggestions, questions, or ideas for new blog posts leave a comment down below. 👇👇
Thank you for taking the time on reading my article. 🎉