Incrementally Open Sourcing Your App
Anat Gilboa at Swift Summit 2017
I'm Anat. Like Garric said, I work at Blue Apron. Today I'm going to be talking about open sourcing a UI component that I built out initially without the intention of open sourcing and kind of the process that went around that. As the title suggests, the premise of the talk is incremental open sourcing, which I define as iterating on the ideas that you're having during the process. While there are pros and cons to the approach, I think there's a key takeaway that is; it's worth it to take your time with the project that you're working on.
We're going to work through the timeline that I went through and talk about what it was like developing the idea, the defining questions and the initial development, and then getting a little bit blocked by the development and that leading to more questions, and kind of repeating steps four and five and step six, until we're done. We're also going to be talking about what it means to be done. I joined my team in March and the first feature that I worked on was the meal rescheduler, which is essentially a date picker that's initialized by a set of available delivery windows. They'll be in order, but not necessarily consecutive. Passed down by the API, we might need to ... Say there might be two dates that are next to each other, and the set of delivery windows, only to fill that in on the client with another date that's just unavailable and indicate that that said date is unavailable.
Like most features in our app that require animations, our designer used this app called Principle to create the animations that were required for this design. It's highly sophisticated, and I really recommend it in case you're looking for a similar kind of tool.
The first thing that I noticed was this kind of like green dot that would essentially pulsate based on the user's scrolling and based on the availability of the window. When I asked Wayne, our designer, how he came up with the design, he said that he was inspired by the Wheel of Fortune ticker. It kind of has this like nice deceleration. There are actually APIs in UICollectionView that support being able to detect the velocity essentially of a scroll. He was like, "Oh yeah. It'd be really cool if we had some haptic feedback too." That's also a gem in there, which is like a really nice touch.
Let me play the basic flow. A couple of things to notice. We present the view at a fixed height by overriding frame for presented view and in containing view, which we already had as like this kind of HUD animation in transition controller in the existing upper right hand corner for our existing calendar view. We already had that in the app, and I was just able to essentially create a view controller that would have also like an additional button and some message texts and like other related items in here. Those would all change based on the availability of the date selected. They're all observing the user scroll. That was pretty much it.
That's what I wanted to open source. I was pretty sure that the selling point was the green dot and the haptic feedback. When I was building this, once we figured out the function that we needed in order to scale the size of the dot, I'm pretty sure I was like scrolling left and right for days. It was like so fun.
To get this project started, I consulted a few of my peers on my team because I had a couple of questions. Where do I even start? After all this is just a UICollectionView, right? For instance, do I want to include the animation controller and the presentation logic for the transition? What are the requirements for the cells? Are they going to be dequeued using a protocol? If so, is this just going to be a date picker or is the only requirement that it's selectable? What should the overlay look like? That little blue box. How should a user be able to customize that?
You can't really just talk about it. You have to actually get to doing it. I went ahead, and I got started. For some reason, my gut instinct was to kind of slowly take what we had out of the reschedule delivery view controller and just put what we had into a separate view in the same project, the HorizontalScrollingPickerView, and reference it from the original view controller.
That lead me to my first question, do I make this a UIViewController or just a UIView? Let's take a closer look. In the existing view controller that this lived in, we've got the collection view, the titleLabel, the messageLabel, and the button that would all change based on the user’s scroll. We also initialize it using a viewModel, which we use MVVM architecture in our app, which essentially just controls the layer between the view and the actual view controller. I figured I was able to simplify this into a UIView, which I thought was the appropriate way to start working on this. We just had a calendarView, the selectedDayOverlay, and the viewModel.
We would be able to reference the cells that we were already using in the collection view. Since we already used the view model in the view controller, that already has a bunch of code related to like the calendar view, swiping stuff. I figured why not just create one from there? I kept coding along and then I realized I didn't actually want to have this kind of like circular dependency between like one view model referencing another and just changing and observing the other. I had my first question. I walked over to Brian on my team, and I presented my dilemma. He pointed me to the first problem in my approach, which was trying to fit this into the app.
He was like, "Just leave and make the pod. Just put all the bare minimum that you need in the pod and just work up." That brings me to my first lesson, take the first step whether it's a CocoaPod or a Carthage, a framework, Swift Package. You can think about how you want to integrate this into your own app, but for us, we use CocoaPods. This is actually especially applicable to those kinds of libraries that you're building that are already built, but they just need to be extracted. Since we use CocoaPods internally, my job was to make this a pod.
All I had to do was literally just; pod lib create HorizontalScrollingPickerView. Which for better or worse is literally what I named it. The CocoaPods, Carthage, Swift Package Manager is a separate discussion, but since CocoaPods is centralized, there's a bit of configuration that needs to happen in this podspec file, which outlines the different properties for the pod, like what someone might see when they look it up.
After running that command and subsequent configuration commands, we finished, and a template is created, and we're left with two folders. Kind of this top most folder, which is where the development pods live, and the bottom most, which is just the example project where we'll reference essentially everything that goes up into the classes. The HorizontalScrollingPickerView, the HorizontalScrollingPickerView cell, the HorizontalScrollingPickerView model, like all of those go in the classes and the respective assets in that folder. Then the example project is really just where … It kind of acts like a Playground where you can actually just play around while you're developing the API.
I was on a roll. I understood the structure, and I was back into development. I would copy everything that I needed from the existing project including the delegate methods for the collection view, the presentation controller and the animation controller, and just filled out the example project as needed.
This took a couple of hours, and I spread the work across a couple of weeks. While this was somewhat intentional, once I got the development pod to work, I was stoked. I was just getting it to compile and render properly was the first step. I knew it wasn't ready to be public, but now I could go to sleep, which brings me to my second point. Most difficult engineering problems take time to process and revisit your work. This is natural. Staying up late and spinning your wheels around how to customize something that can be generalized and think of the most general case from a custom case can be exhausting. Take it from me, stepping away and asking someone to be a sounding board can really help.
Now it came time to generalize, and I had some more questions. How are we going to actually present each cell? Is this a date picker or is this just a HorizontalScrollingPickerView? If so, then we're going to need to require a date for the first. If it's not, then what are we actually trying to sell here? For example, in the existing Reschedule Delivery view controller, we've got this scrollViewDidScroll, and we've got the existing like fancy dot logic at the top and then after that we'll be able to tell like, "Okay, well, if it's selectable, then change the state of the button. If it's not, then change the state of the button with their respective enabling and disabling. "
The question was we need to figure out how to let the consumer of the API actually interact with the collection view delegates and anything that's going on in the pod. That takes me to my third point, maybe the golden rule of API design, develop an API that you would want to use. One of the most helpful links that I read during this process was about API Design by Matt Gemmell, which interestingly enough was written in 2012, around a library that was written in Objective-C. If you may recall in Objective-C, it already kind of implements API design inherently in the way that ... Can you hear that clicking? Is it bothering anyone? All right. Can you hear me now? Okay. Cool. I can hear myself.
In the way that we use interfaces and implementations. The interface is what we use to expose objects to the outside world and the implementation hides that detail. Similarly, we can apply this to API design. Anyway, this is a great read and I highly recommend it. It outlines some other important stuff like how software should be discoverable and default implementations should be useful as is. And how a user should be able to generalize known paradigms. How can we apply this to our problem?
Well, we already have delegates and data sources that we're familiar with. A common pattern in iOS and general Cocoa development is configuring a delegate and a dataSource. We're already using that for UICollectionView, so why not actually expose the ones that we need? We need to respond to user scrolling and when the dragging begins and when the dragging ends and if a user selected an item and ultimately what kind of item to display. I made my own delegate and exposed essentially the collection view delegate methods that we would want to use internally in the Blue Apron app in this delegate.
There's the didSelectItemAtindexPath, the scrollViewWillBeginDragging -WillEndDragging and scrollViewDidScroll. We didn't actually indicated how we wanted to do like cell throw at index path, which brings us back to the original question about using protocols and dequeuing a cell. How should we define this behavior?
We created a configure method, which kind of fixes the problem of dequeuing a cell as HorizontalScrollingPickerViewCell, which was used internally in the pod before or in the project before, but we can't actually use that for the example project or anyone implementing this with their own project because they don't necessarily have a collection of cell view models and what not to configure their own cells. We have this configure UICollectionViewCell for IndexPath. That's going to be called at the end of the cell for at index path. A delegate can implement it as needed.
Next I took a look at how we're using the view model, which again is convenient for me because I've just ripped this out of our existing code base and it's easier for me to implement because we already had it there. There are really only a few core components to this. Just a list of cellViewModels that we had and just a selected cell and a way to select a cell. Then I simplified this and removed the public accessor because we don't want the consumer of the API to know about how it's actually being implemented. It just needs to be able to initialize, store and select.
We don't actually need the cells to use cellViewModels because they really only need to hold at the end of the day an isSelectable, right? Just follow a protocol that's going to dictate whether we're going to show the green dot or we're not going to show the green dot. That leaves us with this smaller simpler implementation, which is still using a protocol, but maybe we'll change this later. That led me to the development pod cell, which is implementing the Selectable protocol with an isSelectable. It also has the cell size because we'll actually need to indicate to the collection view how large to expect each cell.
Also, to dictate the size of the overlay view and the height of the collection view ultimately. That made me take a look at how we're using that cell size in the bigger implementation. We just set the size and have like a didSet that's going to update the constraints accordingly and setNeedsLayout. We use the cell size to determine the size for item at index path, the size of the overlay and the height of the collection view.
When you're looking at this, you might not recognize some of the syntax because we're using actually SnapKit internally at Blue Apron as our like auto layout wrapper, which was convenient when moving everything over initially because I just wanted to get everything working as soon as I could. I installed SnapKit because it's super convenient and I'm already familiar with it. This brings me to my fourth point, which is choose your dependencies wisely. Well, it's not the biggest deal in the world. It's important to work with a language that you're familiar with. If you're used to writing visual format language everyday or using Zibs or Storyboards, that's okay.
Before you commit to something like a dependency for an auto layout wrapper like SnapKit, check to see how often they have releases and traditionally if there are any drastic changes to the API with each new version of iOS. If you think about it, if my library is dependent on another library that's dependent on five other libraries and so on, we can create a dependency graph. It might look cool, but at the end it can also look super frustrating. You might want to consider this before becoming a maintainer of a library. For example, in our app internally we use this library called SwiftHEXColors that initializes UIColors from just a hex number.
If you think about this, this ruthlessly simple UI component doesn't actually need to depend on like having fancy colors for this simple case. At the end of the day, I should expect as the library maintainer that the consumer of this API is going to be able to provide their own UIColors as needed. When you're asking yourself these questions, think about what you're trying to sell again. If it's going to be detrimental to your example project if you remove this dependency, then maybe it's not worth it. If you can cut the cost, then that's just another dependency you don't have to depend on. This brings me to kind of the home stretch.
I've built this out. I'm iterating. I'm asking questions, but I'm also looking for feedback. You might have asked it yourself, "Okay. Well, how did you get peers to look at this," or you might have assumed everything was being sort in Git. Well, since I was open sourcing this with only a few people knowing about it, I knew that at the end of the day whatever the initial commit was going to be would be recorded in the history of the internet or at least just GitHub. There is definitely something scary about that. It can be nerveracking, but the initial commit needs to happen at some point.
Maybe you're open sourcing something at a company that has strict guidelines that go into doing it. Then that's a separate issue. Maybe if it's just a solo project, then you're okay with doing it whenever. If you're super nervous like me and are afraid that the whole world is going to see this and see how you used like a var instead of a let for your first commit, one way you can offset this is by auditing your code. For example, did I really need the TransitionAnimationController and the PresentationController for making that really nifty HUD screen and dimming effect? Probably not.
In the example project, we need to show the pod in the simplest case and maybe provide other cases of sample events as well. You may intend for something to look a certain way, but you can communicate that in the ReadMe if you're concerned at all. Also, since we did get rid of the SwiftHEXColors library, I don't have a fancy UIColors extensions for fancy colors anymore from the Blue Apron app. The ones you get out of the box from UIKit are not necessarily the prettiest, but they get the job done. That's pretty much it.
I have another idea that at this point you've probably been able to tell. Getting another pair of eyes on it is like really helpful.
Maybe just like Slacking over a teammate or a friend to take a look is definitely something that you want to consider in case you run into a roadblock, like I did. Where are we now? Well, at Blue Apron we recently instituted like an open source release process where ... Well, the exciting news is that it was like approved by like the technical leads and the legal team. You might want to check if your project that you're working on has similar requirements. Before I reveal the name of my library, I wanted to share a few tips for first timers out there. Fun fact. In case you haven't checked at this point, HorizontalScrollingPickerView is not on CocoaPods.
I renamed it. Plot twist. Renaming is like really hard for a CocoaPod. I looked into this and I saw one Stack Overflow post that had like a five step process that had like ... In four of those steps included like quitting and restarting Xcode. At the end of the day, I realized if someone's trying to use your library and you just like rename it while it's already published, that's not the nicest thing necessarily. You kind of want to knock that out before publishing your pod.
Also, CocoaPods does this thing where it makes an initial commit for you. There's like a clear difference between a couple of these. I went through the podspec. I finished the command line tool stuff. That was its own thing and I didn't think to check like Git Log to see if there were any commits prior to my initial commit, which at the end of the day I was like worried about like what was going to be in my initial commit, but it didn't actually matter because the initial commit was already there. Now I have two initial commits in my rebuild. You might be asking what's next? Someone mentioned to me when I was going through like a run through of this talk, "So I heard you're still working on your project. Is it finished?"
I think it's fitting to address this question because what does finishing mean to you? The answer might not be the same for everyone. The beauty of open source is that it's ever evolving and it keeps you on your toes when someone submits a PR and when something breaks. All in all I think it's one way for us to become better engineers by going through this process and this exercise of extracting logic into a separate place and putting ourselves into other people's shoes. I think I can improve on the API still internally and add a couple of samples to the example project. Maybe I'll like make the ability to create this into a VerticalScrollingPickerView for what its worth.
While this is a small project, the main reason I wanted to open source this was to allow other people to give their input and enlighten me with other ideas and other applications of this. On that note, before you go ahead and search this, Mandoline is a cooking tool that is used for like slicing. I guess like we were kind of influenced by the idea of like the haptic feedback that you'd get when sliding from back and forth. I don't know. We do like kitchen tools at Blue Apron. It kind of worked. With that, I wanted to thank everyone who helped me get this far and helped me ask the right questions, the iOS team and the folks who pointed me to some really helpful reads along the way.
If you have any ideas and you want to contribute, you can find me on the internet. I look forward to sharing the love of the green dot. Thanks.