A simplistic approach to using Google Analytics in your Swift UIViewControllers

I use Google Analytics to collect usage statistics across both my web and mobile applications.  There are a few great AngularJS and other libraries that track page usage automatically when a controller is initialized.  Using these concepts I wanted to create my own similar approach in Swift.  The end result of this is the creation of a simple BaseController which makes an analytics call when viewDidLoad is called.

class BaseController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Analytics().screenView(self.controllerName)
    }

}

Deconstructing this simple one line method call you will notice there are an extension method called controllerName passed into a static method on the analytics struct.  The controllerName references the below extension method and simply is used to get the name of the UIViewController so that we can track usage by the screen name.  Below is a snippet with the code for this extension method.

extension UIViewController {

    var controllerName: String {
        return NSStringFromClass(self.classForCoder).componentsSeparatedByString(".").last!
    }

}

The analytics struct is an anti corruption layer that I’ve added to my app to wrap the underlying Google Analytics code.  This also provides a layer of abstraction which helps combat the fact that Xcode randomly makes the Google Analytics Objective-C references invalid.  The Analytics struct is used in two places in my app. First it is initialized in my app delegate, then it is used within my BaseController to record feature usage.

Before we get to the Analytics struct code below is the snippet using in my app delegate used to create a default tracker with our Google Analytics key.

Analytics.createTracker(Analytics.GAToken.QAKey.rawValue)

Once initialized the Analytics struct method screenView is used on each viewDidLoad call to record that a UIViewController has been viewed.

import Google

public struct Analytics {

    enum GAToken: String {
        case QAKey = "UPDATE-WITH-QA-KEY"
        case ProdKey = "UPDATE-WITH-PROD-KEY"
    }

    public static func createTracker(key: String) {
        GAI.sharedInstance().trackerWithTrackingId(key)
        GAI.sharedInstance().logger.logLevel = GAILogLevel.None
        GAI.sharedInstance().trackUncaughtExceptions = true
    }

    private func getTracker() -> GAITracker {
        return GAI.sharedInstance().defaultTracker
    }

    public func screenView(name: String) {
        let tracker = getTracker()
        tracker.set(kGAIScreenName, value: name)
        let builder = GAIDictionaryBuilder.createScreenView()
        tracker.send(builder.build() as [NSObject : AnyObject])
    }
}

Although there are more comprehensive Analytics (Google or other) approaches I’ve found this straightforward approach provides the basics in away that works for me.

Helpful Links:

  • gist with source code available here
  • Google Analytics on CocoaPods here
  • Google Analytics for iOS resources

Swift handling the back button title

For my current project we’ve got a design requirement that all screens have a simple “Back” button instead of the default text that iOS provides when pushing a new controller.

Back-Button

I found that Swift’s inheritance model combined with extensions methods make this simple to implement in a re-usable fashion.  To start this approach, I implemented a based controller which I called  BaseController that manages the title of the backBarButtonItem.  This is automatically handled by just inheriting from the BaseController.  In the below example we create a new NotificationController using this pattern.

import UIKit

class NotificationController: BaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

The BaseController only has two lines of code.  The first creates a string with the title of the back button. The second calls an extension method that manages the back button title information.

import UIKit

class BaseViewController: UIViewController {

    override func viewDidLoad() {

        super.viewDidLoad()

        let backTitle = NSLocalizedString("Back", comment: "Back button label")
        self.addBackbutton(backTitle)

    }

}

The heavy lifting is performed in the UIViewController extension.  Using the addBackbutton method to add a new UIBarButtonItem or update the current one’s title.  If a new UIBarButtonItem is added the backButtonAction method is added to dismiss the controller on press.

import UIKit

extension UIViewController {

    func backButtonAction() {
        self.dismissViewControllerAnimated(true, completion: nil)
    }

