Nick Harris

Suntracker

with one comment

Icon 120

Introducing Suntracker!

Suntracker is a simple app that uses Core Location to determine a users location then calculates sunrise and sunset times along with total daylight for the location based on the NOAA Sunrise/Sunset Calculator. Suntracker shows you a graph of 3 months, 6 months or a years worth of daylight time and the ability to drag you finger across the graph to see individual days. The today extension shows the days sunrise and sunset times along with how many hours and minutes the day has of sun light. Suntracker will also notify you each day when the sun rises and sets – even on your Apple Watch!

Built using iOS 9 and Swift 2, Suntracker is designed for use on iPhone 4S through the iPad Pro. Much attention has been paid toward battery usage as well as accessibility. It supports large and bold fonts on all devices as well as multitasking on all iPads.

Screenshots:

4S

4SPortrait    4SDayDrag

4SLandscape

6S Plus

6SPlusPortrait    6SPlusLandscape

6SPlusLandscapeSettings

iPad Air 2

IPadAirMultitaskingTwoThird

IPadMultitaskingHalf

IPadMultitaskingThird

Today Extension and Apple Watch

6SPlusToday   

AppleWatch 

Availability

Suntracker is only available on BitBucket for developers.

Do not release this app or its assets as your own. The code and icon are my copyright. If you’re interested in building an app like this I would encourage you to use EDSunriseSet.

Idea

Suntracker started as an idea a few years ago. I had spent months writing my book and wanted an idea to play with to get me back into everyday coding. It was the beginning of February at the time and I was interested in the sunrise/sunset times of each day as the calendar moved from Winter to Spring. I found the NOAA Javascript calculator and decided to port it to Objective-C. I had enough code to make the calculations based on Core Location but I never built out the UI.

Fast forward 2 years to this past February and again I was in search of a side project. Translating Javascript to Swift was interesting to me but I also wanted to take some time to do something more with Autolayout on multiple devices along with dynamic text. I ended up learning a lot more about Core Location along the way.

Code Highlights

Core Location

I spent a lot of time getting my head around all of the different ways to use Core Location. I ended up with an approach that asks for location data even in the background but still supported using location only while in-app. With iOS 8 you have the opportunity to supply strings explaining why the app needs location information using the NSLocationAlwaysUsageDescription and NSLocationWhenInUseUsageDescription keys in your info.plist. You can choose which you prompt the user for, but you only get one shot at it. I opted for always on but the user can change that in the Settings app if they wish:

4S Settings

When allowed background updates I used startMonitoringSignificantLocationChanges. The documentation for Core Location seems lacking to me but it felt like I could get the best experience while using the least amount of battery this way. I really wanted to use differed location updates but that only works by using the GPS radio with the best accuracy. This app doesn’t need GPS. Wifi or local cell tower triangulation are fine which is what I get with startMonitoringSignificantLocationChanges. I spent a few days with different devices driving around town while leaving others at home and everything worked as expected.

When only allowed in-app location access the code calls requestLocation. The system gives you one location then stops location monitoring. For an app like this its fine but the delay on launch to get a location is a bit annoying.

The Today Extension was more difficult. I couldn’t find a way to use CLLocationManagerDelegate in any way so it just uses the location property on the manager.

Here’s the function that gets called anytime the app is launched or comes to the foreground:

func handleLocationManagerStatus(status: CLAuthorizationStatus) {
        
    switch status {
    case .NotDetermined:
        // need to ask for location permissions
        coreLocationManager?.requestAlwaysAuthorization()
            
    case .AuthorizedAlways:
        // register for notifications only when location permissions have been granted
        UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes:[ .Sound, .Alert], categories: nil))
            
        // the app doesn't need a precise location so no need for GPS which will impact battery use
        // instead use significant location changes
        coreLocationManager?.startMonitoringSignificantLocationChanges()
            
    case .AuthorizedWhenInUse:
        // register for notifications only when location permissions have been granted
        UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes:[ .Sound, .Alert], categories: nil))
            
        // if we're only allowed in-app locations theres no need to get location updates as its highly unlikely the user will
        // be moving great enough distances to alter sunrise/sunset times. Instead just request the current location
        coreLocationManager?.requestLocation()
            
    case .Denied:
        // we need to be sure the view is loaded so the alert can be presented
        if let sunTrackerViewController = sunTrackerViewController where sunTrackerViewController.isViewLoaded() {
            sunTrackerViewController.displayLocationPermissionsAlert()
        }
            
    case .Restricted:
        // we need to be sure the view is loaded so the alert can be presented
        if let sunTrackerViewController = sunTrackerViewController where sunTrackerViewController.isViewLoaded() {
            sunTrackerViewController.displayLocationPermissionsRestrictedAlert()
        }
    }
}

