Handling Remote Control Events.

Binaural accepts remote control events, making most app functions fully controllable via Control Center or the buttons on compatible earphones.

To handle remote control events, the first responder must implement the -remoteControlReceivedWithEvent: method. In my first implementation this was handled by my root UIViewController, but I noticed it was hovering around 300 lines of code - time to refactor.

The logic I wanted to extract included communicating with the synthesizer (i.e. the object that generates the binaural beats) and managing the timers used to handle the seek events. Nothing depended on other parts of the root controller. Additionally, I considered this piece of code as a separate, global entity - which should handle remote control events no matter what the current view and view controller hierarchy might be.

I decided to create a UIResponder subclass that encapsulates these responsibilities, and add it to the responder chain. I chose to insert this responder in the chain between the root controller and the window - a very global place, independent of the current view controller hierarchy. Perfect for the situation.

Here’s the desired responder chain:

The desired responder chain

This turned out to be pretty straightforward. Here’s the relevant code from the root view controller:

// From the @interface
@property (strong, nonatomic) UIResponder *nextResponder;

// From the @implementation
- (void)awakeFromNib {
    self.nextResponder = ({
        XXRemoteControlResponder *remoteControlResponder = XXRemoteControlResponder.new;
        remoteControlResponder.nextResponder = UIApplication.sharedApplication.keyWindow;
        remoteControlResponder;
    });
}

And here’s the XXRemoteControlResponder class:

// XXRemoteControlResponder.h
@interface XXRemoteControlResponder : UIResponder
@property (strong, nonatomic) UIResponder *nextResponder;
@end


// XXRemoteControlResponder.m
@implementation XXRemoteControlResponder

- (instancetype)init {
    self = [super init];
    if (!self) return nil;
    
    [UIApplication.sharedApplication beginReceivingRemoteControlEvents];
    
    return self;
}

- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
    // Handle remote control event
}

@end

To explain the above code:

  1. Both classes have a nextResponder property. This implicitly overrides the -nextResponder method, so whatever we set that property to will be considered the next responder for that object.
  2. We set the root controller’s next responder to our own XXRemoteControlResponder object.
  3. We set the XXRemoteControlResponder object’s next responder to the key window.

This ensures that our object will receive all unhandled remote control events.

As always: if there’s a better way to do this, I’d love to know.