Develop high responsive mobile app, something to take note

Hai Kieu
Hai Kieu
Jul 13 · 5 min read

As an iOS developer, I would like my app to be a perfect one. Smooth experience, lag-free. In other word, it should be high responsive to user interaction. But first and foremost what is the criteria of a high responsive app?

Fortunately, We have a number, ideally 60 FPS (frames per second) and it should not drop below 30 FPS in the worst case. A high responsive app should be able to refresh its views at 60FPS, if not We definitely feel lagging, slowdown while interacting with the app. This is an awful experience to user and a sign for a bad app anyway.

FPS is dropped below 60FPS means the Main-Thread is carrying out a heavy task, or get abused by a ton of tasks, sometimes not relate to UI.

A heavy task can be easily recognized and resolved by dispatching them to background threads , but abusing the Main-Thread is accumulated throughout the entire codebase due to bad coding practices or not knowing the thing. These bite the Main-Thread bandwidth gradually over time and not show symptom or right away consequence.


FPS counter

FPS is the most important for a responsive app, so it is not a bad idea to show FPS counter right on the screen and monitor it through app life time. Anyone from QA to developers can know off-hand when/where the app’s FPS dropped dramatically. This is a criticial info and should be transparent in the very first place.

It is very simple to impement a FPS counter with help of CADisplayLink. The CADisplayLink in default is naturally synced to the refresh rate of the screen, with this you simply count how many frames you get in a seconds.

For a quick start, you can refer to some open sources available on Github.
E.i: konoma/fps-counter. It’s not mine, but a talent guy somewhere.


Bad practices

Let say the FPS is 60, so We has less than 1/60 second (0.016s, or 16 milliseconds) before the main-thread needs to get back to refresh UI, otherwise the frame is dropped. 16 milliseconds is not little but not too much.

Therefore, regarding my perspective, anything lengthy and unneeded should be avoid on Main-theard.

Ok, let go over some bad practices I recap below.

Lengthy init() method

class ChartView {
var dateTimes: []
var values:[]

init(_ dateTimes:[], values: []) {

self.dateTimtes = dateTimes
self.values = values
//Task 3- Calculate lines
...
//Task 2 - Calculate distances
...
//Task 3
//Task 4
}
}

An init() method should limit its scope within initializing its instance variables with initial values. For any following tasks, do it at the right time later. Don’t do all at once.

Tip: A lazy var leaves the variable stay idle and only initialize it once at where needed. This is very helpful tool to cut down the init() execution length.

Instance method vs static method

Math().calculate(…)

Sometimes, I see somebody creates an object just to call an instance method. By creating an object it also will the init() then deinit() afterward. Those are unneed. Why don’t use the static function in the first place?

Disrespect UIViewController lifecycle

  • Create sub-UIView elements too early in the init() method.
class MenuViewController: UIViewController {

init() {
//Create background view
...
//Create sub-menu
...
}
viewDidLoad() {
super.viewDidLoad()
//Do set up extra sub-views, not in init()
}
}

This occupies the main-thread longer than usual, just for doing init() of UIViewController. Besides, effort to set up the UI in init() is not a good thing when the UIViewController has not been fully loaded yet.

  • Force load UIViewController right away by call loadViewIfNeeded().
override func prepare(for segue: UIStoryboardSegue, sender: Any?){

if segue.identifier == “MenuView” {
let destVC = segue.destination
destVC.loadViewIfNeeded() //Force load view instantly
destVC.innerMenuView.dataSource = ...
}
}

This again occupies the main-thread longer than it should be because it force load the main view of UIViewController and also summon viewDidLoad() instantly. Normally, the viewDidLoaded() will be kicked in automatically quickly in next round. Don’t rush it

Load images on the fly

imageView.image = UIImage.init(named: fileName)imageView.image = UIImage.init(contentsOfFile: filePathUrl)

Image is a heavy asset and reading it from disk is always expensive. Especially when you do this on a UITableView, UICollectionView.

Tip: There are many ways to help you gain better performance on display images from disks:

  • Asynchronously load and display images
  • Pre-load images
  • Cache images

Log debug info on the main-thread

print(“debug >>> TableViewCell = \(tableCell)”)

This task is very tiny, right? It looks no harm. However if repeat at scale, it would be an issue. I give an example: UITableView ,and UICollectionView. I used to have a habit to debug the cell info to console. Cell init, cell will display, to cell disappear. When scrolling the debug info come to console massively. The console gets overwhelm with a ton of messages causing main-thread blocked, FPS dropped dramatically.

Tip: Dispatch the task to background anywhere anytime. A Log helper could be helpful to solve this situation like the sample below

let serialBgQueue = DispatchQueue(“com.hk.log”)public final class LogHelper {  public static func debug(message: String) {
serialBgQueue.async { print(messaage) }
}
}
LogHelper.debug("Hello world, I would not disturb the main-thread anymore :)")

Thread.sleep(…) on main-thread

Thread.sleep(...) //Call this on Main-Thread should be avoid anyway

Well, This trick sometimes is used just to work around a race condition. Block the main-thread within even 1 millisecond is pretty bad. Sometimes, it is the last bullet, but avoid this you can. A good design would help in the very first place.

Use Main-thread to enforce thread-safe

//This is just an example, I have never done this :)
func getTimeStamp() -> TimeInterval {
return DispatchQueue.main.sync { Date.now.timeIntervalSince1970 }
}

The Main-Thread is a serial queue. It means We can use make use of it to sync up tasks. But again, this should be avoided anyway. Instead of this, you can create a serial queue in a second like below

let serialQueue = DispatchQueue(“com.hk.serial”)

Where to go from here

Keep the main-thread off load sometimes is not an easy task. It require a creative thinking. Especially when you app is too complex and mixed up many things. But keep in mind the first word “The less you occupy the Main-Thread, the more responsive the app is”, it will give you the direction.

Ok, This post is long enough. I have given ideas from my view to begin with, hope any of them works out for your case.

Next, I’m gonna have some more posts to continue this topic. It would be something very interesting. Please follow and stay tuned :)

Origin: swiftreviewer.com
Reference: https://github.com/konoma/fps-counter

Hai Kieu

Written by

Hai Kieu

Senior developer. Been working on iOS, tvOS, Swift, and Objective-C

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade