(Note: This post uses Swift 5.2 and the iOS13 SDK)
It’s been almost exactly a year since I started taking classes at Lambda School, and now I’m on the cusp of completing my time there and starting my job search in earnest (hit me up if you know a place!). As part of completing my “endorsement” (aka graduation), I need to write a “what I learned at school today”-style article. Once again you can probably read my hint of snarkiness, but also, again, I think this is a potentially useful exercise.
The Stakeholder
My team worked with the non-profit organization Eco-Soap Bank. They describe themselves thus:
Eco-Soap Bank is a humanitarian and environmental non-profit organization working to save, sanitize, and supply recycled hotel soap for the developing world. Our work has three objectives:
- Contribute a highly cost-effective hygiene product to improve health.
- Significantly reduce the waste generated by the hotel industry.
- Provide livelihoods and free education to disadvantaged women with no other reliable source of income.
Lack of access to soap remains a critical factor in fighting the spread of preventable diseases worldwide. In some areas of the developing world, only 1% of households have soap for handwashing. Eco-Soap Bank seeks to address the critical need for hygiene. Working since 2014, Eco-Soap Bank has sustainably supplied over 650,000 people with soap and hygiene education.
This is definitely a project I can get behind, so I was happy to be building an app for their hotel partners to interface with the company.
Team Dynamics & Work Styles
In general our team has been working quite well together, despite some different work styles and communication styles. One thing that’s come up a couple of times is not quite undercommunication, but just not communicating certain important things that we didn’t realize were important.
For example, at one point a team member and I both ended up making helper functions for layout code. After realizing we had both put in this work, we ended up using one set of functions for consistency. They were spur-of-the moment decisions, but they ended up having ramifications.
In the future, I’d love to set up some central repository whose sole purpose would be to record and report what each team member is working on at any given moment. This way, upon starting any task, I could see what everyone else is working on and avoid this sort of overlap. Making this easy to set up would be key, of course; no one wants to use a clunky tool, as evidenced by our minimal use of some of the tools we’d been asigned to use.
Coordinators & Architecture
For our app’s architecture, we decided to use the coordinator pattern (which I recently wrote about here). This allows us to more easily create views programmatically and separate concerns between views and other objects, which in turn increases testability.
It’s not without its downsides, though. Being a non-standard pattern, we had to figure out for ourselves how best to use it, how to inject dependencies, what class had ownership of what, etc. Along the way I think we’ve learned ways to make the pattern work better, including working in a more test-driven development way to ensure everything is as testable as possible even beyond the coordinators and view layer. Use of protocols also helps tremendously so that we can insert mock data in lieu of our live networking code, views, and delegates.
SwiftUI vs. UIKit, or New-Shiny vs. Old-Reliable
I decided early on to write some of my view code in SwiftUI, Apple’s newer UI framework. Many tasks end up being much quicker and easier using this new framework, and I was excited to get to play with it in production.
This turned out to be a bad call. Although it is possible to do anything in SwiftUI that you could in UIKit by bridging to the older framework, this often turns out to be more work than it would have been to just write it in UIKit in the first place, especially when it involves rewriting entire views because SwiftUI obfuscates certain options away from any kind of customizability.
As I went I often discovered 100% “native” SwiftUI methods for accomplishing certain things, but they involved a whole new way of conceiving of things. For example, here’s a simplified version of code I wrote to horizontally align the edges of vertically stacked views.
struct EditProfileView: View {
@State var profile: EditableProfileInfo
/// The width of the widest label; used for aligning labels in a column.
@State var labelWidth: CGFloat?
var body: some View {
Form {
Section(header: Text("Name".uppercased())) {
self.textField("First", text: $profile.firstName)
self.textField("Middle", text: $profile.middleName)
self.textField("Last", text: $profile.lastName)
}
}
}
/// A custom label and text field, aligned vertically.
func textField(_ title: String, text: Binding<String>) -> some View {
LabelAlignedTextField(
title: title,
labelWidth: $labelWidth,
text: text
)
}
}
struct LabelAlignedTextField: View {
let title: String
@Binding var text: String
@Binding var labelWidth: CGFloat?
init(title: String, text: Binding<String>, labelWidth: Binding<CGFloat?>, ) {
self.title = title
self._text = text
self._labelWidth = labelWidth
}
var body: some View {
HStack(alignment: .center) {
Text(title)
.readingGeometry(
key: WidthKey.self,
valuePath: \.size.width,
onChange: {
if let new = $0, let old = self.labelWidth {
self.labelWidth = max(new, old)
} else {
self.labelWidth = $0
}
}
).frame(width: labelWidth, alignment: .leading)
TextField(title, text: $text)
}
}
}
extension LabelAlignedTextField {
struct WidthKey: PreferenceKey {
static var defaultValue: CGFloat?
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let new = nextValue() {
if let old = value {
value = max(old, new)
} else {
value = new
}
}
}
}
}
extension View {
/// Reads the provided key path value from a `GeometryReader`'s proxy
/// applied to the view's background and applies it to the provided
/// preference key, performing the provided closure when the preference
/// changes. Useful for reading size values, writing them to a size binding,
/// and aligning several views according to this value.
func readingGeometry<K: PreferenceKey, V>(
key: K.Type,
valuePath: KeyPath<GeometryProxy, V>,
onChange: @escaping (K.Value) -> Void
) -> some View where K.Value == V?, V: Equatable {
self.background(GeometryReader { proxy in
Color.clear
.preference(key: K.self,
value: proxy[keyPath: valuePath])
}).onPreferenceChange(K.self, perform: { onChange($0) })
}
}
Yes, all that code was written to get this.
Some of the code was written in a way to increase reusability, of course (though I only ended up reusing it once or twice, to the point that I probably should not have wasted the time in writing the reusable version). It’s also quite possibile that I’m missing a much more obvious way of accomplishing this goal (although I did steal the GeometryReader-background-Color.clear-PreferenceKey-alignment trick from objc.io, and they seem to know what they’re talking about).
The point still stands, however, that this likely would have been much simpler in UIKit. Although a couple of views still use SwiftUI, I ended up having to entirely re-write some SwiftUI views using UIKit, which obviously ended up wasting some time in the end. Still, the sunk cost fallacy comes to mind; no use wasting more time on something that’s not working just because you’ve already spent a lot of time on something. Sometimes this lesson is best learned the hard way.
Conclusions
A couple more quick tidbits I learned this year at Lambda School:
- Readibility first, optimization if necessary.
- Naiive solutions first, optimized solutions later (unless the optimized solution is just as quick/easy to write).
- Hard conversations are better in the long run than stewing in negativity.
- Stick to working on one thing at a time.
- (This was a tough one for me to learn, but submitting upon submitting a pull request that makes changes all over the app made me realize it was something I needed to work on (and I’ve definitely improved!).)
- Document as you go, for your teamamates as well as future-you.
- Test as you go; not only will it catch bugs, but you’ll write cleaner code.
- If a generic, reusable abstraction will take more time to write than repeating yourself, just go ahead and repeat yourself. No one will call the code police.
- Keep your architectural hierarchy consistent and clean.
I of course reserve the right to change my mind on any of these.
Happy graduation!