Berry Blue
Berry Blue

Reputation: 16522

Play keyboard click sound in a collection view controller

I created a subclass of a UICollectionViewController that is used as the custom inputAccessoryViewController in a UITextView. https://developer.apple.com/reference/uikit/uiresponder/1621124-inputaccessoryviewcontroller

I want to play the keyboard click sound when you tap a cell in the collection view using playInputClick. https://developer.apple.com/reference/uikit/uidevice/1620050-playinputclick

I cannot figure out how to get this to work in a collection view. It works for a simple view like this using the inputAccessoryView property of a UITextView but I'm not sure what view to subclass in the collection view controller hierarchy to get the keyboard click sound to play.

@interface KeyboardClickView : UIView <UIInputViewAudioFeedback>
@end

@implementation KeyboardClickView
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
        [self addGestureRecognizer:tap];
    }
    return self;
}

- (void)tap:(id)sender
{
    [[UIDevice currentDevice] playInputClick];
}

- (BOOL)enableInputClicksWhenVisible
{
    return YES;
}
@end

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{   
    _inputAccessoryView = [[KeyboardClickView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
    _inputAccessoryView.backgroundColor = [UIColor redColor];
    [[UITextView appearance] setInputAccessoryView:_inputAccessoryView];

    // ...
}
@end

I'm also aware that you can play the keyboard click sound using AudioServicesPlaySystemSound(1104) but this doesn't respect the user's settings if they have the keyboard click sounds disabled.

Upvotes: 7

Views: 900

Answers (3)

bteapot
bteapot

Reputation: 2037

To use the benefits of playInputClick in UIViewController:

Dummy input accessory view:

@interface Clicker : UIView <UIInputViewAudioFeedback>

@end

@implementation Clicker

- (BOOL)enableInputClicksWhenVisible
{
    return YES;
}

@end

View with input accessory view:

@interface ControllerView : UIView

@end

@implementation ControllerView

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

- (UIView *)inputAccessoryView
{
    return [[Clicker alloc] init];
}

@end

View Controller with custom view:

@implementation ViewController

- (void)loadView
{
    self.view = [[ControllerView alloc] init];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.view becomeFirstResponder];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.view resignFirstResponder];
}

@end

When the responder object becomes the first responder and inputView (or inputAccessoryView) is not nil, UIKit animates the input view into place below the parent view (or attaches the input accessory view to the top of the input view).

There is no visual consequences since the height of Clicker view is zero, and conforming to UIInputViewAudioFeedback protocol enables [[UIDevice currentDevice] playInputClick] functionality within ViewController.

Look here for responder chains and here for input accessory views.

Upvotes: 4

dvdblk
dvdblk

Reputation: 2966

Here's a working Swift 4.2 (iOS 11 and 12) version of bteapot's answer.

private class ClickerDummyView: UIView, UIInputViewAudioFeedback {
    var enableInputClicksWhenVisible: Bool { return true }
}

private class ClickerControllerView: UIView {
    override var canBecomeFirstResponder: Bool {
        return true
    }

    override var inputAccessoryView: UIView? {
        return ClickerDummyView()
    }
}


class ClickingViewController: UIViewController {

    override func loadView() {
        self.view = ClickerControllerView()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        view.becomeFirstResponder()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        view.resignFirstResponder()
    }
}

Now call UIDevice.current.playInputClick() from within the ClickingViewController class whenever needed and the keyboard click sound will be triggered (with respect to system settings).

Upvotes: 0

Casey
Casey

Reputation: 6701

Instead of using a UICollectionViewController, define KeyboardClickView as a subclass of UICollectionView and place it on a UIViewController.

@interface KeyboardClickView : UICollectionView <UIInputViewAudioFeedback>

- (id)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {
    self = [super initWithFrame:frame collectionViewLayout:layout];

    // existing implementation

The new view controller could look something like this:

@interface KeyboardClickViewController: UIViewController <UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) KeyboardClickView *clickView;
@end

@implementation KeyboardClickViewController

-(void)viewDidLoad {
    [super viewDidLoad];

    self.clickView = [[KeyboardClickView alloc] initWithFrame:self.view.bounds collectionViewLayout:[UICollectionViewFlowLayout new]];
    self.clickView.delegate = self;
    self.clickView.dataSource = self;
    [self.view addSubview:self.clickView];
}

// existing UICollectionViewController logic

@end

This allows you to make the call to playInputClick from a UIView instead of a UIViewController.

Upvotes: 1

Related Questions