Swift: Alternative to Default Implementations in Protocols
I recently wrote about why I prefer not to use default implementations in Swift protocols.
TL;DR
- I want a lot of conscious thought put in into each method of the protocol – adding a default implementation will make it easy to forget and not think about much. Oh, and the compiler won’t complain!
- I want it to be easy to make changes in the implementation. If a method is not included in the file because the implementation is in the protocol, it’s more work to make the decision to add that into the file. The assumption is that the default should win. If it’s already there, it’s easier to just go in and make a small change.
- Similar to the above point, I want it to be super readable where each method is coming from. If a bunch of methods are in the default implementation of the protocol, it’s not as clear just by looking at the file.
But there might still be repeating or “default” implementations that are shared across protocol implementations. In my case, I approached this issue by creating an object with some variables that could be shared across protocols:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
struct SessionDataDefaults { let title: String let subtitle = "try! Conference" let logoImageURL = Bundle.trySwiftAssetURL(for: "Logo.png")! let imageURL: URL? let location: String let summary = Conference.current.localizedDescription let twitter = Conference.current.twitter! init(session: Session) { title = session.localizedString(for: session.title ?? "TBD", japaneseString: session.titleJP) if let url = session.imageWebURL { imageURL = URL(string: url) } else if let assetName = session.imageAssetName { imageURL = Bundle.trySwiftAssetURL(for: assetName) } else { imageURL = nil } if let location = session.location { self.location = location.localizedName } else { self.location = Venue.localizedName(for: .conference) } } } |
However, when I wrote the article about it, @jckarter tweeted the following:
@NatashaTheRobot Another approach: put the defaults on a separate protocol you conform to when you intentionally want them on your type
— Joe Groff (@jckarter) March 25, 2017
In my case, it didn’t make sense to create a separate protocol with default implementation because the use of the default values is pretty random – there isn’t any type of session that will use all defaults at the same time (this is one reason I didn’t want to include a default implementation in the first place).
But this did spark another idea! I could make my data defaults object conform to the same protocol as the objects where the defaults are being used – in my case, SessionDisplayable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
struct SessionDataDefaults: SessionDisplayable { fileprivate let session: Session init(session: Session) { self.session = session } var title: String { return session.localizedString(for: session.title ?? "TBD", japaneseString: session.titleJP) } var subtitle: String { return Conference.current.name ?? "try! Conference" } var logoURL: URL { return Conference.current.logoURL } var location: String { if let location = session.location { return location.localizedName } else { return Venue.localizedName(for: .conference) } } var sessionDescription: String { return "❤️".localized() } var presentationSummary: String { return Conference.current.localizedDescription } var twitter: String { return Conference.current.twitter! } var selectable: Bool { return false } } |
The best part about this is that when a method needs to use the default implementation, it can use the defaults method that has the same exact name as the method being implemented!
Here is an example of another SessionDisplayable using the new defaults:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
struct CoffeeBreakSessionViewModel: SessionDisplayable { private let session: Session private let dataDefaults: SessionDataDefaults init?(_ session: Session) { if session.type == .coffeeBreak { self.session = session self.dataDefaults = SessionDataDefaults(session: session) } else { return nil } } var title: String { if let sponsor = session.sponsor { return String(format: "Coffee Break, by %@".localized(), sponsor.name) } return "Coffee Break".localized() } var subtitle: String { if let sponsor = session.sponsor { return sponsor.localizedName } // Matching variable name in the defaults // makes it super easy to use!! return dataDefaults.subtitle } var logoURL: URL { if let imageURL = dataDefaults.imageURL { return imageURL } if let sponsor = session.sponsor { return sponsor.logoURL } // Matching variable name in the defaults return dataDefaults.logoURL } var location: String { // Matching variable name in the defaults return dataDefaults.location } var sessionDescription: String { return "❤️".localized() } var presentationSummary: String { // Matching variable name in the defaults return dataDefaults.presentationSummary } var selectable: Bool { return session.sponsor != nil } var twitter: String { let twitter = session.sponsor?.twitter ?? dataDefaults.twitter return "@\(twitter)" } } |
I’m very happy with this solution. It’s super easy to use the defaults, but still allows for the compiler to complain when a protocol method is not implemented.