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 

Checking for missing translations in iOS projects

When you work on an iOS app localized into multiple languages one of the biggest challenges is making sure that everything is translated, no string is missing in any language. Xcode does not provide any tool to make this easier for you, but there are some 3rd party tools that you can integrate into your workflow.

verify-string-files

I have recently found a quite old project called verify-string-files. It is a command line tool take takes you base localization file and compares it to all the translations, informing you about missing strings. Altough the last commit to this project was way back in 2014 it still works reliably, there have not been any changes to the way iOS does localization.

The best thing about this tool is that you can integrate it to your build process.

Build process integration

The tool is available only as source code, so you have to build it first. I put the built binary to a support folder of my projects.

Then I have a Build-Phases folder with all the script used in the build process. I think it is a much better solution than embedding the script right into the Xcode project.

To integrate verify-string-files you just need a really simple script, providing the path to the base localization file

${PROJECT_DIR}/support/verify-string-files -master ${SRCROOT}/iOSSampleApp/Resources/Base.lproj/Localizable.strings

In Xcode, just add a new Run scrip phase and call $SRCROOT/Build-Phases/check-strings where check-strings is the script name. Just do not forget to chmod +x the actual file.

This will result in build errors when there are missing string, shown directly in the base localization file

[Read More]
swift  ios  xcode