    func addBackbutton(title: String) {
        if let nav = self.navigationController,
            let item = nav.navigationBar.topItem {
            item.backBarButtonItem  = UIBarButtonItem(title: title, style: UIBarButtonItemStyle.Plain, target: self, action:
                #selector(self.backButtonAction))
        } else {
            if let nav = self.navigationController,
                let _ = nav.navigationBar.backItem {
                self.navigationController!.navigationBar.backItem!.title = title
            }
        }
    }
}

The full code is available as a gist here.

Hiding .DS_Store files on your desktop while still showing hidden files

Like most developers the first thing I do when I get a new mac or do a clean install is run the below terminal command to show all hidden files and folders.

defaults write com.app.Finder AppleShowAllFiles Yes
killall Finder

This is a necessary evil as now I see Dot files on my desktop. For me this is the annoying .DS_Store and .localized files.   The only way I’ve found to not show these on my desktop yet have hidden files enabled is to change their icon to transparent.  This is very much a hack but accomplishes it’s goal in only a few clicks.  Below is a step by step tutorial on how to get your clean desktop back.

Step 1: Getting the transparent image

This first thing you need to do is copy the below transparent image.  I’ve placed border around the image to make it easier to find.

transparent

To copy the image just right click on Safari and select the Copy Image  option as illustrated below.

CopyImage

Step 2: Get Info

The next step is to right mouse click on the .DS_Store file on your desktop and select “Get Info” as shown below.

getInfo

Step 3: Updating the icon

This opens a dialog with all of the information about your .DS_Store file.  You want to click on the document icon at the top left of the dialog and paste the image copied as part of step 1.

info-dialog

You will see the info dialog icon disappear.

step3-a

After closing the info dialog you will also notice that the .DS_Store icon disappears on your desktop.  Now you just need to repeat the process for each Dot file you wish to hide.

step3

Step 4: Hiding the icon text

Now that the icons are hidden, you will simply see the icon text.  I haven’t found a good way to remove the text but this is easy enough to hide.  If you simply drag the icons off screen, for example to the bottom right of your screen as shown below. Their text will be out of view and they will be out of sight out of mind.

dock-icon-hidden

Although this is a major hack, it does provide those of us that like to have a clean desktop some peace.

Results

Below shows the before and after results.

beforeafter

Wallpaper by Justin Maller.

Plugin to help fight the DerivedData Junk Drawer

It just takes creating a few example projects and suddenly you have a ton of simulator folders to deal with. Over time this becomes a junk drawer of simulator references to experiments and samples.

Toshihiro Morimoto (dealforest) has released the Cichlid Xcode plugin to help combat this.  It is super simple to use and simply deletes the DerivedData associated with your project when you run Product-> Clean.  I’ve been using it for the last week and it simply works.

Give it a try if you are looking for an easy way to clean the ~/Library/Developer/Xcode/DerivedData folders associated with your project.

Learn more on Github : https://github.com/dealforest/Cichlid

banner

Resetting the iOS Simulator

There seems to be several ways to reset or clear the iOS simulator ranging from pressing the “Reset Content and Settings..” button on the iOS simulator from deleting folder under ~/Library/Developer/CoreSimulator/DerivedData.

For me the easiest and faster approach for my workflow is to simply issue the below in terminal.

xcrun simctl erase all

Android orientation locking for Titanium

Locking the orientation of a Titanium Android app if fairly straightforward after you’ve done it once or twice.  I thought it would be helpful to outline the steps that I follow to lock several of my applications in a portrait orientation.

To start with you first must build your Titanium Android app.  It doesn’t matter if you have any functionality in your project yet.  You just need to make sure an AndroidManifest.xml file is created in your project’s build/android folder.  Once you have done a build, you will need to open your AndroidManifest.xml and look for the android:name property of your launch activity.  You can see an example of this in the below snippet found in our AndroidTest AndroidManifest.xml. Look for the .AndroidtestActivity line.  This is the line and pattern you will want to look for in your project’s AndroidManifest.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
	<application android:icon="@drawable/appicon" 
           android:label="AndroidTest" 
           android:name="AndroidtestApplication" 
           android:debuggable="false" 
           android:theme="@style/Theme.AppCompat">
		<activity 
                 android:name=".AndroidtestActivity" 
                 android:label="@string/app_name" 
                 android:theme="@style/Theme.Titanium"       
                 android:configChanges="keyboardHidden|orientation|screenSize">
		   <intent-filter>
		     <action android:name="android.intent.action.MAIN"/>
		     <category android:name="android.intent.category.LAUNCHER"/>
		    </intent-filter>
		</activity>
	</application>
</manifest>

You can see the naming convention is a period then your project name with the first letter capitalized followed by Activity.  In our example project this is .AndroidtestActivity. This is the pattern you will want to look for in your AndroidManifest.xml.

Once you have the activity name of your launch activity you will want to replace the default Android configuration with a new template.

First open your project’s tiapp.xml and locate the below default Android entry.

<android xmlns:android="http://schemas.android.com/apk/res/android"/>

Replace the default information with the below template.

