Simple bindable "no data" placeholder for UITableView

Almost every iOS application uses UITableView to display some kind of data and in many cases the data can be missing. In such situation you typically want to display some kind of “no data” placeholder. The placeholder fills in the empty space and can tell the user what needs to be done to get the data.

There are many ways wo implement such “no data” placeholder, including using libraries like UIEmptyState. If you just need to display a simple text message, there is an really easy way to implement it yourself, as I did in a recent project.

Showing and hiding the placeholder

The easiest way to create a “no data” placeholder for UITableView is to set it as the background view. It will be shown when there is no data in the UITableView so it will not be hidden under any other wiew.

I created a simple UITableView extension to show the placeholder with a specific message

extension UITableView {
    func setNoDataPlaceholder(_ message: String) {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        label.text = message
        // styling
        label.sizeToFit()

        self.isScrollEnabled = false
        self.backgroundView = label
        self.separatorStyle = .none
    }
}

To hide it when no longer needed I created another extension method

extension UITableView {
    func removeNoDataPlaceholder() {
        self.isScrollEnabled = true
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
}

With this I would have to call those two extension method manually depending on the data in the UITableView which is unnecessary manual work.

[Read More]
ios  xcode  rxswift 

Why is there a "lag" in iOS gesture detection near the edges of the screen?

A few weeks ago I encountered a strange problem when working on an iOS application that seemed really strange at first sight.

The task

The task was simple. In the chat detail screen I had to add a voice recording button next to the message input. When the user started holding the button (.touchDown) the voice recording should have started, releasing the button (.touchUpInside) should have finish the recording and sliding to side (.touchDragOutside) should have canceled the voice recording.

The message input was located in the bottom right corner of the screen when keyboard was not shown

and moved up with the keyboard

The problem

When the keyboard was visible the quick recording button worked as expected, but when the quick recording button was at the bottom of the screen there was a “lag”, a delay of about 1 second, between touching the button and the .touchDown even firing.

[Read More]
swift  ios  xcode 

Animating tab bar buttons on tap

If you use the Twitter app on iOS you might have noticed that tapping the buttons in the tab bar makes them bounces.

This is a very subtle animation that I really like so I decided to do the same for the tab bar in the app I currently work on.

The first step is to find the right place to insert the bounce animation into. When you tap a button in the tab bar, the tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) method of the UITabBarController gets called. You cannot override this method in extension, so you have to create a new subclass.

This method gives you the selected UITabBarItem but you need to get to the actual view and its image. I found out that the tab bar contains (at least in my case) a background subview and then subviews corresponding to the tab bar buttons, so when a tab bar button at index N is tapped, its subview is at N+1.

class AnimatedTabBarController: UITabBarController {

    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        // find index if the selected tab bar item, then find the corresponding view and get its image, the view position is offset by 1 because the first item is the background (at least in this case)
        guard let idx = tabBar.items?.index(of: item), tabBar.subviews.count > idx + 1, let imageView = tabBar.subviews[idx + 1].subviews.compactMap ({ $0 as? UIImageView }).first else {
            return
        }

        // animate the imageView
    }
}

To create a bounce animation we can use CAKeyframeAnimation and animate the transform.scale key path. Basically, you need to make the image bigger, then slightly smaller and original size again. This is the animation I use

[Read More]
swift  ios  xcode 

Unit testing view controller memory leaks

After adding a new feature to the iOS app I currently works on I noticed an unexpected memory spike after the app was used for a while. This usually means a memory leak; some object not being deallocated after it is no longed need. This is often caused by using self without unowned / weak or by forgetting to make the delegates weak (tools like SwiftLint can warn you about this case).

In my case the problem was a UIViewController not being deallocated after being removed from the navigation stuck because of a error in a binding. I found the bug using the Instruments in Xcode but it got me ask some questions. What if there are memory leaks in other parts for the app, in flows that are not used so much? Is there a way to somehow automatically test for memory leaks? I found SpecLeaks as the best way to answer those questions.

SpecLeaks

SpecLeaks is a framework build on top of Quick and Nimble that helps you to unit test memory leaks in Swift. You can use it to unit test memory leaks in any kind of objects, I chose to unit test my view controllers because they seemed to be most probable cause of memory leaks in my apps.

SpecLeaks can detect that your are testing a UIViewController and also call viewDidLoad to fully initialize the UIViewController. A simple memory leak test may then look like this

class SomeViewControllerTests: QuickSpec {
    override func spec() {
        describe("SomeViewController") {
            describe("viewDidLoad") {
                let vc = LeakTest {
                    return SomeViewController()
                }
                it("must not leak"){
                    expect(vc).toNot(leak())
                }
            }
        }
    }
}

You can initialize your view controllers using init or get them from story boards, it does not matter. The unit tests will fail for every leaking view UIViewController.

[Read More]
swift  ios  xcode 

Building iOS dependencies with Carthage

In all my iOS projects I use and strongly prefer Carthage. It is easy to use, does not do any changes to your project, all the dependencies are built just once and then linked to the project as dynamic frameworks. There are many good posts about the advantages of Carthage compared to CocoaPods so in this post I will just focus on the actual usage, mainly in CI.

Carthage basics

All your Carthage dependencies are listed in the Cartfile file in the root of your project. In case you split your app into multiple projects like I do, there is a Cartfile for every project in the workspace. Next to every Cartfile there is a Cartfile.resolved file pinning all your dependencies to a specific version.

You just need to keep those two files in your source control and then run carthage bootstrap when you clone the project so Carthage downloads and builds all the dependencies. This happens just once for a developer, but it is slow and time consuming. If you use a CI for automatic builds, it becomes a real time waste rebuilding all the dependencies before each build.

Carthage approaches

Developers typically try to speed things up with multiple approaches

  • Keeping Carthage/Checkouts in source control. This makes the repository bigger by keeping unnecessary files, the checkout is faster but the build is still slow.
  • Keeping Carthage/Build in source control. This also makes the repository bigger, potentially much bigger if you update your dependencies often, but the build times are super fast as there is nothing to actually build.
  • Caching the Carthage builds in CI. This does not make the repository bigger and can be really fast when done properly
  • Caching the Carthage builds using tools like Rome. This does not make the repository bigger and can be very powerful and flexible, but typically requires a paid 3rd party storage service like Amazon S3.
[Read More]
swift  ios  xcode