Detecting click on a specific NSTableViewCell

When you use NSTableView in an macOS application, there is no direct way to know a specific NSTableViewCell was clicked by the user. In my Localization Editor project I wanted the user to be able to focus a NSTextField when clicking anywhere in the NSTableViewCell it is contained in, so I had to implement it myself.

I created a new delegate extending the NSTableViewDelegate with one additional method informing about a NSTableViewCell getting clicked

protocol NSTableViewClickableDelegate: NSTableViewDelegate {
    func tableView(_ tableView: NSTableView, didClickRow row: Int, didClickColumn: Int)
}

Then I added an extension to the NSTableView to compute the index of the clicked NSTableViewCell

extension NSTableView {
    open override func mouseDown(with event: NSEvent) {
        let localLocation = self.convert(event.locationInWindow, to: nil)
        let clickedRow = self.row(at: localLocation)
        let clickedColumn = self.column(at: localLocation)

        super.mouseDown(with: event)

        guard clickedRow >= 0, clickedColumn >= 0, let delegate = self.delegate as? NSTableViewClickableDelegate else {
            return
        }

        delegate.tableView(self, didClickRow: clickedRow, didClickColumn: clickedColumn)
    }
}

To be able to use this extension you just need to implement NSTableViewClickableDelegate instead of NSTableViewDelegate and use the additional method it provides.

macos  swift 

Making copy & paste work with NSTextField

When I started working on my open-source Localization Editor, which is a macOS application, I encountered some things that were a bit strange compared to iOS development. One of those things is that copy & paste does not automatically work on a NSTextField.

To be exact, copy & paste works on a NSTextField as long as you do not delete the Edit menu from the standard Main menu. If you do that for some reason, you have to implement all the copy & paste functionality yourself.

The key to the implementation is overriding the performKeyEquivalent method and manually checking for cmd + c/v/x/z/a

final class EditableNSTextField: NSTextField {

    private let commandKey = NSEvent.ModifierFlags.command.rawValue
    private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue

    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        if event.type == NSEvent.EventType.keyDown {
            if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
                switch event.charactersIgnoringModifiers! {
                case "x":
                    if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
                case "c":
                    if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
                case "v":
                    if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
                case "z":
                    if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
                case "a":
                    if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
                default:
                    break
                }
            } else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
                if event.charactersIgnoringModifiers == "Z" {
                    if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
                }
            }
        }
        return super.performKeyEquivalent(with: event)
    }
}
macos  swift 

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