PIXEL
DOCK

I like the smell of Swift in the morning…

The curious case of the phantom dates

Posted: | Author: | Filed under: iOS, Swift | Tags: | No Comments »

Recently I had to fix a bug where a DateFormatter would return nil when trying to convert a String to a Date. The date string was in a perfectly valid format and the formatter would return nil while on two dates when all the other dates could be transformed without problem.

Here is a screenshot of a playground that I used to examine the problem:

As you can see the formatter does not have a problem with 2022-03-27T01:30:00 but for some reason it cannot transform 2022-03-27T02:00:00 to a valid date

I observed that this invalid dates only occur on March 27th between 02:00a.m. and 02:59a.m.

And then it dawned on me. Daylight Saving Time! Of course. In my country we change our clocks on March 27th directly from 01:59 to 03:00. So the formatter is correctly returning nil when trying to format a date that lies in the hour that does not exist during that night.

So a DateFormatter is quite clever and knows about the oddities of DST. Turns out, that DateFormatter is even more clever when you want it to be: By setting its isLenient property to true the formatter uses heuristics to guess what date you wanted when you provides the date string for a date that does not exist.

Here is what happens when you let the DateFormatter guess what you really wanted:

As you can see now the formatter correctly transforms the 02:00a.m. date to 03:00.a.m. during the night of the switch to Daylight Saving Time.

This might not always be what you want, but in my case it was the (easy) fix to the bug.

When a DateFormatter gives you the correct day and month but the wrong year

Posted: | Author: | Filed under: iOS, Swift | Tags: | No Comments »

My new year started with a really strange bug. I use a DateFormatter to show the current month to the user. So when I opened the app on January 02, 2021 I expected to see “Saturday, Jan 2, 2021”. But what I got was “Saturday, Jan 2, 2020”. Somehow my DateFormatter did not get the message that 2020 was finally over.

let isoDateFormatter = ISO8601DateFormatter()
let january2 = isoDateFormatter.date(from:"2021-01-02T12:00:00+0000")!

let fmt = DateFormatter()
fmt.dateFormat = "EEEE, MMM d, YYYY"
fmt.locale = Locale(identifier: "en_GB")
fmt.string(from: january2) // gives you "Saturday, Jan 2, 2020"

So what happened? It took me quite a while to get my head around this bug. We all know that timezones can be tricky, but my DateFormatter did get the weekday, day and month correctly but the year was wrong. So it was not a matter of timezone confusion.

After fiddeling around a bit I replaced “YYYY” with “yyyy” and the bug was fixed.

fmt.dateFormat = "EEEE, MMM d, yyyy"
fmt2.string(from: january2) // gives you "Saturday, Jan 2, 2021"

But why? Turns out that “YYYY” uses a week-based calendar for its calculations.

So what is a week-based calendar???

A week-based calendar uses calendar weeks for its calculations. The concept of a calendar week is pretty straight forward. A year usually has 52 weeks so you can identify any week in a year by its number (e.g. week 17 is usually the last week in April).

But when does the first calendar week start? January 1st? That would be easy. Unfortunately it is more complicated than that. I found this definition:

In Europe, the first calendar week of the year is the first week that contains four days of the new year.

For 2021 this means that the first calendar week starts on Monday, Jan 4, because the first 3 days in January are in the previous week. And that week only contains 3 days of the new year and is therefor considered the last calendar week of 2020.

So, in a week-based calendar the first three days of 2021 are still part of 2020. Crazy.

But that’s not the end of the craziness. You might have wondered about the “In Europe” part of the definition”. The thing is: In Europe the first day of a week is Monday. In other countries (like the US and Canada) it is Sunday and in the Middle East it is Saturday. To add even more confusion: Some countries (like the US and Canada) choose to make things easier and start the first calendar week on January 1, no matter which day of the week this happens to be.

Here is a comparison between calendars in the USA and the UK (have a look at the week numbers and the first day of the week):

Calendar weeks in the USA Calendar Weeks in the United Kingdom

So the same day can be in different calendar weeks, depending on your locale:

let fmtGB = DateFormatter()
fmtGB.dateFormat = "YYYY-ww-D"
fmtGB.locale = Locale(identifier: "en_GB")

let fmtUS = DateFormatter()
fmtUS.dateFormat = "YYYY-ww-D"
fmtUS.locale = Locale(identifier: "en_US")