Tuples

This is another app based primarily on NSOperations. After years of messy block to block implementations, I can’t imagine not breaking functionality into operations. The push toward functional programming aids in that thought. What I like about Suntracker is the one operation that calculates sunrise/sunset and other auxiliary operation groups that use it to complete the app.

I wrote about using tuples to pass data into operations and getting results out using operationInput and operationOutput properties. I really like the approach and plan on continuing that pattern. For example:

class CalculateSunriseSunsetOperation: NSOperation {
    
    // MARK: Public Properties
    var operationInput: (coordinate: CLLocationCoordinate2D?, date: NSDate?)
    var operationResult: (error: NSError?, sunriseTimeInMinutes: Double?, sunsetTimeInMinutes: Double?)
 
    . . . 

Just yesterday I saw a post about tuples being lightweight / limited scope structs. I think that’s a fair comparison.

Constants

I had been using enums for constants such as user default keys. I like that since it groups like values together but I was using rawValue everywhere which wasn’t ideal. For Suntracker I decided to use a struct instead. Here are my user default keys:

struct SuntrackerUserDefaultsConstants {
    static let UserDefaultsSuiteName = "group.cliftongarage.suntracker"
    static let SunriseTimeUserDefaultsKey = "SunriseTimeUserDefaultsKey"
    static let SunsetTimeUserDefaultsKey = "SunsetTimeUserDefaultsKey"
    static let TotalDaylightUserDefaultsKey = "TotalDaylightUserDefaultsKey"
    static let DaysUntilDSTSwitchUserDefaultsKey = "DaysUntilDSTSwitchUserDefaultsKey"
    static let LastLocalityUserDefaultsKey = "LastLocalityUserDefaultsKey"
    static let LastDayCalculatedUserDefaultsKey = "LastDayCalculatedUserDefaultsKey"
    
    static let SunriseNotificationsUserDefaultsKey = "SunriseNotificationsUserDefaultsKey"
    static let SunsetNotificationsUserDefaultsKey = "SunsetNotificationsUserDefaultsKey"
    
    static let LastLocationFetchUserDefaultsKey = "LastLocationFetchUserDefaultsKey"
    static let LastStringsOperationUserDefaultsKey = "LastStringsOperationUserDefaultsKey"
    
    static let SelectedTrendChartUserDefaultKey = "SelectedTrendChartUserDefaultKey"
}

They’re still grouped but there’s no need for rawValue everywhere in code to use them. Much better!

Today Extension

Suntracker also has a Today Extension.

I found developing for the Today view incredibly frustrating. I’d like to see data on how many users enable a today widget to determine if the struggle is worth the effort. As an iOS user I can’t imagine it is. I’ve seen some say they use the Today view often but personally I don’t.

What frustrated me the most was the odd auto layout constraints as well as the inability many times to connect the debugger. It was hard to tell if I could rely on the widgetPerfromUpdateWithCompletionHandler to get called every time the user viewed the Today panel. I found some saying to call code to update the view in viewWillAppear as well so that’s what I ended up doing.

Shared NSUserDefaults

Setting up shared NSUserDefaults was fun. Just make sure your user defaults suite name is exactly the same as your app group identifier.

Dynamic Text

Dynamic text was more difficult to support then I would like. Not because its all that hard but designing a user interface that would react well was difficult. Layouts that looked fine at regular size look horrible at large and bold. In the end I decided that calculated values were more important then simple labels so I went with shrinking the simple labels in order to keep the calculated values large. For instance, here’s a 4S in landscape with the largest text set to bold:

4SBoldBigText

Conclusion

This app was a lot of fun to finish. I spent a good portion of time working with Core Location and battery usage in order to get the best user experience but I think that time was well worth it. Also spending more time playing with auto layout in different size classes using different accessibility settings was great. I can’t imagine being a developer with a single test device though. I wrote this app using my 4S, 5, 6, 6S Plus, iPad Mini, iPad Air and iPad Pro. I needed all of them in order to find issues with device speed and well as layout differences.

All in all I’m just happy Spring is here! 

Code is available on BitBucket.

Advertisements

Written by Nick Harris

March 9, 2016 at 10:53 pm

Posted in Uncategorized

One Response

Subscribe to comments with RSS.

  1. If you wanted to make this pure Swift, you could replace the Objective-C dependency with https://github.com/kraigspear/SunriseSunsetSwift

    Kraig (@kraigspear)

    June 10, 2016 at 10:00 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: