TL;DR - I enjoy Swift overall but I wonder how Apple will keep from collapsing under it's own weight in it's lack of progress on SDKs and development tools relative to other platforms and tools.
Last year, I cofounded a startup and developed an iOS app full time using Swift 2.x and 3.x. While I had previously managed teams that had been building both Android and iOS apps, I had never built an iOS app myself, so I was excited to get my first iOS app into the AppStore!
See the previous post for a little more detail on my background with iOS and Apple.
This post is for developers who have curiosity about, and/or experience with, developing for Apple products. I will assume at least some exposure to terms like AutoLayout and Provisioning Profiles.
So rather than go into significant depth about everything, I'll provide an overview with examples in some key areas first and some bullet points for the rest. If anyone wants to talk more one-on-one or would like to see more details on an area, please contact me on Twitter (@tc_hype) and we can either discuss there or I could write additional posts if that helps.
All of my Apple-based development the last year has been for iOS apps using Xcode 7.x and 8.x using Swift 2.x and 3.x.
Storyboards, Xibs, and UI ToolingFrom my time managing teams developing iOS in the past, I remembered Interface Builder (IB) and .nib files. Now there are .xib files and Storyboards. How I think about them are:
- xib files: stand-alone UI component definition files that you use IB to define and manage visually.
- Storyboards: compose ViewControllers together using segues in a xib-file-like IB visual tool
I started my app with the single (default) Storyboard: Main.storyboard. I heavily used the tooling available in there: Segues between ViewControllers, ContainerViews to embed a ViewController into another, etc..
Common Issues I Ran Into
- Very often, I would forget to wire the component (e.g., a UIButton a TableView) on the Storyboard UI to the @IBOutlet or @IBAction defined on the UIViewController. This would cause no warning or error until runtime, when the code in the UIViewController subclass would try to access something on that outlet. Or, when I renamed the outlet in the code but forgot to in the Storyboard, it would crash on ViewController initialization. Yes, this is my issue, but I found that I have to actively remember to keep those in sync.
- Storyboards create a very hierarchical ownership chain and are very limiting. See the next section on Building Reusable UI Components for more.
- No built-in support for application Themes. As soon as I wanted to do that, Storyboards and xib files fell quickly out of favor with me. They hid property settings and made me define fonts, colors, and sizes in the UI/plist-format storyboard or xib files, and that definitely violates any DRY principals.
- There are many strings used for IDs: IDs to instantiate ViewControllers from within Storyboards, reuse IDs for cell definitions in storyboards or xib files, etc. There is no compiler check for this available. For Objective-C developers that are used to very dynamic coding styles, this may not seem like an issue. Heck, I love dynamic languages! But one of the benefits of Swift is the developer-time spent ensuring correctness up-front to eliminate errors and crashes on user devices.
Why Is This Bad?This has less to do with Bad vs. Good and more to do with the fact that Apple is one of the most valuable companies in the world, yet Microsoft and Google are decades ahead of Apple in these types of things (also see the Xcode section later). Apple isn't even close to state-of-the-art here.
Violating DRY principals isn't the worst sin one can commit. Sometimes, copy/paste reuse is the fastest, safest way to reuse code--but app themes are not that.
Themes and making IDs a compile-time check are not new concepts. Android has had them all along. Styles, strings, and UIs can all be defined in XML files. You can create composite styles for composite UI elements. And all of that can be referenced in code because the compiler generates the Java files with these constants defined in code as well.
Windows development has had some of this since the old WinForms days of .NET. Even with WPF (Windows Presentation Foundation) that uses XAML definition files, a similar approach is used. You define UI in an XML dialect using editors -- or by hand -- and that simply is a way to define the view object hierarchy, the style dictionaries, and set the initial values on those objects declaratively. This all can also generate designer files, which are C# files that have partial class definitions that are tool-generated code from designer tools. You are not meant to hand-modify these files, but you instead modify the main .cs file for a class and the compiler tooling combines the tool-generated designer partial class definition with the hand-crafted parts of the class definitions into a complete definition.
HTML and CSS are well understood and I won't talk about them here, but they too are better suited to hand editing and tooling support.
Isn't It Just Six-of-One, A Half-Dozen of the Other?I bring up designer/code-gen files and updating UI markup by hand for several reasons:
- The IB tools seem to set default values on objects that are not the default values in the class definitions. It became clear through my working through issues as I had to transition some things from Storyboards to code-based UI that there were "mystery settings" that I could not find through searching the code base for where those were getting changed, making debugging frustrating and hard.
- There are keys in the storyboard and xib "plist" files that are somehow generated by the IB tools and have something to do with hashing something about those elements. This mystery makes creating xib or storyboard file generation tools for common use-cases impossible. It also makes using alternative tools like AppCode by JetBrains difficult if not impossible. Somehow, Xamarin has done this lately...
- The more you put in storyboards, the longer your compilation takes, the more often you need to compile, and your errors and warnings are not reliable until the full linking is complete. This is not the case when using code to define UIs. (Plus, the issues with outlets and actions wiring I mentioned earlier).
- UIs driven by dynamic data do not represent what they will actually look like in with multiple examples of data in the designer tools. Most of this is probably due to UIKit and lack of flow-based layout though. However, the Microsoft Blend tool (using WPF/Silverlight/XAML) used to allow data binding in the designer so that the professional Designer could see what would happen in various test cases and common scenarios with their layout decisions. This is always better than a Photoshop or Illustrator mockup that always has ideal data without variable length sections of text and only beautiful people's avatars showing.
- Editing requires a full recompile, link, and deploy process. While this is true for all changes (not just UI), it's a huge productivity issue. Boy, do I miss Chrome/Safari dev tools and hot reloading or Command-R refresh. Android Studio recently added quick reloading and React Native and Flutter provide this too.
- There continues to be an IB/Xcode bug where a sufficiently complex Storyboard can cause a full lock-up-rainbow-spinner of Xcode when it is open and Xcode is on an external monitor. I even broke up my app into multiple Storyboards per product-area (e.g., Login, Settings, Users, Businesses, etc.) to try to eliminate the problem. It would still come back on some storyboards. Breaking up the storyboards also meant I could no longer use built-in segues, so I had to instantiate, present and push view controllers in my own code anyway. Without the ability to modify these files entirely by hand or in another editor, I'm limited in my ability to use "designer features" and I'm also stuck with this long-standing bug and I have to kill my neck and ergonomics staring at my laptop screen to design a UI where I could use a full 27" monitor.
When doing active new product development--especially when creating a new product where speed and flexibility on UI elements and themes is critical to be able to test many ideas--the limitations and issues described above only make using the UI tooling a limiting factor rather than a productivity gain.
What I Do CurrentlyAs a result of the things above (and some of the below), I now take the following approaches:
- Write as much UI in code as possible. The mystery goes away, all of the property settings are explicit, and code files are more easily merged when there are conflicts, and no lockup issues on external monitors. Searching for where properties are set is much more straightforward.
- Define my own theme classes with static constants and methods: Fonts, Colors, Storyboards, and ViewControllers can all be referenced or instantiated through simple code like Fonts.mainHeader, Colors.darkThemeTitle, or ViewControllers.newBusinessDetails(business: businessId, allowComments: true)
- Anything I repeat enough, I define static helper methods, class extensions, or (last resort) new subclasses. I do not want "UI Frameworks" on top of the Apple-provided ones. It only increases my risks and decreases my ability to fix things when they break or I run into limitations. What I want instead are composable additions to, and helper methods for the built-in, SDKs that simplify common use cases, so that is the approach I did my best to take. When I do it well, these become great code shortcuts that are composable, reusable, and theme-able features and components.
Building Reusable UI Components and UIKitStoryboards contain ViewControllers, ViewControllers contain UIViews such as UITableView and UICollectionView elements. Those UITableView and UICollectionView items would then contain UITableViewCell or UICollectionViewCell layouts. Each element contained its own property values that were set either by default or by the UI of the Storyboard editor.
As soon as I wanted to reuse a component--either a base component like a layout of a cell, or a composite component like a "search box with collection view of users within the application" that could be reused in multiple contexts in the app, I found I had to switch to .xib files or writing code-only components.
Once I did that, I also found that proper use of NotificationCenter and Delegates (see below) really helped in making my components reusable in new contexts as well.
Challenges of Creating Reusable And Flexible UI ComponentsUnderstanding AutoLayout was the first challenge, but a worthwhile one. It's great. That was not a problem (see the section on AutoLayout).
Understanding how much I needed to override when creating a custom component was difficult and makes estimating work pretty much useless. See the section on UIKit, Defaults, and Backwards Compatibility.
Examples are probably best. Here are a few brief examples.
Example #1 - a button with text on the left and an icon on the right that can gracefully handle being different sizes depending on context:
UIButton supports an image or text by default. You can also do both with some playing with contentInsets, but only if you want the image on the left and the text on the right.
In order to get the text on the left and the image on the right, I had to use the same trick you would use to reverse the words of a string: first, you reverse the entire string, but then you are left with the sentence backwards, so you reverse each word in the string to get each word back to being forward, but the words are now in reverse order. Similarly with the button, I had to:
- Use title and image contentInsets to make it look good with the image on the left.
- Then, I had to use Core Graphics (CGAffineTransform(scaleX: -1.0, y: 1.0)) to first "flip" the content of the button (the text and image) horizontally so they are reversed . This leaves the button with the image on the right, but the image and text are backwards. Then, I had to reverse the internal UILabel and the UIImageView components each independently to make each of them stay in the right place but then be forwards instead of backwards (just like the words in a sentence).
I wrote a UIButton extension method called putImageOnRight that encapsulates this trick into a method that appears to be built right into UIButton.
Example #2 - Rounding corners and making profile pictures circles:
Another common need is rounding corners of UI elements. Like CSS border radius, you can set a cornerRadius on the layer of a UIView to make rounded corners. Making a profile image be a circle involves setting the layer of the UIImageView to have a cornerRadius that is half the entire width or height (having a square UIImageView is key). A common issue is setting these cornerRadius properties but then it doesn't seem to work. It turns out that the default behavior of a view is to not clip the contents of a UIView to the view's bounds. To avoid this duplicate code and common mistake of forgetting to set the views' clipsToBounds = true, I created UIView extension methods called makeRounded(_ radius: CGFloat) and makeCircle().
As an aside, makeCircle just checks to ensure the UIView's frame's height and width are within some small tolerance of each other and then calls makeRounded(view.frame.width / 2.0).
Example #3 - Making a UILabel support top and bottom vertical alignment:
UILabel does not support vertical text alignment other than center. In order to support that, you have to subclass UILabel and override the methods that determine what the rectangle for the text should be within the bounds of the UILabel's frame. Yes, there were samples online, but those were insufficient as they broke the built-in horizontal alignment logic for UILabel as well. I had to write my own using a combination of what I found online and my own sleuthing to fix issues.
Subclassing Is (Can Be) The DevilIn general, I hate creating sublcasses. Inheritence is basically composition that breaks encapsulation (see Gang of Four Design Patterns):
The authors discuss the tension between inheritance and encapsulation at length and state that in their experience, designers overuse inheritance (Gang of Four 1995:20). The danger is stated as follows:
- "Because inheritance exposes a subclass to details of its parent's implementation, it's often said that 'inheritance breaks encapsulation'". (Gang of Four 1995:19)
They warn that the implementation of a subclass can become so bound up with the implementation of its parent class that any change in the parent's implementation will force the subclass to change. Furthermore, they claim that a way to avoid this is to inherit only from abstract classes—but then, they point out that there is minimal code reuse.
Using inheritance is recommended mainly when adding to the functionality of existing components, reusing most of the old code and adding relatively small amounts of new code.
To the authors, 'delegation' is an extreme form of object composition that can always be used to replace inheritance. Delegation involves two objects: a 'sender' passes itself to a 'delegate' to let the delegate refer to the sender. Thus the link between two parts of a system are established only at runtime, not at compile-time. The Callback article has more information about delegation.
Notice the use of Delegation -- I'll talk about this in an upcoming section.
My Summary of Reusable Components With UIKitMy overall issues here are not whether customizations can be done or can't be done--they can. The issues I have are:
- UIKit has not been updated to provide convenience methods or facade APIs to make things people commonly do simple, so we all write our own extension methods and subclasses to overcome this.
- Making my app components only "slightly different" seems to require me to basically reimplement existing functionality in UIKit myself just so I can slightly change things way too often.
This is not sufficient in my opinion, because these frameworks don't encourage genericism and reusability and extensibility at the right levels, so many of the items posted online are really only useful for the specific person's problem for their specific use case. I also believe this could be a historical and cultural issue since the Apple development community started out small and in a culture of secrecy from Apple and the wins were hard-won (no facts or citations, just my impression). This is the opposite of Microsoft and Google SDKs.
- AutoLayout is a great way to do layout
- Interface Builder warnings are (somewhat) helpful
- IB tools for setting AutoLayout constraints are pretty good
- Once you learn how to set constraints in code, its easier to see explicitly how the constraints and UIViews all relate to each other
- Once you learn how to set constraints in code, you can write code to make it easier and reusable.
- It was clearly built by people who understand how to solve for n variables (where n is more than one) using n-1 equations that relate constraints to each other.
- AutoLayout is really hard to understand at first
- VisualLayoutFormat seems like a good idea and a cool shortcut to defining constraints, but actual errors (not unintended results) with it are only discovered at runtime and will break your application
- Defining constraints in code requires you to remember to set a view's translatesAutoresizingMaskIntoConstraints to false, or the frame of the view (position and size of view) set using older techniques prior to AutoLayout will create additional and potentially conflicting constraints that you can't discover until runtime.
I suspect the last bullet item has to do with UIKit Backwards Compatibility prior to Apple pushing for flexible layouts.
UIKit, Defaults, and Backwards CompatibilityWhile I have come to understand that UIKit is a big improvement over AppKit, UIKit feels to me like a Legacy UI framework that has been patched over and added to organically over time. This results in some obvious baggage that Apple could eliminate by doing what they did with things like the AddressBook vs. Contacts frameworks by providing a more modern UIKit, even if all it does is wrap the current UIKit in more common-sense, modern approaches.
People who have done only or mostly iOS or Mac development over the years may not have the exposure to what the state of the art has been, especially from Microsoft, Google, and JetBrains.
Here a few examples of things I find disappointing or frustrating, even though I can understand why they are that way:
- Flow Layouts - for the love of all that is holy, make EVERYTHING a flow-based layout with dynamic sizing based on the contents. We can always set maximums, minimums, or explicit sizes when we need them! StackViews don't count. They don't do what we need them to. If I have to write String#boundingRectWithSize or use a TableView to simulate flow-based layouts one more time...
- Resizing Masks as Constraints - as mentioned above, UIView constructors want to be told the size of the view. AutoLayout constraints determine those depending on what else is going on in the screen. These two things don't match, but the layout engine needs to become the owner of layout. Probably as a transitional tool, the default for UIViews is to allow their Resizing Masks (whether they can have flexible width, flexible height, both, or none) to generate the equivalent behavior in constraints. As mentioned earlier, I default to code-based constraints, and if I forget to set translatesAutoresizingMaskIntoConstraints to false every time I end up not seeing my view, seeing it in weird places, and/or getting an exception that there is a layout constraint conflict. Every time. I must set it Every Time. Ugh.
- UIViews Do Not Clip to Their Bounds By Default - I do not understand why we have to set clipsToBounds to true. This seems like the wrong default value.
Delegates and (NS)NotificationsAs mentioned above, the Delegation pattern is used widely to enable reuse of components and customization of their behavior. I didn't get why this was necessary at first because it's kind of a weird way to do it. That being said, I make heavy use of delegates (and protocols to support them) in my own components. Good stuff!!
The only downside to the delegate pattern is that you have to have one delegate that implements the protocol, even if the logic needed by the delegate really needs to be provided from multiple places. You can combat that through composition, creating a "wrapper class" that implements the protocol and uses other classes to do the work.
Delegates are a good implementation of the Delegate design pattern. NSNotifications are a good representation of the traditional Observer pattern, albeit a more "loosely-coupled, dynamic language way" of doing it.
Where a delegate on a component is really about providing functionality, data, and actively influencing the behavior of that component from another object, NSNotifications (Notifications in Swift) are used broadcast events and event data to zero-or-more interested "listeners".
Java has had this for years, although in a more Swift-like, interface (protocol) based way that can be checked at compile time for compliance. I believe the .NET Common Language Runtime, BaseClass Library, and C# compilers have the best way of handling both scenarios.
Example: Imagine a UIScrollView that you want to handle the user hitting the top or the bottom so you can show some additional feedback to the user.
In UIKit, you would have your UIViewController implement the UIScrollViewDelegate protocol. In there, you would have to handle one or more delegate methods on that protocol to react to that change. Luckily, some methods are optional so you don't have to implement all of them on your UIViewController, but the ones you care about all have to be the same instance of a class that implements the protocol.
In Java, the protocol is called an Interface, and you have to create a class (either an explicit class or an anonymous inner class) that implements that interface and all of it's methods. You would just provide no/default implementations of the ones you don't need right now. Again, a single class has to implement the entire interface. Depending on the implementation for that behavior, the author may only allow ONE listener (like a Delegate) or allow multiple subscribers (like NSNotifications).
In both of these cases, this is the traditional Observer pattern -- the Subject is the UIScrollView and the Observer is the class that implements the interface/protocol and subscribes to notifications from the Subject. The Subject (UIScrollView) is responsible for managing registration and deregistration of Observers, and the interface or protocol needs to cover all things a potential delegate or listener could need to care about for that Subject.
In .NET/C#, there is a class built in called a Delegate class. It basically is a reference to a function with a certain signature (so it's type safe). Then, there is a MulticastDelegate class built in that is essentially a generic "Subject" of the Observer pattern: it handles adding and removing listeners, and when you "notify" on the MulticastDelegate, it takes care of notifying the Observers (functions) in order.
.NET Components tend to have Events declared on them. An event could be, Clicked for a button, or BeforeDraw for a UI element, or PropertyChanged for when a property on an object changes values (kind of like KVO in iOS). Behind the scenes, each event compiles into a MulticastDelegate with a function signature that is relevant for observers of that specfic event.
This is unique because a class or component can have MANY events on them, and since each event manages it's own observers, and each event has only one function signature, you can compose and separate event observers into many different classes, all in one class, and/or simply ignore events you don't care about even if you care about some.
This way of handling things means the EVENT is the Subject, not the component or class--and handling adding and removing listeners and notifying them is all code that is built-in and no one has to write. It also means that events can be used for both delegates and notifications. Pretty cool!
Xcode and iOS SimulatorsXcode is not the worst IDE I have ever used, and it is improving, but it's far from really good. Visual Studio 2005 (12 years ago) was much, much better than Xcode is now. AppCode for Objective-C and Swift (and any of the IntelliJ-based IDEs JetBrains has produced) are much better. Hell, even AppCode has Refactoring tools for Swift--Xcode does not.
Xcode is too tightly coupled with iOS versions. It sucked when I had to upgrade not only Xcode, but my Mac to Sierra when I got a new version of iOS on my phone over the air.
Simulators. Why Simulators? Android and Windows have full-on emulators that run virtual machines that run the full OS, not a simulated OS. The simulator does not simulate processor speed or network responsiveness well.
And why the hell does the simulator have all these rendering artifacts on external monitors? Dumb, dumb, dumb. Yet again I have to use my small laptop screen or plug in my device all day to get a decent approximation of what my app will look like on a real device.
Provisioning Profiles and (User/Remote) NotificationsThese are hard. Xcode has an option to manage these now, but I have yet to see it work totally right. I get that Apple gets to decide on what runs on our devices, but man. It's kind of obtuse. I don't remember Android development being so hard with signing and so forth.
Also, using APNs should not be as hard to get working the first time as it is. Come on, Apple.
Compile Times, iTunesConnect, TestFlight, AppReview, and RadarFeedback timeliness is paramount to developer productivity. Cool things like "you should make that var a let or that let a var" are actually really awesome for ensuring intent-matches-code, and that quick feedback is great.
Almost all other developer feedback items are painfully slow compared to other platforms:
- Compile Times for Swift are currently measured in minutes, especially after doing a Clean or deleting your DerivedData
- The fact that I have to Clean my project or delete my DerivedData at all
- Simple property updates on UIView elements are not immediately reflected in the Simulator or on the debugging device
- Building an Archive takes many, many minutes
- Publishing an archive that was already built seems to "re-archive" or "update the archive" for like 2x the time as the original archive. What the hell is it doing?
- Uploading the second archive takes way too long is is pretty buggy
- iTunes connect UI is a dumpster fire
- Fundamental signing, capabilities, or entitlement issues are not known until the package is uploaded, when they clearly could be identified locally
- Did I mention that iTunes Connect UI is a dumpster fire?
- Why, when Apple rejects my app for some text on the screen that came from an external system, do I have to upload a new binary, taking me another full hour before I can even submit the app for review again when the issue can be fixed in 5 seconds?
- Until my app goes to TestFlight or app review, why can't I cancel the processing of my earlier binary and reuse the build number from before to let me fix a small issue I found during the hour I was waiting for my new app build to be available?
- I do not want to use TestFlight the way you want me to use it. Stop enforcing a development workflow on me. Bring back TestFlight as a separate thing!!
- I submitted what I perceive to be a serious, easily reproducable issue having to do with Always vs. When In Use access to location information in a Radar. I waited two months to get back, "Please provide a sample app reproducing the issue". I clearly spelled out that any app with both prompt settings in their Info.plist file will ask the user for Always access, even if the code is only requesting When In Use access (and MAY ask for Always access later, when it could be neccessary). All the repro steps and simple settings were in there. Why do I have to do more work for something so simple for an Engineer to test on their part? Do they not care if this is a real issue?
My Closing Thoughts
I have concerns. Yes, Swift is cool. Yes it is a great direction. Yes Xcode is improving. Yes, app reviews are down to a couple of days. All good. But all not good enough.
Where are the developer evangelists? Where are the MANY conferences throughout the year? Where are the investments in making a package manager work with Xcode well? Where are the new frameworks that make common things easy?
Apple is killing it. They should be investing more in their developer division (if there is one) and in the hardware and software that drives it. They need to take keeping developers happy seriously. The only reason they have gotten away with not doing that thus far is because the users are in iOS and we need users to use our apps. But that could be changing back if Apple doesn't keep up in conversational assistants, AR, and server-side services that work reliably.
As my previous post pointed out, along with the feedback I have received has shown, many of us fear putting all of our trust into one company that seems to not be placing high priorities on supporting our (developer) use-cases and needs. We need fast macs with long battery life and the development tools to support external monitors. We need ergonomic, physical keyboards and full-size arrow keys to navigate our code without looking down at our hands and breaking our flow. We need modern development, tools with fast (read: instant) feedback loops that aren't burdened by a C-based compiler and linker chain during development. We need real emulators. We need more intuitive frameworks from Apple that do the modern things by default and don't carry around baggage from iOS 2 or AppKit. We need Apple to level-up and come into the 21st century for it's developer tooling.
Just like with Objective-C and now Swift, these advancements take years (decades?) and Apple is at least 10 years behind Microsoft, Google, and JetBrains with much of their developer tooling, frameworks, and supporting evangelists and communities. That's not hyperbole. It's fact. Swift doesn't fully solve the Copeland 2010 risks.
Add to that their lack of updating the Mac Pro, Mac Mini, and trying to push towards "tablets for everything" and I feel like I'm falling behind in terms of keeping the tools I use fresh and worth the thousands of dollars I spend on them.
To summarize, my experiences with Swift have been good overall; It's a good language. However, everything else about developing for iOS (along with the lack of Mac improvements) made me want to go back to using Android and Google as my primary platforms (and I even considered Windows and Windows Phone). I want to be in an open ecosystem that supports developers, has strong server-based services, and has a proven record of investing in developers' livelihoods. But I also value Apple's tastes and tradeoffs as a consumer and use their devices. I just wish I felt supported, valued, and not taken advantage of in my developer life in return.