fmtGB.string(from: january2) // gives you "2020-53-2" (day 2 in week 53 of 2020)
fmtUS.string(from: january2) // gives you "2021-01-2" (day 2 in week 1 of 2021)

So in the future I will be really careful NOT to use “YYYY” in a DateFormatter. The worst thing about this bug is that it only occurs in the first days of a new year (or the last days of the old year). The rest of the year everything is working well. Quite a sneaky bug that is 😉

Setting the Status Bar Style without pulling your hair out

Posted: | Author: | Filed under: iOS, Swift | Tags: | No Comments »

Some things are inexplicably hard in iOS. Like changing the color of your app’s status bar. In theory it is really easy:

  • Set UIViewControllerBasedStatusBarAppearance to true in your Info.plist
  • Override preferredStatusBarStyle in your view controllers to return your desired UIStatusBarStyle

So why doesn’t it work in some cases? Chances are that you are trying to set the status bar style in a view controller that is embedded in a UINavigationController. In that case the system is asking the UINavigationController for its preferredStatusBarStyle. The override in your custom view controller is simply ignored.

To fix this you can use this handy little extension on UINavigationController:

extension UINavigationController {
    override open var preferredStatusBarStyle: UIStatusBarStyle {
        return topViewController?.preferredStatusBarStyle ?? .default
    }
}

Now the UINavigationController gets the preferredStatusBarStyle from its topViewController (your custom view controller). Or it does return the default value if there is no topViewController (which should never happen)

Debugging a “terminating with uncaught exception of type NSException” crash

Posted: | Author: | Filed under: Swift, Xcode | Tags: | 2 Comments »

From time to time it happens to all of us: An app crashes with the not really helpful message “terminating with uncaught exception of type NSException”. Great.
This can happen when an assertion in a framework you are using fails (among other reasons). When this happens Xcode is not very helpful and just shows you that an exception was thrown. It does not show the reason nor the location where the exception was thrown.

Not very helpful.

To find our more about the exception you can use an Exception Breakpoint:

1. Open the breakpoint navigator (⌘8)
2. Press “+” in the footer
3. Choose “Exception Breakpoint…”
4. Leave all default values. Only change “Action” to “Debugger Command”
5. Enter po $arg1 into the text field

That’s it! Your shiny new Exception Breakpoint should look like this:

Now, when you try to start your app again, it still crashes (obviously), but you’ll see the exception’s message in the Debug area and Xcode shows you the code where the exception was thrown:

Much better! Now you know why your app crashed.

How to show a Push Notification while the app is in foreground

Posted: | Author: | Filed under: iOS, Swift | Tags: , , | 1 Comment »

I currently work on a project where we use Apple’s User Notifications (formerly known as Push Notifications) to deliver chat messages to our users. Whenever the user is not in the chat screen we want to show the Notification using the standard system banner on top of the screen.

Per default when a User Notification is received while the app runs in the foreground the notification is not shown to the user. With the arrival of Apple’s iOS10 UserNotifications framework you can easily decide whether to show the received Notification or not. When a Notification is delivered to a foreground app the UNUserNotificationCenterDelegate method userNotificationCenter(_:willPresent:withCompletionHandler:) is called. By calling this method’s completionHandler you tell the system if and how it should show the Notification to the user:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // do your stuff 

    // this tells the system NOT to show the notification to the user
    completionHandler()
}

If you want to show the notification you can add a set of UNNotificationPresentationOptions to the completion handler:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // do your stuff 

    // this tells the system to show the notification to the user with sound
    completionHandler([.alert, .sound])
}

If you don’t want to play the notification sound while the app is running in the foreground you can simply omit the sound option:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // do your stuff 

    // this tells the system to show the notification to the user without sound
    completionHandler(.alert)
}

How to dismiss the phone call prompt during a Xcode UITest

Posted: | Author: | Filed under: iOS, Swift, UITests | 2 Comments »

Sometimes you offer your users the ability to make a phone call directly from your app. For example there could be a “Call Help Center” button that a user can press to make the call. When the user taps on that button the system will prompt a dialog where the user can either cancel or make the call.

When you write a UITest for this button you want to test that tapping the button will initiate the call. But you don’t actually want to make the call. So you need a way to cancel the call when the dialog shows up.