    <android xmlns:android="http://schemas.android.com/apk/res/android">
        <manifest>
            <application>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:screenOrientation="portrait"
                    android:name="[replace me]">
                    <intent-filter>
                        <action android:name="android.intent.action.MAIN"/>
                        <category android:name="android.intent.category.LAUNCHER"/>
                    </intent-filter>
                </activity>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:name="org.appcelerator.titanium.TiActivity" 
                    android:screenOrientation="portrait"/>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:name="org.appcelerator.titanium.TiTranslucentActivity"
                    android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Translucent"/>
            </application>
        </manifest>
    </android>

Next replace the [replace me] placeholder with the name of the launch activity you found earlier.  The below example shows this replacement being done for our AndroidTest app.  Please remember to remove the brackets when making the replacement.

    <android xmlns:android="http://schemas.android.com/apk/res/android">
        <manifest>
            <application>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:screenOrientation="portrait"
                    android:name=".AndroidtestActivity">
                    <intent-filter>
                        <action android:name="android.intent.action.MAIN"/>
                        <category android:name="android.intent.category.LAUNCHER"/>
                    </intent-filter>
                </activity>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:name="org.appcelerator.titanium.TiActivity" 
                    android:screenOrientation="portrait"/>
                <activity
                    android:configChanges="keyboardHidden|orientation|screenSize"
                    android:name="org.appcelerator.titanium.TiTranslucentActivity"
                    android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Translucent"/>
            </application>
        </manifest>
    </android>

Next you will need to tell Alloy to only create Windows with a portrait orientation.  This is super easy and can be done by adding one class to your app.tss as shown below.  Your app.tss can be located in your app/styles folder.  If your project doesn’t already contain an app.tss you can create one by simply creating a file called app.tss in your project’s style folder.  To learn more about global styles please read the documentation available here.  The following snippet creates a global style for the windows in your project setting the orientation to portrait.

"Window" : {
	orientationModes :[
		Ti.UI.PORTRAIT
	]
}
FAQ:
  • Q: Why lock at the Android Activity level, isn’t adding the Alloy style enough?
  • A: Unfortunately no. The Alloy style is applied after the window is created. So if the device is held in a landscape orientation you will see the window open and turn. Locking at the Android Activity level stops this behavior.
  • Q: Does this mean all of my Ti.UI.Window object will be locked in portrait?
  • A: Yes, this locks both at the Android Activity and Titanium object levels.
  • Q: Can I later change the orientation by updating my Ti.UI.Window orientation?
  • A: No, orientation is locked at the Android Activity level as well.

Using IANA Time Zones with NSTimeZone

As part of a recent project I’ve been spending more time then I’d like with time zones.  In my case a variety of dates and time zone details are provided by a remote service. Having spent the last few years solving this type of problem with moment.js I was excited to see that NSTimeZone contained many of the same features.

The time zone information is provided in the IANA time zone database format.  I was really existed to see that you can create a new NSTimeZone using an IANA time zone name.  Below is an example using the Kuala Lumpur time zone.

let timeZone = NSTimeZone(name: "Asia/Kuala_Lumpur")
//What is the abbreviation
timeZone?.abbreviation
//Get the description with everything
timeZone?.description

So my first experiment was to parse the IANA time zones into a Swift String Array so that I could write a few Playground tests.  For this blog post we will show a few of the time zone names, instead of the full 417 item array.

let IANAtimeZones = ["Europe/Andorra","Asia/Dubai","Asia/Kabul",...]

* You can see the full array here

Next I found that the NSTimeZone method knownTimeZoneNames provides a list of all time zones iOS supports out of the box.  This gives me an easy way for me to later compare the IANA time zone database with those supported by iOS.  I use the below to get a list of those time zones known by iOS.

let iOStimeZones = NSTimeZone.knownTimeZoneNames()

To make writing any comparison testing alittle easier we create a method called tzExists.  This will allow us to compare a provided time zone name against all of those known by iOS.  In your production code you will want to cache the knownTimeZoneNames array for performance reasons.

func tzExists(name : String) -> Bool {
    return NSTimeZone.knownTimeZoneNames().contains(name)
}

Now that we have everything setup a simple loop can be used to compare our IANA time zones with those known to iOS.

for tz in IANAtimeZones {
    if(!tzExists(tz)) {
        print(tz + " not available")
    }
}

Oddly enough there are differences.  It could be I have an incomplete version of the IANA time zone database as iOS has 10 more known time zones then my IANA test harness.  Even with these differences the straight forward usage of NSTimeZone solves my problem perfectly.

Additional Resources:

  • Playground : A playground which shows the above objects, tests, and methods is available here.
  • Gist : The playground is available as a gist here.

Settings Dialogs with Titanium

The handling for prompts for network, location, and other settings is fairly common when building apps.  I think we have all written code that verifies a network connection and then prompts the user to check their network settings.

For a recent project I encapsulated a network and location prompt into a CommonJS model called SettingsDialog.  This is just a light wrapper around Ti.UI.AlertDialog.  You can find the module here along with a sample app.js.

Network Example

Many of my apps deal with travel, so I end up dealing with network issues more than I would like.  Once a network issue has been identified you might have to alert the user that a feature is unavailable to them do to their network conditions.  The following example shows how to use the module to create a cross platform dialog with a network settings button.  When clicked on Android this will take the user directly to their Network Settings and on iOS will take them directly to their Application Settings.

The below is an example how to create a cross platform network dialog as shown below. When the user taps on the Settings button, they are taken to the device’s settings screen.  On iOS you are taken to the App’s Settings and on Android you are taken directly to the Network Settings screen.

Code:


//Before we get started, require the module into our project
var alertSettings = require("settings-dialogs");
	alertSettings.prompt({
		title:"Information",
		message:"We can't find a network connection. Please check your settings.",
		buttonNames:["Settings", "Continue"],
		settingsType : alertSettings.SETTINGS_TYPE.NETWORK, //The type of prompt
		settingsIndex : 0 //What button index should launch the settings
	}, function(d){
		console.log("prompt results = " + JSON.stringify(d));
	});

UI Example:

ios_network android_network

Location Example

With greater permissions being applied to both iOS and Android likely will need to handle the scenario where the user might change location permissions needed for a feature.  You can use the SettingsDialog module to create a prompt asking them to enable Location Services and providing a Settings button to jump them to the Settings screen to make the change.  The below example illustrates how to use the module to create cross platform location dialogs.

Code:


//Before we get started, require the module into our project
var alertSettings = require("settings-dialogs");
	alertSettings.prompt({
		title:"Information",
		message:"Please enable location services to use this feature.",
		buttonNames:["Settings", "Continue"],
		settingsType : alertSettings.SETTINGS_TYPE.LOCATION_SERVICES, //The type of prompt
		settingsIndex : 0 //What button index should launch the settings
	}, function(d){
		console.log("prompt results = " + JSON.stringify(d));
	});

UI Example:

ios_locationandroid_network

A full gist is available here.

CaptiveNetwork Depreciated in iOS9

If you use Network SSIDs in your application you most likely have a breaking change or two in store for you in upgrading to iOS9.  Most likely you are using a block of code that looks something like the below to get the current SSID of the device.

#import <SystemConfiguration/CaptiveNetwork.h>

-(NSString*) findSSID {
#if TARGET_IPHONE_SIMULATOR
 	return @"simulator";        
#else
	NSString * informationUnknown = @"unknown";        
	CFArrayRef interfaces = CNCopySupportedInterfaces();
	if(interfaces == nil){
		return informationUnknown;
	}
	CFIndex count = CFArrayGetCount(interfaces);
	if(count == 0){
		return informationUnknown;
	}
	CFDictionaryRef captiveNtwrkDict =
	CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(interfaces, 0));
	NSDictionary *dict = ( __bridge NSDictionary*) captiveNtwrkDict;
	CFRelease(interfaces);

	return (([dict objectForKey:@"SSID"]==nil)? informationUnknown :[dict objectForKey:@"SSID"]);
#endif
}

A quick look at the prerelease API documentation shows almost all of CaptiveNetwork’s properties and methods are depreciated.  This Apple developer forum post, highlights how you will now need to use the NetworkExtension framework’s new NEHotspotHelper class to access this information.

The below is how in iOS9 you can get an NSArray of all of the supported network interfaces.

#import <NetworkExtension/NetworkExtension.h>  

NSArray * networkInterfaces = [NEHotspotHelper supportedNetworkInterfaces];  
NSLog(@"Networks %@",networkInterfaces);  

The NEHotspotHelper class is part of the Hotspot improvements introduced in the NetworkExtension  framework.  There is a great WWDC video, “What’s New in Network Extension and VPN” which goes into detail on these new features and the associated breaking changes.

Unfortunately implementing NEHotspotHelper isn’t as easy as adding a conditional, you will need to add the new “com.apple.developer.networking.HotspotHelper”  entitlement in your project.  You will need to contact Apple to request this entitlement and complete a short questionnaire before this option will appear

Unlike most entitlements, you will need to contact Apple as outlined here to request access to the Network Extension entitlements for your team.  I’m still in the approval process so will see how easy it will be to switch to NEHotspotHelper.

Helpful Links: