What a better way to start 2021 than reflecting on one of my main goals for 2020: learning SwiftUI and building my first app.
While I was, and still am, just a beginner in the iOS development world, I felt that the best way to learn would be to build an app from scratch. I had this idea of a simple yet beautiful weather app, which seemed pretty straightforward to build. However, a lot of things didn't go as planned š . Whether it was lack of planning, too high expectations, or just the developer experience itself, the closer I was getting to finishing this project, the less confident I became about my app being worth releasing on the App Store. So we can call this project a failure if you want, but I'm nonetheless still pretty proud of what I ended up building!
Thus I felt a lookback at this whole experience would be an interesting topic for a blog post. Let's take a look at my first SwiftUI app, what I built, some of the challenges I faced that made me learn a lot, and why I failed to finalize this project.
Introducing Sunshine
I built Sunshine, my weather app, during the Summer and Fall of 2020. If you're following me on Twitter, you might have seen quite a few screenshots, video recordings showcasing how the app evolved throughout its development. For those who did not get the chance to see them, here is a little screen recording for you, showcasing what I built:
My goal was to build a simple and beautiful weather app, with "home-made" assets that would animate on the screen based on the weather at a given location.
What makes it unique compared to other apps was the asset I built (sun, clouds, rain), the focus on the UX, and the little animations sprinkled across the interface. Although challenging, I tried to stand by these principles from the prototyping phase and throughout the development of this app.
The app has three main screens, each of them having a simple role yet featuring little details for a polished look:
Main Screen
The main screen features the name of the location, the date, and one of the most complex SwiftUI View I built for this app: the Weather Card.
This card is central to the UX. It displays all the information about the current weather conditions at a glance such as:
- Temperature
- Weather description
- Other metrics: wind speed, humidity, etc.
- The sun's position throughout the day
- Sunrise and sunset time
- An animated representation of the weather: the sun rising, clouds sliding from the sides of the card, etc
The color of the card also adapts based on both the weather conditions and the time of the day. You will get a blue gradient at midday and a more orange pastel gradient at dawn, a more grayish color when the weather is cloudy, etc.
Forecast Panel
Sliding the bottom panel up reveals the Forecast Panel. I felt it was a good idea to hide the complexity of this panel away from the main screen while still keeping the user "in context" within the main screen when it's displayed.
On this screen you can see both:
- The hourly forecast for the next 6 hours
- The daily forecast for the next 7 days
Each card will display the temperature, and the weather conditions are reflected through the combination of an icon and a background gradient, just like the weather card on the main screen.
Settings Panel
Tapping the menu icon on the top left corner brings the Settings Panel. This is where you can manage some settings and also the list of locations.
While the Sunshine feels somewhat simple from what we've just seen, it presented its own set of challenges and setbacks during the development... which was great! š These challenges allowed me to learn so much more than I would have had by solely focusing on mini-projects around a specific aspect of SwiftUI, so if you ask me now, all that frustration was worth it!
Challenges, setbacks, and what I learned along the way
Building a whole SwiftUI app from scratch can feel a bit overwhelming. I mostly proceeded as I'd usually do on any complex project: one feature at a time, baby steps, breaking down any problem into smaller achievable tasks.
There were, however, a few problems that showed up along the development of particularly challenging features. Here's the list of interesting ones I handpicked:
TabView with PageTabViewStyle
I used the following code snippet to implement a simple TabView with pages that could be swiped left/right:
Initial implementation of TabView with PageTabViewStyle used in Sunshine
1import SwiftUI23struct MainView: View {4var city: String56var body: some View {7VStack {8Text("\(city)")9}.onAppear {10print("Appear!")11print("Call API to fetch weather data")12fetchWeatherData(city)13}14}15}1617struct ContentView: View {18@State private var selected = 019var body: some View {20VStack {21TabView(selection: $selected) {22MainView(city: "New York").tag(0)23MainView(city: "San Francisco").tag(1)24}25.tabViewStyle(PageTabViewStyle())26}27}28}
In my case, I wanted this TabView component to do the following:
- each "page" would show the weather at a given location
- swiping to another page would show the weather to the previous/following location
- when done swiping, i.e. the index of the current page displayed changes, I'd use the
onAppear
modifier to detect that the page is visible and make an API call to fetch the weather data of the location currently in view.
The entire app was architected around these few lines and the idea of pages, and it worked... until iOS 14.2 š¤¦āāļø. If you copy the code above and try it out today, you'll see the onAppear
being called multiple times instead of just once! I reported this issue to the SwiftUI community on Reddit and it sadly looks like every iOS dev is kind of accustomed to this kind of things happening. This is not very reassuring I know..., and many developers share this frustration:
Upgrading OS, even minor, break your app? That's insane. Clicking a button doesn't work because my user upgrade iOS 13 to iOS 14. My app also crash because I use opacity of 0 when upgraded to BigSur. -- Philip Young, creator of Session
As someone working primarily on the web, I'm not used at all to this kind of issues. This didn't even cross my mind that it could be a possibility when starting this project.
The fix? Instead of handling whether a view within a TabView "appears", I'd move the state of the index in an "observable" and trigger my API call whenever a change in the index has been observed:
Latest implementation of TabView with PageTabViewStyle used in Sunshine
1import SwiftUI23class PageViewModel: ObservableObject {4/*5Every time selectTabIndex changes, it will notify the6consuming SwiftUI view which in return will update7*/8@Published var selectTabIndex = 09}1011struct MainView: View {12var city: String1314var body: some View {15VStack {16Text("\(city)")17}.onAppear {18print("Appear!")19}20}21}2223struct ContentView: View {24@StateObject var vm = PageViewModel()2526var cities: [String] {27return ["New York", "San Francisco"]28}2930var body: some View {31return VStack {32/*33We keep track of the current tab index through vm.selectTabIndex.34Here we do a Two Way binding with $ because we're not only reading35the value of selectTabIndex, we're also updating it when the page36changes37*/38TabView(selection: $vm.selectTabIndex) {39MainView(city: cities[0]).tag(0)40MainView(city: cities[1]).tag(1)41}42.onReceive(vm.$selectTabIndex, perform: { idx in43// Whenever selectTabIndex changes, the following will be executed44print("PageView :: body :: onReceive" + idx.description)45print("Call API to fetch weather data")46fetchWeatherData(cities[idx])47})48.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))49}50}51}
Because of this issue, the app ended up in an half broken state on iOS 14.2, and I had so much refactoring to do that I ended up restarting the development of Sunshine almost from scratch š¬.
Using MapKit to build a location service
One of the things that can feel weird when one gets started with iOS development is knowing that SwiftUI is, to this day, still "incomplete". Many core APIs are still not available to SwiftUI and the only way to interact with those is to do it through UIKit. One of those API I had to work with was MapKit.
Sunshine needed a simple "Location Service" to search for cities and getting their corresponding lat/long coordinates. For that, I needed to use MapKit, and that's where things got rather complicated:
- Using anything MapKit related felt less "Swift" and I wasn't the most comfortable with UIKit
- There were very few MapKit related resources or blog posts besides the Apple Documentation
The hardest part was actually to know the right keywords to search for. What I needed to use was a combination of:
MKSearchCompleter
: a MapKit utility to output a list of locations based on a partial string: i.e. passing "New" would output, "New York", "New Jersey"MKLocalSearch
: a MapKit utility with all the tools to do points of interest search: this is what I used to get the coordinates associated with a given MKSearchCompleter result.
Knowing that these were the MapKit utility functions I needed to use to build my "Location Service" took a lot of time digging through the documentation. This can be a bit frustrating at the beginning, especially as a frontend developer where I'm used to "Google my way" through a problem or an unknown.
In case anyone has to build that kind of "Location Service", you'll find the code just below. I added some comments to explain as much I could in a small format, but I might write a dedicated blog post about this in the future:
Implementation of a Location Service to search for cities and get their coordinates
1import Foundation2import SwiftUI3import MapKit4import Combine56// The following allows us to get a list of locations based on a partial string7class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {8/*9By using ObservableObject we're letting know any consummer of the LocationSearchService10of any updates in searchQuery or completions (i.e. whenever we get results).11*/12// Here we store the search query that the user types in the search bar13@Published var searchQuery = ""14// Here we store the completions which are the results of the search15@Published var completions: [MKLocalSearchCompletion] = []1617var completer: MKLocalSearchCompleter18var cancellable: AnyCancellable?1920override init() {21completer = MKLocalSearchCompleter()22super.init()23// Here we assign the search query to the MKLocalSearchCompleter24cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer)25completer.delegate = self26completer.resultTypes = .address27}2829/*30Every MKLocalSearchCompleterDelegate let's you specify a completer function.31Here we use it to set the results to empty in case the search query is empty32or in case there's an uknown error33*/34func completer(_ completer: MKLocalSearchCompleter, didFailWithError: Error) {35self.completions = []36}3738/*39Every MKLocalSearchCompleterDelegate let's you specify a completerDidUpdateResults function.40Here we use it to update the "completions" array whenever results from the MapKit API are returned41for a given search query.4243These results can be filtered at will, here I did not do any extra filtering to keep things simple.44*/45func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {46self.completions = completer.results47}48}4950extension MKLocalSearchCompletion: Identifiable {}5152// Example of LocationSearchService consummer5354struct ContentView: View {55@ObservedObject var locationSearchService: LocationSearchService5657var body: some View {58// Typing in this field will update the search query property in the LocationSearchService59TextField("type something...", text: $locationSearchService.searchQuery)60}.onChange(of: locationSearchService.completions) {61/*62Since searchQuery is changed, the LocationSearchService will update63the completions array with results.6465Here we'll perform the getCoordinatesLocation on the first element in66the list of results.67*/68getCoordinatesLocation(locationSearchService.completions[0])69}7071func getCoordinatesLocation(_ completion: MKLocalSearchCompletion) {72// We initiate a MKLocalSearch.Request with the completion passed as argument of the function73let searchRequest = MKLocalSearch.Request(completion: completion)74// We define and trigger the search75let search = MKLocalSearch(request: searchRequest)7677search.start { response, error in78/*79In this callback we can get the response of the search request,80which contains the coordinates of the completion passed as arguments81*/82guard let coordinates = response?.mapItems[0].placemark.coordinate else {83return84}8586guard let name = response?.mapItems[0].name else {87return88}8990print(name)91print(coordinates)9293/*94In Sunshine, I'd save the name and the coordinates and used both95of these to retrieve the weather data of a given location96*/97}98}99}
User Default vs Core Data
SwiftUI provides a system called UserDefaults
to store user preferences, very similar to LocalStorage
on the web. It's simple and straightforward to integrate into an existing codebase:
Small example showcasing how to use UserDefaults
1let defaults = UserDefaults.standard2defaults.set("celsius", forKey: "temperature")3defaults.set("mph", forKey: "speed")
I planned on using UserDefaults
to save some user preferences: which unit between Kelvin, Celsius, or Fahrenheit the user wanted to use to display the temperature and also the lists of "locations".
That's where I hit a wall š¤... I didn't carefully read the documentation about UserDefaults
: you can't save custom types to this system (at least out of the box) and in my case my "locations" were defined as a custom type:
Location type used in Sunshine
1struct Location {2var name: String3var lat: Double4var lng: Double5}
The only way to go forward was to use CoreData, another system that helps saving data that are defined with more complex types. However, integrating CoreData mid-way through a project seemed extremely complicated, so I simply decided to restart a whole new XCode Project, with CoreData enabled this time, and copy over the code š . Total lack of planning on my end.
Failing to materialize the project
The screenshots and video recordings from the first part and the details I gave about the problems I faced and eventually solved in the second part might leave you wondering why the app didn't end up being released.
The answer to that is that I simply stopped working on it. I have a few reasons why, and this part focuses on the main ones.
I bit more than I could chew
Let's start with the obvious one, that I realized half-way through the development of the app: it was a bit too ambitious for a first project. One could build a very simple weather app, but the vision I had for mine was a bit more complex and tricky. I built lots of custom Views, had to integrate some UIKit utilities, make API calls, and tons of animations.
Maybe my first app should have been a bit simpler, like a single view app focused solely on UX (which initially was what I wanted to focus on the most anyway).
Commitment
The "iOS 14.2 update incident" that broke my app left a bad taste in my mouth. It made me reconsider the commitment that one must put in an iOS project.
A simple iOS update can easily break your app, especially SwiftUI-based, to a point where it can be completely unusable. The only way to avoid this as an iOS developer is to test your app on every iOS betas as soon as they get released. If I were to fully commit to this project I'd be in a perpetual race with Apple's update cycle and could not afford to miss an update at the risk of getting bad ratings or letting down my users.
This is not something I usually have to worry about when working on a web-based project.
On top of that releasing a patch or a new version of an iOS app is significantly slower and more complex than patching your web app: No third-party company reviews your website or SaaS when you update it. You just patch the issues, run your deploy scripts, and done! For iOS apps, you have to go through the App Store review process which can take a significant amount of time. I did not take all of these elements into account when starting this project.
This is not a critic of the Apple Ecosystem, far from that. I'm pretty sure these drawbacks would have easily been minimized would my project be less complex.
The result did not meet expectations
While Sunshine may look great on the video recordings and screenshots, in reality it's a different story.
The app ended up feeling sluggish at times. Swiping pages randomly drops frames, even if I disable all the animations or hide complex views. There are a few memory leaks that I tried my best to track down. However, after weeks of investigation, and no progress made, I simply gave up.
Are the underlying reasons linked to SwiftUI itself? Or the way I use it? I still have no way to know. SwiftUI is still in its infancy, and while Apple is extremely invested in it, it still feels it's not quite there yet in some specific areas at times.
That last bit was quite discouraging after all this work. It is probably is the main reason why I completely stopped working on Sunshine and why it's stuck in an unfinished state. The result was simply not on a par with what I originally envisioned and wanted to release.
On top of that, drawing my own assets was way more time consumming that I thought it would be. There were too many weather types to handle, and I was not able to provide a satisfying result for some of them with my current Figma skills.
Cost
Probably the least important reason, but worth mentioning still. I used Open Weather Map's One Call API to provide accurate weather data. They have a decent free tier that's perfect for development. However, I'd quickly exceed the limit of calls per hour/day if I were to release it.
The next tier is $40/month, which I can afford without a problem, the next one though is $180/month which made me think a bit more: Was I serious enough about this project to start spending a significant amount of money to run it over time or was this just for fun?
Conclusion
Despite all the setbacks and the looming "doom" of this project, I still had tons of fun! I loved sharing my journey and my solutions to the little problems encountered along the way with all of you on Twitter. Seeing this app slowly take shape was incredibly satisfying. I feel confident that the lessons learned here will be tremendously helpful and guarantee the success of my future SwiftUI projects.
This project also helped me realized how lucky we frontend/web developers are. The speed at which we can develop an idea from a prototype to a product, the tooling, and the community we have is something to be cherished.
Nonetheless, I will still continue to build stuff with SwiftUI. My next project will probably be very simple, like the ones I mentioned in the previous part, or maybe just a series of bite-sized apps/experiments like @jsngr does so well. This was my first failed SwiftUI project, it probably won't be the last. There's still a lot to learn and a lot of fun to have building stuff.
Want to checkout more of my SwiftUI related content?
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
ā Maxime
A reflection on my experience building my first SwiftUI app, what I learned, the challenges I faced, and the reasons that made me not release it.