Normally you would add a UIInterruptionMonitor at the beginning of the UITest. This works for a lot of system dialogs (e.g. the dialog that asks the user for permission to use his location). But in this case it does not work. The UIInterruptionMonitor’s closure is never executed.

The reason is quite simple. UIInterruptionMonitor only works with dialogs that are presented by your app. All the permission dialogs are presented within your app’s scope, so the UIInterruptionMonitor can handle them. The phone call dialog however is presented from outside your app’s scope. You need to access the dialog via the Springboard which is now possible with Xcode 9.

You can test the help center button and dismiss dialog like this:

func testCallHelpCenterButton() {
    let app = XCUIApplication()
    let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    app.launch()
    app.buttons["Call Help Center"].tap()

    // check that the dialog is presented and tap the 'Cancel' button 
    let cancelButton = springboard.buttons["Cancel"]
    XCTAssert(cancelButton.exists)
    cancelButton.tap()
}

Dismiss a SFSafariViewController in a Calabash test

Posted: | Author: | Filed under: Calabash, iOS, UITests | Tags: | No Comments »

When writing a Calabash test for a new feature I encountered the following challenge: During the test a SFSafariViewController is presented and has to be dismissed again. For security reasons the user’s activity and interaction with SFSafariViewController are not visible to the app. In other words: Calabash cannot access the SFSafariViewController’s UI elements via query.

So you cannot query the ‘Done’ button like this:

def doneButton
   "view:'_UIModernBarButton' marked:'Done'"
end

Because SFSafariViewController’s ‘Done’ Button is not accessible for the app, Calabash cannot see it and the test fails.

Calabash added a DeviceAgent for that case. The DeviceAgent can detect views that are outside of your own app or views that are presented by the OS (like SFSafariViewController or MFMailComposeViewController).

So to tap the SFSafariViewController’s ‘Done’ button and dismiss it you can do this:

Then(/^I close Safari/) do
   device_agent.touch(type: 'Button', marked: 'Done')
end

You can also use this to dismiss an Email Compose View Controller (Although there you have to touch the ‘Cancel’ button).

Just be aware that you should use the DeviceAgent when you really have to, because Calabash’s query engine is faster and can find more views than the DeviceAgent.

Testing Push Notifications with Xcode UITests

Posted: | Author: | Filed under: iOS, Swift, Tools, UITests | Tags: | 8 Comments »

I recently worked on an app for a big retailer that made intensive use of remote notifications (aka push notifications). One use case: Whenever you use the app to pay for your shopping you will receive a remote notification when the payment was successful. Tapping on the remote notification would launch the app and show the receipt for the transaction.

As I used a lot of Xcode UITests on this project I was wandering if it is possible to test the remote notification handling in an UITest. It turns out that it is possible. There are two things that need to happen during the test:

1. Trigger a mock remote notification
2. Tap on the remote notification when it is received

Triggering a remote notification is relative easy thanks to the work of the fine folks at noodlework. They build a framework to play around with the Apple Push Notification service (APNs). Using their framework (NWPusher) makes it possible to trigger a remote notification right from a UITest class.

There is one new feature that comes to the XCUITest framework with Xcode 9: Multi App Testing. It is now possible to interact with other apps (and the Springboard) when running a UITest 🎉. So with Xcode 9 we can wait for the remote notification to appear and tap on it. Yay!

So, let’s do it.

Set up a dummy application (if needed)

I’m assuming that you already have an app that uses remote notifications. Otherwise just setup a simple app like I did. My app has a main view controller (with a gray background) and three child view controllers with different background colors that are presented modally based on the remote notification that was received:

My remote notification’s payload contains a key vcType that can have three values red, blue or green. When you tap on the remote notification the app looks for that key to determine which view controller to present.

This is the remote notification payload that makes the app present a red view controller:

{
   "aps" : { "alert" : "Hello Red" },
   "vcType" : "red"
}

If you are setting up a new app for this remember to activate the PushNotification capabilities in your target.

Trigger a remote notification in a UITest

1. Install NWPusher
Add NWPusher to the UITest target of your app. I use Carthage to do that but you can also use CocoaPods or add their source files directly to your app.

2. Add push notification certificate to your app
To trigger a remote notification you need to add a valid Apple Push Notification service SSL certificate to the UITest target of your app.

There is a detailed description on how to do this in the NWPusher repo, so I keep it short here:
– Create a Development APN Certificate on the Apple developer center (if you do not already have created one).
– Download the certificate and add it to your Keychain
– Export the certificate to a PKCS12 file. Set a password (and remember it ;-))
– Add the p12 file to the UITest target.

