Video #37: Protocol-Oriented Library Design: Part 1
Episode: Video #37 Date: Nov 12, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep37-protocol-oriented-library-design-part-1

Description
Perhaps the most popular approach to code reuse and extensibility in Swift is to liberally adopt protocol-oriented programming, and many Swift libraries are designed with protocol-heavy APIs. In today’s episode we refactor a sample library to use protocols and examine the pros and cons of this approach.
Video
Cloudflare Stream video ID: 73530c4fa2fcb0f09e1e516b5ba099c5 Local file: video_37_protocol-oriented-library-design-part-1.mp4 *(download with --video 37)*
References
- Discussions
- FBSnapshotTestCase
- iOSSnapshotTestCase
- an open source app
- 1,000 images
- Jest
- Protocol-Oriented Programming in Swift
- Modern Swift API Design
- Snapshot Testing in Swift
- Protocol Witnesses: App Builders 2019
- 2019 App Builders
- 0037-protocol-oriented-library-design-pt1
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We’ve spent the past four episodes taking a deep study of protocols and exploring how their features can be represented using concrete datatypes and functions, and we’ve seen how in doing so we can address some of the shortcomings and complexities of protocols while unlocking a world of composition that was previously hidden. We’ve even suggested that the way we design our APIs could be dramatically improved by eschewing protocols in favor of these concrete datatypes and functions. That’s quite the claim, seeing as we’ve spent four episodes on the subject without ever demonstrating this! So much for “what’s the point,” huh!?
— 0:38
Well this week we finally put our theory to the test! We’re going to refactor a small library to be more flexible and extensible by abstracting away part of its design. We’ll start in the protocol-oriented way because that’s how we’re told to design these kinds of things in Swift. It’ll give us an opportunity to analyze the problems that protocols have that we think concrete datatypes and functions can solve.
— 1:06
We want to stress that we do not consider the actual library we are design to be the most interesting aspect of this topic, although it is quite cool. The real topic we are dissecting here is that of how we approach library design in Swift, and what lessons from our series on protocol witnesses can we apply so that we avoid the pitfalls of protocols. An overview of the problem
— 1:42
The library we are going to be designing is a “snapshot testing” library. Our viewers are most likely to be familiar with Facebook’s FBSnapshotTestCase , which was later transferred to Uber for maintenance and renamed iOSSnapshotTestCase . This allows you to write tests that take screen shots of your views and saves images of them. When you run your tests again, new screen shots are generated and compared with the reference images, and if a single pixel is off the test fails.
— 2:19
We love screen shot testing. We did it a lot when we were at Kickstarter, which is an open source app , I think we had nearly 1,000 images in the test suite, one for every screen, in every language we supported, and in all types of states and edge cases. Screen shot testing is also one of the first things we bring into a codebase when we work with people. It’s so powerful and allows you to refactor code with confidence.
— 2:56
The original snapshot library from Facebook was written in Objective-C, and the design hasn’t evolved much since then. Essentially it provides an assert function that you pass your view into and then the library does all the work. We’ve written our own version of that assertion helper that mimics this API, but in Swift. Let’s show that off real quick.
— 3:17
Here we have an example of a snapshot test. We instantiate this EpisodeListViewController , which is a controller we actually first implemented in a previous Point-Free episode on styling UIKit using functions. We then assert a snapshot of the controller’s view: class PointFreeFrameworkTests: SnapshotTestCase { func testEpisodesView() { let episodesVC = EpisodeListViewController(episodes: episodes) assertSnapshot(matching: episodesVC.view) } }
— 3:48
When we first run it, we get a test failure saying that a snapshot was recorded. This is an image of the view, written to disk, that repeat test runs will compare against. failed: Recorded - … “…/__Snapshots__/SnapshotTestingTests/testView.png”
— 4:11
When we rerun the test, it passes, meaning our view matches the screen shot on disk!
— 4:24
And just to make sure it works, let’s change the view by omitting the first episode we pass. let episodesVC = EpisodeListViewController( episodes: episodes.dropFirst() )
— 4:32
Now, when we run the test, it fails. failed: Snapshot didn’t match reference
— 4:39
And we can check out the test report to inspect the failure. We can see our reference image, the snapshot that failed, and even a difference.
— 5:10
This is pretty powerful! We can write tests against how our views should render, catch accidental changes and regressions, and even keep track of changes over time in source control and pull requests.
— 5:19
When we undo our change, the test passes again.
— 5:25
Let’s dive into the code and see how it works. class SnapshotTestCase: XCTestCase { var record = false func assertSnapshot( matching view: UIView, file: StaticString = #file, function: String = #function, line: UInt = #line) { let snapshot = UIGraphicsImageRenderer(size: view.bounds.size) .image { ctx in view.layer.render(in: ctx.cgContext) } let referenceUrl = snapshotUrl(file: file, function: function) .appendingPathExtensions("png") if !self.record, let referenceData = try? Data(contentsOf: referenceUrl) { let reference = UIImage( data: referenceData, scale: UIScreen.main.scale )! guard let difference = Diff.images(reference, snapshot) else { return } XCTFail( "Snapshot didn't match reference", file: file, line: line ) XCTContext.runActivity( named: "Attached failure diff" ) { activity in [reference, snapshot, difference] .forEach { image in activity.add(XCTAttachment(image: image)) } } } else { try! snapshot.pngData()!.write(to: referenceUrl) XCTFail( "Recorded: …\n\"\(referenceUrl.path)\"", file: file, line: line ) } } }
— 5:29
It’s an XCTestCase subclass with a single boolean property, record , and the assertSnapshot helper we saw earlier. func assertSnapshot( matching view: UIView, file: StaticString = #file, function: String = #function, line: UInt = #line) { This helper first takes a view to be snapshot and compare with a saved reference. It then takes a few arguments— file , function , and line —which by default evaluate to the name of the file, function, and line wherever assertSnapshot is called. We can pass file and line to all of the XCTest assertion helpers to tell Xcode where it should highlight failures, and we can additionally use the function name to build unique file URLs for our snapshots. let snapshot = UIGraphicsImageRenderer(size: view.bounds.size) .image { ctx in view.layer.render(in: ctx.cgContext) } let referenceUrl = snapshotUrl(file: file, function: function) .appendingPathExtensions("png") Our helper starts by converting the view into a diffable, serializable snapshot format: a UIImage . Then it uses a helper to generate a reference URL for this snapshot. if !self.record, let referenceData = try? Data(contentsOf: referenceUrl) { let reference = UIImage( data: referenceData, scale: UIScreen.main.scale )! guard let difference = Diff.images(reference, snapshot) else { return } XCTFail( "Snapshot didn't match reference", file: file, line: line ) XCTContext.runActivity( named: "Attached failure diff" ) { activity in [reference, snapshot, difference] .forEach { image in activity.add(XCTAttachment(image: image)) } } We then hit an if - else branch that first checks to make sure that the record property isn’t set to true and that a reference exists on disk. If record is false and a reference is found, another helper is used to compare the two images. If the images match, we bail out of the function: the test passed! If the images don’t match, we get an image of the difference , we call XCTFail with an error message, and we attach our reference, snapshot, and difference images to the failing test. } else { try! snapshot.pngData()!.write(to: referenceUrl) XCTFail( "Recorded: …\n\"\(referenceUrl.path)\"", file: file, line: line ) } Finally, if record is set to true or no reference exists, we record the snapshot and fail in order to alert the user. Generalizing screen shot tests
— 7:08
Now this is a perfectly useful, usable little library on its own as is! However it’s not super flexible. All of the logic is pretty rigid to work with a single kind of snapshot: images of UIView s.
— 7:26
Let’s make things more reusable by describing what it means to be snapshot-able, because it could be useful to take snapshots of our view controller directly, or perhaps to take snapshots of layers and images. We should be able to generalize using protocols. protocol Snapshottable { } We’ll start with an empty protocol. When we use it in our assertion helper, the compiler will guide us along the way.
— 7:58
Let’s swap out UIView for Snapshottable and rename view to value . func assertSnapshot( matching value: Snapshottable, file: StaticString = #file, function: String = #function, line: UInt = #line) {
— 8:06
We get a couple errors: let snapshot = UIGraphicsImageRenderer(size: view.bounds.size) .image { ctx in value.layer.render(in: ctx.cgContext) } Use of unresolved identifier ‘view’ We are trying to get a snapshot image of the view, so the first requirement of our Snapshottable protocol could be that it can produce a snapshot image. protocol Snapshottable { var snapshot: UIImage { get } } With this defined, we can update our snapshot helper accordingly. let snapshot = value.snapshot
— 8:34
We have one other failure in our test. assertSnapshot(matching: episodesVC.view) Note Argument type ‘UIView?’ does not conform to expected type ‘Snapshottable’
— 8:41
We need to extend UIView to be Snapshottable , and we can reuse the logic that we previously inlined. extension UIView: Snapshottable { var snapshot: UIImage { return UIGraphicsImageRenderer(size: self.bounds.size) .image { ctx in self.layer.render(in: ctx.cgContext) } } }
— 8:58
It builds, it runs, and tests pass! It may not seem like we’ve done much, but we’ve generalized the interface to work with any Snapshottable thing. We only have a single conformance so far, so let’s change that.
— 9:06
For example, we can conform UIViewController by returning the view’s snapshot. extension UIViewController: Snapshottable { var snapshot: UIImage { return self.view.snapshot } }
— 9:23
And now we can simplify our test by passing the view controller directly to assertSnapshot : assertSnapshot(matching: episodesVC)
— 9:32
What other conformances are out there? Well our view conformance is rendering the view’s layer, so maybe we want to define a layer conformance directly. extension CALayer: Snapshottable { var snapshot: UIImage { return UIGraphicsImageRenderer(size: self.bounds.size) .image { ctx in self.render(in: ctx.cgContext) } } }
— 9:50
And we can even clean up our UIView conformance to delegate to CALayer logic. extension UIView: Snapshottable { var snapshot: UIImage { return self.layer.snapshot } }
— 10:05
Well, we could write snapshot tests against any UIImage that our code may render, not just views. extension UIImage: Snapshottable { var snapshot: UIImage { return self } } This would be helpful if you are doing any image processing in your app and you want to write tests that verify the output of that processing.
— 10:19
Where assertSnapshot used to only take UIView s, it now takes any type that conforms to the Snapshottable protocol, which we rapidly built four conformances around. And any type that can produce an image could be extended to conform to this protocol easily. We could keep going and conform more types in the standard library, Foundation, UIKit. We could even conform our own types! We also got here with very little work and very few changes to the base library. It’s fair to say that protocols are a very powerful tool for abstraction. Generalizing the snapshot format
— 10:45
One issue with our current abstraction is that it limits what we can snapshot into to images. This is maybe what we’re most used to, because libraries like iOSSnapshotTestCase popularized the idea of snapshot testing views as images in our community.
— 11:04
Outside our community, React and JavaScript have a testing framework called Jest , which allows you to take text-based snapshots. Jest is typically used to snapshot and assert against React DOM view hierarchie.
— 11:30
Let’s try to improve our library to allow for that.
— 11:37
We’ll need to adjust our Snapshottable protocol, though. Let’s take a look. protocol Snapshottable { var snapshot: UIImage { get } } Right now the snapshot property returns a UIImage .
— 11:48
In order support snapshotting into other types we need to introduce an associatedtype that can represent these various formats. protocol Snapshottable { associatedtype Snapshot var snapshot: Snapshot { get } } This means that types conforming to Snapshottable must declare what type they are snapshotting into.
— 12:07
This breaks a lot of things.
— 12:16
The first major thing is that we can no longer refer to Snapshottable as a type directly in our assertSnapshot helper. We get the dreaded error: Protocol ‘Snapshottable’ can only be used as a generic constraint because it has Self or associated type requirements
— 12:26
We can adjust the signature to satisfy the compiler: func assertSnapshot<S: Snapshottable>( matching value: S, file: StaticString = #file, function: String = #function, line: UInt = #line) { We’ve introduced a generic to assertSnapshot and constrained it to the Snapshottable protocol. This is the dance we need to do whenever we work with protocols that refer to capital Self or have associated types.
— 12:32
One error down, two to go. The next error occurs when we pass our snapshot to Diff.images : guard let difference = Diff.images(reference, snapshot) else { return } Cannot convert value of type ‘S.Snapshot’ to expected argument type ‘UIImage’
— 12:44
This appears to be the first requirement of our associated Snapshot type: the ability to diff! All of our current Snapshottable types produce an image, which is diffed using a Diff.images helper. In order to make this associated type “diffable” we need to constrain it to a protocol that describes diffing. We can even call it Diffable . protocol Diffable { static func diff(old: Self, new: Self) -> Self? }
— 13:24
The Diff.images helper takes in two images and, if they don’t match, spits out another image describing the difference.
— 13:33
Let’s constrain our associated Snapshot type to Diffable . protocol Snapshottable { associatedtype Snapshot: Diffable var snapshot: Snapshot { get } }
— 13:47
Alright, a lot of things break. All of our Snapshottable types no longer conform because UIImage doesn’t conform to Diffable . Conforming should be relatively simple: we just need to wrap up a call to the Diff.images helper. extension UIImage: Diffable { static func diff(old: UIImage, new: UIImage) -> Self? { return Diff.images(old, new) } }
— 14:21
We add the conformance, let Xcode add a stub, and make the call to Diff.images , but we get an error: Cannot convert return expression of type ‘UIImage?’ to return type ‘Self?’ Our helper returns an explicit UIImage , but our protocol requires we return this Self instead.
— 14:31
We can’t merely change our function to return UIImage . static func diff(old: UIImage, new: UIImage) -> UIImage? { Method ‘diff(old:new:)’ in non-final class ‘UIImage’ must return ‘Self’ to conform to protocol ‘Diffable’
— 14:39
Unfortunately, UIImage is a “non-final” class, which means it could technically have subclasses! Whenever such a class conforms to a protocol with capital Self requirements, as our Diffable protocol does, then we must also use Self in our conformance, to ensure that the proper init is called and the proper subclass is instantiated. If we had a MyImage subclass and called MyImage.diff , the protocol says it should return an optional MyImage .
— 14:57
And this is one of the tricky things about protocols and non-final classes! Whenever we conform non-final classes to protocols with a requirement that returns Self , we need to call a “required” self.init initializer, which can be difficult to impossible.
— 15:27
We have a few initializers at our disposal, like init(cgImage:) and init(data:) , that we could use. For example, we can take our UIImage difference, convert it to a CGImage or Data , and then convert it back again: extension UIImage: Diffable { static func diff(old: UIImage, new: UIImage) -> Self? { guard let difference = Diff.images(old, new) else { return nil } return self.init(cgImage: difference.cgImage!) } }
— 16:18
But this is kind of a mess: we’re doing extra work that feels like a hack in order to satisfy a language quirk. Fortunately, if we look a bit further down the road, we can avoid this.
— 16:34
How are we using this difference in the assertSnapshot helper? guard let difference = Diff.images(reference, snapshot) else { return } XCTFail("Snapshot didn't match reference", file: file, line: line) XCTContext.runActivity( named: "Attached failure diff" ) { activity in [reference, snapshot, difference] .forEach { image in activity.add(XCTAttachment(image: image)) } }
— 16:45
It seems that the difference is mainly used as an XCTest attachment. We can alter the diff protocol requirement to return those attachments instead and obviate the need for returning Self . protocol Diffable { static func diff(old: Self, new: Self) -> [XCTAttachment] } Our Diffable protocol is now very much coupled to XCTest , but that’s how we’re using it, so it seems fine.
— 17:06
What’s the UIImage implementation look like? extension UIImage: Diffable { static func diff(old: UIImage, new: UIImage) -> [XCTAttachment] { guard let difference = Diff.images(old, new) else { return [] } return [old, new, difference].map(XCTAttachment.init) } } If the images match, we bail out with an empty array, otherwise we return a bunch of XCTest attachments.
— 17:36
We can now capture these attachments in a let , but our call to diff still has a problem. let reference = UIImage( data: referenceData, scale: UIScreen.main.scale )! guard let difference = S.Snapshot.diff(old: reference, new: snapshot) else { return } Cannot invoke ‘diff’ with an argument list of type ‘(UIImage, S.Snapshot)’
— 18:11
We’ve currently hardcoded loading a UIImage from referenceData , but we need to somehow load a Snapshot instead in order to pass the right type to diff .
— 18:21
This means our associated Diffable type has another requirement: it needs to be loadable from Data . protocol Diffable { static func diff(old: Self, new: Self) -> [XCTAttachment] init(data: Data) } Protocol wrinkle #2: naming collisions Non-failable initializer requirement ‘init(data:)’ cannot be satisfied by a failable initializer
— 18:37
Unfortunately we can’t add init(data: Data) to our protocol because it collides with UIImage ‘s failable init?(data: Data) initializer. This is another problem with protocols: the potential for naming collisions. We’re building this protocol ourselves, so we can avoid this collision, but if we were trying to take a third-party protocol and conform another third-party type to it, a naming collision could prevent us from doing so entirely!
— 19:06
There are a lot of complicated rules around init , like required vs. convenience , so let’s keep it simple and use a static func instead. protocol Diffable { static func diff(old: Self, new: Self) -> [XCTAttachment] static func from(data: Data) -> Self }
— 19:26
And now we can get UIImage to conform. extension UIImage: Diffable { static func from(data: Data) -> Self { return self.init(data: data, scale: UIScreen.main.scale)! } static func diff(old: UIImage, new: UIImage) -> [XCTAttachment] { guard let difference = Diff.images(old, new) else { return [] } return [old, new, difference].map(XCTAttachment.init) } }
— 19:45
We can now fix our error from before: let reference = S.Snapshot.from(data: referenceData)
— 20:08
Next, let’s update our explicit XCTAttachment code to work with the attachments returned. let attachments = S.Snapshot.diff(old: reference, new: snapshot) guard !attachments.isEmpty else { return } If none are returned, we bail out. The snapshot matches the reference and the assertion succeeds.
— 20:27
Otherwise, we loop over them and add them to the test report. XCTContext.runActivity(named: "Attached failure diff") { activity in attachments.forEach { attachment in activity.add(attachment) } }
— 20:48
We can even go point-free! XCTContext.runActivity(named: "Attached failure diff") { activity in attachments.forEach(activity.add) }
— 21:00
Only one error left! While we can load Diffable types from Data , we can’t yet save them to Data . try! snapshot.pngData()!.write(to: referenceUrl) Value of type ‘S.Snapshot’ has no member ‘pngData’
— 21:20
Easy enough! We can add a computed property. protocol Diffable { static func diff(old: Self, new: Self) -> [XCTAttachment] static func from(data: Data) -> Self var data: Data { get } }
— 21:30
And make UIImage conform. extension UIImage: Diffable { var to: Data { return self.pngData()! } static func from(data: Data) -> Self { return self.init(data: data)! } static func diff(old: UIImage, new: UIImage) -> [XCTAttachment] { guard let difference = Diff.images(old, new) else { return [] } return [old, new, difference].map(XCTAttachment.init) } }
— 21:45
Let’s swap out the implementation in assertSnapshot . try! snapshot.data.write(to: referenceUrl) XCTFail("Recorded: …\n\"\(referenceUrl.path)\"", file: file, line: line)
— 21:55
It took a bit of work to get here, but what we’ve done is completely decoupled the Snapshottable protocol from image-based snapshots. Till next time…
— 22:22
OK! So everything compiles and tests run and pass just as before! Unfortunately it was pretty complicated. We really butted heads with some seriously complicated protocol features, like Self , non-final classes, and required initializers. But at least now the Snapshottable protocol is completely decoupled from taking image snapshots. In fact, it’s more generic than any snapshot testing library out there. Can we do anything interesting with that? References Protocol-Oriented Programming in Swift Apple • Jun 16, 2015 Apple’s eponymous WWDC talk on protocol-oriented programming: At the heart of Swift’s design are two incredibly powerful ideas protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write. https://developer.apple.com/videos/play/wwdc2015/408/ Modern Swift API Design Apple • Jan 2, 2019 As of WWDC 2019, Apple no longer recommends that we “start with a protocol” when designing our APIs. A more balanced approach is discussed instead, including trying out concrete data types. Fast forward to 12:58 for the discussion. Note Every programming language has a set of conventions that people come to expect. Learn about the patterns that are common to Swift API design, with examples from new APIs like SwiftUI, Combine, and RealityKit. Whether you’re developing an app as part of a team, or you’re publishing a library for others to use, find out how to use new features of Swift to ensure clarity and correct use of your APIs. https://developer.apple.com/videos/play/wwdc2019/415/?time=778 uber/ios-snapshot-test-case Uber, previously Facebook Facebook released a snapshot testing framework known as FBSnapshotTestCase back in 2013, and many in the iOS community adopted it. The library gives you an API to assert snapshots of UIView ’s that will take a screenshot of your UI and compare it against a reference image in your repo. If a single pixel is off it will fail the test. Since then Facebook has stopped maintaining it and transfered ownership to Uber. https://github.com/uber/ios-snapshot-test-case Snapshot Testing in Swift Stephen Celis • Sep 1, 2017 Stephen gave an overview of snapshot testing, its benefits, and how one may snapshot Swift data types, walking through a minimal implementation. https://www.stephencelis.com/2017/09/snapshot-testing-in-swift Protocol Witnesses: App Builders 2019 Brandon Williams • May 3, 2019 Brandon gave a talk about “protocol witnesses” at the 2019 App Builders conference. The basics of scraping protocols is covered as well as some interesting examples of where this technique really shines when applied to snapshot testing and animations. Note Protocol-oriented programming is strongly recommended in the Swift community, and Apple has given a lot of guidance on how to use it in your everyday code. However, there has not been a lot of attention on when it is not appropriate, and what to do in that case. We will explore this idea, and show that there is a completely straightforward and mechanical way to translate any protocol into a concrete datatype. Once you do this you can still write your code much like you would with protocols, but all of the complexity inherit in protocols go away. Even more amazing, a new type of composition appears that is difficult to see when dealing with only protocols. We will also demo a real life, open source library that was originally written in the protocol-oriented way, but after running into many problems with the protocols, it was rewritten entirely in this witness-oriented way. The outcome was really surprising, and really powerful. https://www.youtube.com/watch?v=3BVkbWXcFS4 Downloads Sample code 0037-protocol-oriented-library-design-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .