The some keyword in Swift declares opaque types, and Swift 5.1 introduced it with support for opaque result types. Many engineers experience working with opaque types for the first time when writing a body of a SwiftUI view. Though, it’s often unclear what some keyword does and when to use them in other places.
With the introduction of Opaque Parameter Declarations in SE-0341, there are many more places where you can start adopting the some keyword. In this article, I’ll explain what opaque types are and when you should use them.
What are opaque types?
Opaque types allow you to describe the expected return type without defining a concrete type. A common place where we use opaque types today is inside the body of a SwiftUI view:
var body: some View { ... }
At first, it looks like we’re returning a protocol type. Though, the some keyword is crucial here as it allows the compiler to access the actual type information and perform optimizations. For example, the compiler can see the following return types:
var body: VStack { ... }
// or:
var body: Text { ... }
The entire view hierarchy is opaque, allowing the compiler to know the exact size of the returned view body. The compiler can optimize the code, and fewer heap allocations are needed. Without diving into details, you could say we’re giving the compiler more information than just stating we’re expecting a View
protocol to be returned by appending the some keyword.
Opaque return type without matching underlying types
When writing SwiftUI views, you might run into the following error:
Function declares an opaque return type ‘some View’, but the return statements in its body do not have matching underlying types
An example of code triggering the error looks as follows:
func makeFooterView(isPro: Bool) -> some View {
if isPro {
return Text("Hi there, PRO!") // Return type is Text
} else {
return VStack { // Return type is VStack<TupleView<(Text, Button<Text>)>>
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
As you can see, we’re returning two types of views: a VStack
when the isPro
boolean returns true, otherwise a Text
view.
As explained before, the compiler wants to know the underlying concrete type through the some keyword. Opaque types need to be fixed for the scope of the value, so we can’t return different types within the same method scope.
We could solve the above code by using a wrapping container, like a VStack
:
func makeFooterView(isPro: Bool) -> some View {
return VStack {
if isPro {
Text("Hi there, PRO!")
} else {
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
However, we’re now adding an extra container which would only be needed in case the isPro
returns true. Therefore, it’s better to rewrite the above code using the @ViewBuilder
attribute, allowing you to use result builders instead:
@ViewBuilder
func makeFooterView(isPro: Bool) -> some View {
if isPro {
Text("Hi there, PRO!")
} else {
VStack {
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
Using opaque types to hide type information
The some keyword allows describing the return value by providing the protocol it supports, allowing to hide the concrete type accordingly. When developing modules, you can use opaque types to hide concrete types that you don’t want to expose to implementors.
For example, if you provide a package for fetching images, you might define an image fetcher:
struct RemoteImageFetcher {
// ...
}
You could provide the image fetcher through an image fetcher factory:
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> RemoteImageFetcher
}
The API is defined within a Swift package module and requires the public keyword to expose APIs to implementors. Since we defined a concrete RemoteImageFetcher
return type, the compiler now requires us to convert the remote image fetcher into publicly accessible code:
We can solve this problem by defining a public ImageFetching
protocol and use that as a return type:
public protocol ImageFetching {
func fetchImage() -> UIImage
}
struct RemoteImageFetcher: ImageFetching {
func fetchImage() -> UIImage {
// ...
}
}
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> ImageFetching {
// ...
}
}
Returning a protocol without associated types works fine without defining opaque types, but as soon as we define an associated type Image
:
public protocol ImageFetching {
associatedtype Image
func fetchImage() -> Image
}
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> ImageFetching {
// ...
}
}
We’ll run into the following error:
Protocol ‘ImageFetching’ can only be used as a generic constraint because it has Self or associated type requirements
Solving errors like these requires defining opaque types. Let’s dive in!
Solving Protocol can only be used as a generic constraint errors
When working with protocols and associated types in Swift, it’s common to run into the following error:
Protocol ‘X’ can only be used as a generic constraint because it has Self or associated type requirements
The protocol requires you to provide information about the generic constraints; the compiler can’t resolve these without extra details. We allow the compiler to read out that additional information by using the some keyword:
public func imageFetcher(for url: URL) -> some ImageFetching { ... }
While we will enable the compiler to read out all necessary information, we can still hide implementation details like the concrete RemoteImageFetcher
type used underneath.
Using an opaque return type solves generics constraints in the above example, but we could also run into the same error when using the protocol as a function parameter:
public extension UIImageView {
// Protocol 'ImageFetching' can only be used as a generic constraint because it has Self or associated type requirements
func configureImage(with imageFetcher: ImageFetching) {
// Member 'fetchImage' cannot be used on value of protocol type 'ImageFetching'; use a generic constraint instead
image = imageFetcher.fetchImage()
}
}
The above error throws in Xcode 13 only. Xcode 14 comes with Swift 5.7 and several improvements regarding opaque and existential types. SE-0341 Opaque Parameter Declarations is one of the implemented proposals and allows to use opaque types in parameter declarations. Though, the compiler will tell you:
Use of protocol ‘ImageFetching’ as a type must be written ‘any ImageFetching’
I’ll explain existential any in another article later on, but for now, the only thing you need to know is that you could use either any
or some
. In other words, we could change our method as follows:
func configureImage(with imageFetcher: some ImageFetching)
Using Primary Associated Types and constraints using some
While this solves the compiler error related to our method definition, we will still run into the following error regarding image fetching:
public extension UIImageView {
func configureImage(with imageFetcher: some ImageFetching) {
// Cannot assign value of type '<anonymous>.Image' to type 'UIImage'
image = imageFetcher.fetchImage()
}
}
The compiler will recommend you to force unwrap the value to a UIImage
but we don’t want to risk runtime exceptions. Therefore, we’ll have a look at another Swift 5.7 feature implemented with SE-358 Primary Associated Types in the Standard Library, allowing us to configure the primary associated type for our image fetcher:
public protocol ImageFetching<Image> {
associatedtype Image
func fetchImage() -> Image
}
By matching the associated type Image
inside the protocol name declaration, we configure the primary associated type for our image fetching protocol. Swift 5.7 also comes with proposal SE-0346 Lightweight same-type requirements for primary associated types allowing us to now update our extension method to be constraint to UIImage
types only:
public extension UIImageView {
func configureImage(with imageFetcher: some ImageFetching<UIImage>) {
image = imageFetcher.fetchImage()
}
}
All compiler errors are solved, and we’ve configured our method so that the compiler knows we’re dealing with some image fetcher returning a UIImage. The UIImageView expects the same type for its image property, and we can configure the fetched image accordingly.
By using opaque types in the above examples, we’ve removed the need to expose code publically, allowing us to refactor code internally without publishing breaking changes. Gaining this flexibility is crucial during normal development with internal APIs and when providing frameworks.
Replacing generics with some
The some keyword can also be used as syntactic sugar to replace generics and improve readability. If a generic parameter is only used in a single place, we can replace it with opaque types.
For example, we could have defined a custom print method:
func printElement<T: CustomStringConvertible>(_ element: T) {
print(element)
}
The generic parameter is only used in a single place, so we can replace it using the some keyword accordingly:
func printElement(_ element: some CustomStringConvertible) {
print(element.description)
}
In other words, you can use opaque type some Protocol
as a shorthand of T where T: Protocol
and improve the readability of your code.
Conclusion
Opaque types in Swift help you to simplify your code and improve readability. Swift 5.7 introduced many improvements making it possible to benefit from the some keyword in more places. Using primary associated types and opaque type constraints, we can create powerful APIs. The compiler can optimize the code while we keep the ability to hide concrete types.
If you like to improve your Swift knowledge, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!