Now you are ready to trigger a Remote Notification right from your UITest’s code.

3. Trigger a remote notification from a UITest
I created a helper function to trigger a remote notification because I want to trigger multiple notifications:

func triggerPushNotification(withPayload payload: String, deviceToken: String) {
   let uiTestBundle = Bundle(for: PushNotificationUITests.self)
   guard let url = uiTestBundle.url(forResource: "pusher.p12", withExtension: nil) else { return }
        
   do {
      let data = try Data(contentsOf: url)
      let pusher = try NWPusher.connect(withPKCS12Data: data, password: "pusher", environment: .auto)
      try pusher.pushPayload(payload, token: deviceToken, identifier: UInt(arc4random_uniform(UInt32(999))))
   } catch {
      print(error)
   }
}

The code loads the certificate file into a Data object, connects to NWPusher’s service and triggers the remote notification.

You might have noticed that this function expects a deviceToken that is needed for the NWPusher service. That is a bit of a problem, because the deviceToken changes everytime you install the app or run the test on a different device.

To get around this I use a dirty hack. I do not really like this but I could not think of another way to make the deviceToken available to the UITest. If you have a better idea, please let me know in the comments!

To make the deviceToken accessible to the UITest I add a launch argument “isRunningUITests” when launching the app from a UITest. The app checks if the launch argument is present and if it is, adds a tiny UILabel to its root view controller and sets the label’s text to the device token. The UITest class then can read the deviceToken from that label. Shudder. Really ugly, I know.

The alternative would be to manually set the deviceToken when running the UITests. Not an option.

So now we can trigger a remote notification from a UITest class. Neat!

One more thing: When you run the app for the first time a system dialog will pop up to ask the user’s permission to send remote notifications. This also happens during a UITest, so we have to dismiss this dialog during the test.

I added a helper function that dismisses the system dialog:

func allowPushNotificationsIfNeeded() {
   addUIInterruptionMonitor(withDescription: "“RemoteNotification” Would Like to Send You Notifications") { (alerts) -> Bool in
      if(alerts.buttons["Allow"].exists){
         alerts.buttons["Allow"].tap()
         return true
      }
      return false
   }
   XCUIApplication().tap()
}

The tap on the XCUIApplication seems a bit strange but you have to interact with the app after setting up the interruption monitor to make it work.

Write the UITest that tests the 3 different remote notifications

Now we can write a UITest that does the following steps:

1. Hold a reference to the app and the springboard
2. Get the deviceToken from the app
3. Close the app
4. Trigger a “red” remote notification
5. Tap on the notification
6. Assert that the red view controller is shown
7. Close the app
8. Trigger a “green” remote notification
9. Tap on the notification
10. Assert that the green view controller is shown
11. Close the app
12. Trigger a “blue” remote notification
13. Tap on the notification
14. Assert that the blue view controller is shown

func testPushNotifications() {
   let app = XCUIApplication()
   app.launchArguments.append("isRunningUITests")
   app.launch()
        
   // access to the springboard (to be able to tap the notification later) [1]
   let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
        
   // dismiss the system dialog if it pops up
   allowPushNotificationsIfNeeded()
        
   // get the current deviceToken from the app [2]
   let deviceToken = app.staticTexts.element(matching: .any, identifier: "tokenLabel").label
        
   // close app [3]
   XCUIDevice.shared.press(XCUIDevice.Button.home)
   sleep(1)
        
   // trigger red Push Notification [4]
   triggerPushNotification(
      withPayload: "{\"aps\":{\"alert\":\"Hello Red\"}, \"vcType\":\"red\"}", 
      deviceToken: deviceToken)
                
   // tap on the notification when it is received [5]
   springboard.otherElements["PUSHNOTIFICATION, now, Hello Red"].tap()
        
   // check if the red view controller is shown [6]
   XCTAssert(app.staticTexts["Red"].exists)
        
   // dismiss modal view controller and close app [7]
   app.buttons["Close"].tap()
   XCUIDevice.shared.press(XCUIDevice.Button.home)
   sleep(1)
        
   // trigger green Push Notification [8]
   triggerPushNotification(
      withPayload: "{\"aps\":{\"alert\":\"Hello Green\"}, \"vcType\":\"green\"}",
      deviceToken: deviceToken)
        
   // tap on the notification when it is received [9]
   springboard.otherElements["PUSHNOTIFICATION, now, Hello Green"].tap()
        
   // check if the green view controller is shown [10]
   XCTAssert(app.staticTexts["Green"].exists)
        
   // dismiss modal view controller and close app [11]
   app.buttons["Close"].tap()
   XCUIDevice.shared.press(XCUIDevice.Button.home)
   sleep(1)
        
   // trigger blue Push Notification [12]
   triggerPushNotification(
      withPayload: "{\"aps\":{\"alert\":\"Hello Blue\"}, \"vcType\":\"blue\"}",
      deviceToken: deviceToken)
        
   // tap on the notification when it is received [13]
   springboard.otherElements["PUSHNOTIFICATION, now, Hello Blue"].tap()
        
   // check if the blue view controller is shown [14]
   XCTAssert(app.staticTexts["Blue"].exists)
        
   // dismiss modal view controller 
   app.buttons["Close"].tap()
}

