Implementing a reimagined user interface: crown swipes

As noted in a previous post on the reimagined user interface for World Champ Tech’s family of workout apps, the Apple Watch screen has a significant issue: touch gestures on the screen don’t work terribly well when the screen is wet from perspiration or rain. The swipe gesture used to switch between screens in a page based interface is particularly challenging on a hot day when sweat is pouring over your hands and fingers, or when you are swimming in a pool. But, Apple allows developers to access the Digital Crown control through the WKCrownSequencer object, and this allows for an alternative to swipe gestures that works well when the screen is wet - just rotate the Digital Crown up on down to flip between data screens.

Implementing this novel user interface technique required a complex dance between various software objects, and an intricate software architecture. The WKCrownSequencer object has a delegate property that - once assigned - can receive data signals from the Digital Crown. The delegate can receive crownDidRotate:rotationalDelta: messages that include the amount and direction of each rotation update, and can also receive crownDidBecomeIdle: messages on the user stops rotating the Digital Crown. The root ExtensionDelegate object was assigned as the WKCrownSequencer delegate to receive the Digital Crown data messages. The ExtensionDelegate object also configures the array of interface controllers managed by a page controller. The ExtensionDelegate contained a selectedController property that tracks the currently visible interface controller. When the Digital Crown is rotated, the selectedController property is set based the next controller in the array depending on the direction of rotation of the Digital Crown. When the Digital Crown returns to rest, it outputs the crownDidBecomeIdle: message, and the ExtensionDelegate forwards this message to the array of of interface controllers notifying them of which interface controller should become the currently visible page displayed by the page controller.

Digital Crown page swipe gesture software implementation

One of the most important design decisions in the implementation of Digital Crown swipe gestures, was deciding which direction a Digital Crown rotation should index through the page controller’s array of interface controllers. Apple’s documentation notes for the rotationalDelta property state that “positive values always indicate an upward scrolling gesture, while negative numbers indicate a downward scrolling gesture,” independent of which wrist the watch is worn. For standard swipe touch gestures on the Apple Watch screen, a downward swipe indexes to a lower value in the page controller array, and a upward swipe indexes to a higher value in the page controller array. Apple’s intention is to make it feel like your swipe touch gesture is physically moving the interface controller on the screen. With the Digital Crown swipe gesture, since you are not directly touching the screen to simulate dragging the screen downward, we reversed this behavior to have an up motion index to a lower value in the page controller array and a down motion index to a higher value in the page controller array.

Digital Crown control up swipe motion rotates to a lower index selected controller in the page controller array

Digital Crown control down swipe motion rotates to a higher index selected controller in the page controller array

The crownDidRotate:rotationalDelta method is implemented in the ExtensionDelegate as:

-(void)crownDidRotate:(WKCrownSequencer *)crownSequencer rotationalDelta:(double)rotationalDelta
{
    if (rotationalDelta > 0)
    {
        // Positive Rotation (Go To Lower Index Interface Controllers - Index 0 is Topmost Controller)

        if (self.selectedController > 0)
        {
            self.selectedController--;
        }
    }
    else if (rotationalDelta < 0.0)
    {
        // Negative Rotation (Go To Higher Index Interface Controllers - Index N - 1 is Bottommost Controller)

        if (self.selectedController < (self.interfaceControllers - 1))
        {
            self.selectedController++;
        }
    }
}

The crownDidBecomeIdle: method simply notifies all of the interface controllers managed by the page controller that the Digital Crown has updated, and sends them the index of the selected interface controller in the page controller array.

-(void)crownDidBecomeIdle:(WKCrownSequencer *)crownSequencer
{
    [[NSNotificationCenter defaultCenter] postNotificationName:CrownUpdateNotification object:@(self.selectedController)];
}

Each interface controller configures the default notification center in its init method to observe the crown updates. When an interface controller receives the notification, it checks the value of the index passed along with the notification. If the index matches the stored interface controller index value, the interface controller becomes the current page after playing a short haptic click to provide feedback to the user.

__weak __typeof(self) weakSelf = self;

[[NSNotificationCenter defaultCenter] addObserverForName:CrownUpdateNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {

    __strong __typeof(self) strongSelf = weakSelf;

    if (((NSNumber *)(note.object)).integerValue == strongSelf.interfaceControllerIndex)
    {
        [[WKInterfaceDevice currentDevice] playHaptic:WKHapticTypeClick];

        [strongSelf becomeCurrentPage];
    }
}];

The final step in the dance between the interface controller and ExtensionDelegate is to reset the ExtensionDelegate’s WKCrownSequencer delegate to the interface controller’s WKCrownSequencer delegate when it becomes visible, and focus the interface controller’s WKCrownSequencer so that it receives data from the Digital Crown. Since the design still supports standard swipe touch gestures, the interface controller has to update the ExtensionDelegate’s selected controller index to insure that the ExtensionDelegate’s selected index matches the index of the visible interface controller.

-(void)didAppear
{    
    [super didAppear];

    // Inform Extension Delegate About Selected Controller Change

    ExtensionDelegate *extensionDelegate = (ExtensionDelegate *) [WKExtension sharedExtension].delegate;

    extensionDelegate.selectedController = self.interfaceControllerIndex;

    // Setup Crown Sequencer Focus

    self.crownSequencer.delegate = extensionDelegate;

    [self.crownSequencer focus];    
}
Previous
Previous

Implementing a reimagined user interface: a custom monospaced font timer

Next
Next

Implementing a reimagined user interface: buttons