Along with the introduction of Swift 5.7, we have been frequently witnessing the wide usage of some
and any
keywords in Swift code, haven't we? This is because both keywords play a major role in the Swift ecosystem helping us diminish the difficulties of dealing with protocols.
In this article, you will learn some
and any
keywords along with sample code, understand the difference between them and have a bigger picture of how to use them in the right places. So let's dive into it.
Understanding some
keyword
With the release of Swift 5.1, the some
keyword was introduced in the Swift ecosystem. It can be put in front of the type at the type declaration to convert a particular type to an opaque type without losing the originality of the type. In other words, it only belongs to a specific type and does not allow any changes of the underlying concrete type from one to another forcing it to be consistent throughout the lifetime of variables. To get a bigger picture, let's take a look at the following code snippet:
protocol Animal {
var name: String { get set }
func makeSound()
}
struct Dog: Animal {
var name: String = "Dog"
func makeSound() {
print("Bark!")
}
}
var animal: some Animal = Dog() // Create an opaque type of `Animal` with the underlying type of `Dog`
You will see Animal
protocol and Dog
struct conforming to Animal
and there is also the declaration of an animal variable using some
keyword. Here it creates an opaque type of Animal
revealing the concrete type of Dog
to the compiler so that the compiler won't allow type changes dynamically and it will yell the error when we attempt it.
struct Cat: Animal {
var name: String = "Cat"
func makeSound() {
print("Meow!")
}
}
animal = Cat() // Cannot assign value of type 'Cat' to type 'some Animal'
animal = Dog() // Cannot assign value of type 'Cat' to type 'some Animal'
If we compile the above code, we will see the error pop-ups and also notice that it prevents us to assign a new instance to the variable because the opaque type only allows a specific concrete type throughout the lifetime of a particular variable. If you are familiar with SwiftUI, you will notice that we have to return a specific type of view in the view block.
func createHeader() -> some View { // Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types
if isCompact {
return VStack { // return VStack
Text("Hi ๐,")
Text("Welcome to my blog")
}
} else {
return Text("Hi ๐, Welcome to my blog") // return Text
}
}
The above code will not compile because the method tries to return different types - VStack
and Text
while it only accepts a specific type of view. At the moment, you may get the idea of what some
keyword is. So let's move to any
keyword.
Understanding any
keyword
The any
keyword was introduced in the Swift 5.7 release to provide more flexibility when dealing with protocols. Although the intention of the any
keyword is the same with the some
keyword, the way they work is quite different from each other. As it is named any
, the any
keyword serves as a type-eraser just like Any
and AnyObject
. Technically speaking, it defines the existential type which belongs to any kind of type meeting a specific protocol. Let's jump into code with our beloved animals:
var animal: any Animal = Cat()
animal = Dog() // Now the `animal` variable will hold the instance of `Dog` as `any Animal`
In the above code, we can see that with any
keyword, the variable can hold any instances of types that conform to a specific protocol. That's why the compiler allows us to assign the variable with different values as it already erases the concrete type.
Comparison between some
and any
keywords
As you understand how the magic of some
and any
keywords work, let's make them compare to highlight the rules of both keywords.
As we all know that the some
keyword only holds a specific type conforming to a particular protocol while the any
keyword accepts various types that meet a particular protocol. We can illustrate this using the array of an opaque type - some
and an existential type - any
so that we will see that the compiler prompts the error when we attempt to use various types in the array of an opaque type but it will be compiled properly with the array of an existential type.
// The array of any Animal containing different types of animals
var animals: [any Animal] = [Dog(), Cat(), Dog(name: "Husky")] // Compile properly
// The array of some Animal with a specific type of animal
var animals: [some Animal] = [Dog(), Dog()] // Compile properly
// This won't allow to put different types of animals in the array
var animals: [some Animal] = [Cat(), Dog()] // Type of expression is ambiguous without more context
Another interesting thing is that we cannot append the array of an opaque type with the new element regardless of the type while the existential array can achieve it.
var animals: [any Animal] = [Dog(), Cat(), Dog(name: "Husky")]
animals.append(Cat()) // Compile properly
var animals: [some Animal] = [Dog(), Dog()]
animals.append(Dog()) // Fail to compile
animals.append(Cat()) // Fail to compile
Another thing to highlight in this section is that the return type with some
and any
keyword. As I mention in the earlier section, we suffer a lot when we try to return different types of views based on a specific condition in SwiftUI because of an opaque keyword. So, we have to return a specific type by force before the arrival of @ViewBuilder
attribute (we won't cover it here as it is a different topic). On the other hand, we can return whatever we want using any
keyword.
// Return any Animal (Dog or Cat) based on a condition
func getAnimal(_ isLoyalPet: Bool) -> any Animal { // Compile properly
isLoyalPet ? Dog() : Cat()
}
// This won't allow to return one of different types
func getAnimal(_ isLoyalPet: Bool) -> some Animal { // Fail to compile as the return types mismatch each other
isLoyalPet ? Dog() : Cat()
}
// It only allows us to return the same type
func getDog(_ isLoyalPet: Bool) -> some Animal {
isLoyalPet ? Dog(name: "German Shepherd") : Dog()
}
The above code shows the limitation of using the some
keyword which only allows the same type as a return type.
Utilization of some
and any
keywords
Along with the Swift evolution, these two keywords are extremely powerful and widely adapted in the Swift ecosystem. The usage of the some
keyword grows as the introduction of SwiftUI to detach the concrete type of the associated type of View
protocol so that we don't need to specify the concrete type.
struct HeaderView: View {
var body: some View { // Return an opaque type of view instead of Text
Text("Hi ๐, Welcome to my blog")
}
}
Besides, we can see these keywords in the replacement of the generic method instead of using the traditional generic approach. I also feel that it improves the readability of code because of its simple syntax.
// Using the traditional generic type
func makeAnimalSing<T: Animal>(_ animal: T) {
animal.makeSound()
}
// Using the opaque type
func makeAnimalSing(_ animal: some Animal) {
animal.makeSound()
}
// Using the existential type
func makeAnimalSing(_ animal: any Animal) {
animal.makeSound()
}
All of those methods will compile properly and works functionally but the second and third methods look more readable. If we apply these keywords in the parameter, it is not obvious to see the limitation. In case these are applied to the return type, we need to consider carefully which one is the best fit for our requirements.
Another advantage is that as these keywords help us to conceal the concrete type, they are super powerful to pass dependencies through API interfaces without revealing the concrete type which is not necessary for API. Let's take a look at the snippet below:
protocol NetworkWorker {
func fetch()
}
struct UatNetworkWorker: NetworkWorker { // network worker for uat target
func fetch() {
print("Fetching data on UAT server")
}
}
struct ProdNetworkWorker: NetworkWorker { // network worker for production target
func fetch() {
print("Fetching data on PROD server")
}
}
class APIService {
let networkWorker: any NetworkWorker
init(_ networkWorker: some NetworkWorker) {
self.networkWorker = networkWorker
}
func load() {
networkWorker.fetch()
}
}
In the above code, we have APIService
depending on the instance of NetworkWorker
to perform networking based on the target. That's why we only inject the blueprint by hiding the concrete type and APISerive
have no idea of the actual type of network worker.
let networkWorker: any NetworkWorker
#if UAT
networkWorker = UatNetworkWorker()
#else
networkWorker = ProdNetworkWorker()
#endif
let apiService = APIService(networkWorker)
apiService.load()
So we can manipulate the network worker based on the target and inject it accordingly because of the power of some
and any
keywords.
Drawback of some
and any
keywords
As these keywords hide the concrete type of the instances, sometimes it struggles to know its concrete type in case we want to access it. If we want to reveal it, we have to manually typecast the instance and handle the error unless the types match each other.
Conclusion
To conclude, the some
and any
keywords have become powerful and magical keywords in the Swift ecosystem and it is critical to understand and master these two keywords by applying them in our code on our daily basis. The more we applied new techniques we learned, the stronger our experience and competence in our domain. So let's apply them in code from today.