I added sleep statements after closing the to make sure that the app is fully in background mode before triggering the remote notifications.

Now we can run the test and Voila! UITests that test the handling of remote notifications! Just don’t try to run the tests in a Simulator as remote notifications only work on real devices.

You can checkout the demo app including the UITest here. Make sure to add the demo app as your own app to your developer account and to import your own APN development certificate (the p12 file) to the project to make it work.

How to use your app’s Localizable.strings in your Xcode UITests

Posted: | Author: | Filed under: iOS, UITests, Xcode | Tags: , | 1 Comment »

When your are writing UITests for a multi language app you need access to your Localizable.strings. For example when you need to press a “Save” Button in your test. Because your app is localized the text “Save” is part of your Localizable.strings file.

In your app you set the title of your “Save” Button like this:

saveButton.setTitle(NSLocalizedString("Button.Save", comment: ""), for: .normal)

When you want to tap that button in a UITest you need to know the title of the button:

XCUIApplication().buttons["Save"].tap()

You could use the quick and dirty approach by just using “Save”. But that will break your test when the button’s title is changed in your Localizable.strings file. And it feels dirty, too 😉

The right way to do this is to retrieve the button title from your Localizable.strings file. To make that work you have to do two things:

1. Add your Localizable.strings file to your UITest target

2. Access the file via the UITest bundle (I use a little helper method for that):

func localized(_ key: String) -> String {
    let uiTestBundle = Bundle(for: AClassFromYourUITests.self)
    return NSLocalizedString(key, bundle: uiTestBundle, comment: "")
}

You can use any class from your UITests to access the UITest bundle. When you are not explicitly asking to use the UITest bundle when initializing the NSLocalizedString the default value Bundle.main will be used. Using Bundle.main does not work when running UITests because it gives you the bundle of the UITest Runner App instead of the UITest bundle.

Voilà! You can now use your localized strings in your UITests:

XCUIApplication().buttons[localized("Button.Save")].tap()

Xcode UITests: How to check if a UISwitch is on

Posted: | Author: | Filed under: Swift, UITests, Xcode | 12 Comments »

A simple scenario: You are writing a UITest that should check if a UISwitch is switched on.

In a UnitTest you would simply do this:

XCTAssert(yourSwitch.isOn)

When you are running a UITest you cannot do that, because during a running UITest you cannot access the UISwitch directly. Xcode only gives you access to your app’s UI elements via the XCUIElement class. That class is used for all accessible UI elements so it does not have a isOn property like UISwitch has.

So how do you test that the UISwitch is on?

It’s actually pretty easy, but not really obvious. XCUIElement conforms to XCUIElementAttributes which gives you access to a value property of type Any?

So, as value can literally be Any(thing) I tried to cast it to a Bool, because isOn also is a Bool, right?

Not working! Turns out that when you access a UISwitch in your UITest its value property is of type NSTaggedPointerString. Which is a subclass of NSString and can be cast to String (when you are using Swift). So the Bool isOn value has been mapped to a “0” or “1” String.

To test if the first UISwitch in your current view has been switched on, you can do this in a UITest:

let firstSwitch = app.switches.element(boundBy: 0)
XCTAssertEqual(firstSwitch.value as! String, "1")

If you want to access your UISwitch via the accessibility identifier you can do it like this:

let mySwitch = app.switches["MySwitchIdentifier"]