As an iOS engineer, for the most time in my career, I have been “transforming JSONs into beautiful UI”. Compared to backend development, handling large amounts of data and doing performance optimizations are not a typical part of our work. Of course, from time to time, performance does matter — especially to make the UI smooth, but the techniques are often different — such as reusing views or offloading expensive work from the main thread. Additionally, if we want the client to be thin, most of the heavy job is delegated to the server, for example, content ranking, search, filtering and so on.
However, sometimes you still have to perform some expensive operations on the client side — for example, because for privacy reasons you don’t want some local data to leave the device. It’s easy to accidentally make those parts of code extremely inefficient — especially if you haven’t built this muscle of quickly spotting potential complexity issues yet. Algorithms and data structures do matter — this is something I only truly realized only several years into my mobile career, and I still see this thing to be often overlooked in the industry. Of course, early optimization is not needed and may even do harm (see premature optimization), but even basic calculations can become performance bottlenecks that severely harm user experience.
There is only one way to solve it — embrace the basics — which means using appropriate algorithms and data structures for the task at hand. One real example that I always recall is a thing I built for one of my projects many years ago. For an invitation flow, I had to implement a contact merge feature where the data would come from three different sources — backend, social account and local iPhone address book. We wanted to combine contacts from these sources into one if they had any overlapping channels (phone numbers or emails). The result would be an array of contacts with all their channels, so there would be no duplicate channels for two different contacts.
At first, my naive approach was just to go one by one and see if in the remainder of the list any contact has overlapping channels with the current one, merge them if yes, repeat. This was needed because, for example, the last channel in the list could have two channels — one that would overlap with the current one, and the second one that could have appeared in the previous contacts which would mean having to go through the list again.
I implemented this, and it worked pretty reliably, here is the pseudocode:
func slowReliableSmartMerge(contacts: [Contact]) -> [Contact] {
var mergedContacts = contacts
var results = [Contact]()
var merged = true
while merged {
merged = false
results.removeAll()
while !mergedContacts.isEmpty {
var commonContact = mergedContacts.first!
let restContacts = mergedContacts.dropFirst()
mergedContacts.removeAll()
for contact in restContacts {
if contact.hasNoOverlappingChannels(with: commonContact) {
mergedContacts.append(contact)
} else {
merged = true
commonContact = Contact.mergedContactFrom(contact: commonContact, otherContact: contact)
}
}
results.append(commonContact)
}
mergedContacts = results
}
return mergedContacts
}
An experienced engineer would quickly spot the issue here, but plese bear with me for a minute. I tested this on my device which had roughly 150 local contacts, 100 friends on social media, and a couple dozen users from the server. It would finish in just a couple of seconds after showing a spinner — “not a huge deal” I thought and moved on to the next feature. Test devices had much fewer contacts, so it worked instantly there. Then a couple of weeks later we started getting some reports from the users that this spinner can take a minute or even longer. Suddenly I realized that the issue was related to complexity, and then I figured that the approach I had taken could actually hit the O(n^2) complexity — similar to the bubble sort.
I quickly discussed that with another engineer on a whiteboard, and we came up with hashmaps to optimize this significantly:
func smartMerge(contacts: [Contact]) -> [Contact] {
var channelToContact = [String: Contact]()
var contactToChannels = [Contact: Set<String>]()
for contact in contacts {
var mergedContact = contact
for channel in contact.allChannels {
if let matchingContact = channelToContact[channel] {
if mergedContact !== matchingContact {
let mergedMatchingContact = Contact.mergedContactFrom(contact: matchingContact, otherContact: mergedContact)
contactToChannels[mergedMatchingContact] = (contactToChannels[mergedContact] ?? []).union((contactToChannels[mergedMatchingContact] ?? []))
if let channels = contactToChannels[mergedContact] {
for c in channels {
channelToContact[c] = mergedMatchingContact
}
}
contactToChannels[mergedContact] = nil
mergedContact = mergedMatchingContact
}
} else {
channelToContact[channel] = mergedContact
if contactToChannels[mergedContact] != nil {
contactToChannels[mergedContact]!.insert(channel)
} else {
contactToChannels[mergedContact] = [channel]
}
}
}
}
return contactToChannels.keys
}
The eventual complexity was linear, the spinner would just flicker for a split second, and the tests were luckily green.
Since then, I’ve always been much more alert when it comes to doing some computation on the client side that potentially can have a variable-sized input. This all seems to be very obvious to me now, but back in the day this didn’t look too important to me. I think having a proper understanding of the complexity that comes with various algorithms and data structures can make you a much better software engineer which will lead to better products you build. After all, this is how the big tech companies hire — they value coding skills more than knowledge of certain frameworks.
These days, it’s also important for new folks who switch to software engineering from other areas — they often start their career with simple projects that involve UI work or simply connecting the stuff that’s built on top of well-known frameworks. I’d encourage them to also master the core things like algorithms in order to excel at this job.
I'm thrilled to announce that Pribyvalka got its first major update #3.2.4 since the previous one that happened almost 5 years ago just before the World Cup 2018.
We've heard a lot of interesting and passionate feedback since the original launch in 2015 — at least 3K App Store reviews total and almost 1500 in-app reviews every year. We had addressed some points before, however, this time we've implemented the top 3 user wishes:
Favorites — now the stops that you like are always there on the home page.
Plate number — a quick tap on the transport row on the arrivals list screen now shows the plate number and the model of transport.
Show route on map — now, in addition to the step-by-step route overview, we render the suggested public transport directions on the map.
There were other wishes too, for example, see the transport live on the map — like on Android. This is a good idea but most likely something for the next update ;-)
It's been live for a week already, and two thirds of the users are already on the latest version. The monthly active users number is healthy — close to 35K which is a solid number for the quietest month of the year. The peak was 60K+ in 2018, and I do hope that in September/October we will see the regular 43-47K regular users. The app is as stable as a mountain — as usual — which makes me as a software engineer really happy.
Full what's new notes from App Store:
We slightly got out of our usual cadence to update Pribyvalka once every two years, so we switched to a 5-year one now:
– We've spent all this time thinking where to put the Like button (but still haven't reached an agreement).
– We added the glorious registration plate number and vehicle model label.
– We tried to ask multiple AI chatbots how to open a .sketch file in 2023 to refresh the app icon. We gave up and made it in Figma.
– We wanted to fix stuff under the hood but instead we were surprised that everything still works.
– We built the route visualization on the map. The route calculation algorithm quality is still stuck in 2017, but at least now it looks gorgeous.
– We have been thoroughly gaining experience in the best tech companies of Russia and the world, but the result is still the same – we added new bugs and fixed the old ones.
We do hope that everyone connected to Samara – whether living there, or visiting, or just feeling some nostalgia with the help of Russian VPN – will enjoy swiping the cards and tapping the hearts on their warm summer night.
Pavel Shadrin
Самые комментируемые за месяц
Performance Bottlenecks Hiding on the Client Side ★
As an iOS engineer, for the most time in my career, I have been “transforming JSONs into beautiful UI”. Compared to backend development, handling large amounts of data and doing performance optimizations are not a typical part of our work. Of course, from time to time, performance does matter — especially to make the UI smooth, but the techniques are often different — such as reusing views or offloading expensive work from the main thread. Additionally, if we want the client to be thin, most of the heavy job is delegated to the server, for example, content ranking, search, filtering and so on.
However, sometimes you still have to perform some expensive operations on the client side — for example, because for privacy reasons you don’t want some local data to leave the device. It’s easy to accidentally make those parts of code extremely inefficient — especially if you haven’t built this muscle of quickly spotting potential complexity issues yet. Algorithms and data structures do matter — this is something I only truly realized only several years into my mobile career, and I still see this thing to be often overlooked in the industry. Of course, early optimization is not needed and may even do harm (see premature optimization), but even basic calculations can become performance bottlenecks that severely harm user experience.
There is only one way to solve it — embrace the basics — which means using appropriate algorithms and data structures for the task at hand. One real example that I always recall is a thing I built for one of my projects many years ago. For an invitation flow, I had to implement a contact merge feature where the data would come from three different sources — backend, social account and local iPhone address book. We wanted to combine contacts from these sources into one if they had any overlapping channels (phone numbers or emails). The result would be an array of contacts with all their channels, so there would be no duplicate channels for two different contacts.
At first, my naive approach was just to go one by one and see if in the remainder of the list any contact has overlapping channels with the current one, merge them if yes, repeat. This was needed because, for example, the last channel in the list could have two channels — one that would overlap with the current one, and the second one that could have appeared in the previous contacts which would mean having to go through the list again.
I implemented this, and it worked pretty reliably, here is the pseudocode:
An experienced engineer would quickly spot the issue here, but plese bear with me for a minute. I tested this on my device which had roughly 150 local contacts, 100 friends on social media, and a couple dozen users from the server. It would finish in just a couple of seconds after showing a spinner — “not a huge deal” I thought and moved on to the next feature. Test devices had much fewer contacts, so it worked instantly there. Then a couple of weeks later we started getting some reports from the users that this spinner can take a minute or even longer. Suddenly I realized that the issue was related to complexity, and then I figured that the approach I had taken could actually hit the O(n^2) complexity — similar to the bubble sort.
I quickly discussed that with another engineer on a whiteboard, and we came up with hashmaps to optimize this significantly:
The eventual complexity was linear, the spinner would just flicker for a split second, and the tests were luckily green.
Since then, I’ve always been much more alert when it comes to doing some computation on the client side that potentially can have a variable-sized input. This all seems to be very obvious to me now, but back in the day this didn’t look too important to me. I think having a proper understanding of the complexity that comes with various algorithms and data structures can make you a much better software engineer which will lead to better products you build. After all, this is how the big tech companies hire — they value coding skills more than knowledge of certain frameworks.
These days, it’s also important for new folks who switch to software engineering from other areas — they often start their career with simple projects that involve UI work or simply connecting the stuff that’s built on top of well-known frameworks. I’d encourage them to also master the core things like algorithms in order to excel at this job.
Pribyvalka Update: 2023
I'm thrilled to announce that Pribyvalka got its first major update #3.2.4 since the previous one that happened almost 5 years ago just before the World Cup 2018.
We've heard a lot of interesting and passionate feedback since the original launch in 2015 — at least 3K App Store reviews total and almost 1500 in-app reviews every year. We had addressed some points before, however, this time we've implemented the top 3 user wishes:
There were other wishes too, for example, see the transport live on the map — like on Android. This is a good idea but most likely something for the next update ;-)
It's been live for a week already, and two thirds of the users are already on the latest version. The monthly active users number is healthy — close to 35K which is a solid number for the quietest month of the year. The peak was 60K+ in 2018, and I do hope that in September/October we will see the regular 43-47K regular users. The app is as stable as a mountain — as usual — which makes me as a software engineer really happy.
Full what's new notes from App Store:
Download from App Store