Play keyboard click sound in a collection view controller - ios

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.

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.

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.

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).

Related

Custom UIView doesnt show

I have very simple UIView that creating the box, but what is happen is the UIView does not show at all, here is my code on sharingButtons.m
-(void)createContainer{
winWidth = [UIScreen mainScreen].bounds.size.width;
buttonContainer = [[UIView alloc] initWithFrame:CGRectMake(0, 0, winWidth, 20)];
buttonContainer.backgroundColor = [UIColor redColor];
[self.view addSubview:buttonContainer];
}
-(void)createButton{
[self createContainer];
}
and here is my sharingButtons.h
#interface SocialSharing : UIViewController {
int winWidth;
}
- (void)createButton;
- (void)createContainer;
#pragma mark - Properties
#property(nonatomic, strong) UIView* buttonContainer;
#end
And createButton method is called from MyViewControler.m on viewDidLoad
Is any wrong with my code??
EDITED
Here is my code on MyViewControler.m
- (void)loadSocialSharingButton {
socialButtons = [[SocialSharing alloc] init];
[socialButtons createButton];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self loadSocialSharingButton];
}
Sorry I just learn about obj c :)
Thanks a lot
The reason your buttonContainer is not visible is, it is not loaded in your view hierarchy.
To make it visible you should add it as subview. In MyViewController.m in viewDidLoad add the following line after [self loadSocialSharingButton];
[self.view addSubview:socialButtons.buttonContainer];
Hope this Helps!
Your SocialSharing is subclass of UIViewController.
And you add your buttonContainer view to this SocialSharing Controller,this controller is not on screen if you just call
socialButtons = [[SocialSharing alloc] init];
[socialButtons createButton];
So,you can not see anything.
In an iOS App, only one ViewController at a time is active. And as you are at MyViewController, so MyViewController is active, if you want to navigate to any other view controller than you need to present or push the instance of same. Doing so will make the other view controller as active.
In your case the problem is your SocialSharing is a subclass of UIViewController as it is created as SocialSharing : UIViewController and it's not active, so adding any view over it won't be visible as the instance of SocialSharing is not pushed/ presented. If you need to show the view from SocialSharing than either you subclass it from UIView or push/ present the instance of SocialSharing to make it's view active and visible.
You are currently # MyViewController and But you are loading your custom view # SocialSharing ViewController, Both ViewController are distinct and you can't just get your custom view at social sharing to MyViewController by initializing it.
You have change SocialSharing class as sub class of UIView and initialize this view and add to subview of MyViewController.
SocialSharing.h
#interface SocialSharing : UIView {
int winWidth;
}
- (instancetype)createButton;
#pragma mark - Properties
#property(nonatomic, strong) UIView* buttonContainer;
#end
SocialSharing.m
- (instancetype)createButton
{
winWidth = [UIScreen mainScreen].bounds.size.width;
self = [super initWithFrame:CGRectMake(0, 0, winWidth, 20)];
if (self) {
buttonContainer = [[UIView alloc] initWithFrame:];
buttonContainer.backgroundColor = [UIColor redColor];
[self addSubview:buttonContainer];
}
return self;
}
MyViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
[self loadSocialSharingButton];
}
- (void)loadSocialSharingButton {
socialButtons = [SocialSharing alloc] createButton];
[self.view addSubView:socialButtons];
}

Confused about addTarget pointer behavior for addTarget:action:forControlEvents:

I have a UIView subclass with a delegate property. In the init method, I set
self.delegate = nil.
The view also has a button, so in the init method, I also set the target of the button to be self.delegate, which is nil:
[myButton addTarget:self.delegate action:#selector(buttonAction) forControlEvents:UIControlEventTouchUpInside]
In the UIViewController that sets up my UIView subclass, I call a method in the UIView that sets the UIView's self.delegate to the UIViewController. When I click the button, the change in target seems to be reflected.
I am wondering how this ends up working, as my understanding is that addTarget:action:forControlEvents takes an id as the target, and pointers should be pass by value in Obj-C. Thus, I am pretty confused about why the originally nil-valued pointer was updated after the addTarget method was already called.
The right way to do that is declaring a protocol for your view, which will delegate for button's tap action, i.e.
YourView.h
#class YourView;
#protocol YourViewDelegate
#optional
- (void)customView:(YourView *)view didSelectButton:(id)button;
#end
#interface YourView : UIView
//...
#property (weak, nonatomic) id <YourViewDelegate> delegate;
#end
YourView.m
#interface YourView()
#end
#implementation
- (id)init
{
if (self = [super init]) {
//...
[self setup];
}
return self;
}
- (void)awakeFromNib
{
//...
// setup logic when this view created from storyboard
[self setup];
}
- (void)setup
{
[myButton addTarget:self
action:#selector(buttonTapped:)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)buttonTapped:(id)sender
{
if (self.delegate && [self.delegate respondsToSelector:#selector(customVIew:didSelectButton)] {
[self.delegate customView:self didSelectButton:sender];
}
}
#end
Then, in your view controller implement YourViewDelegate category:
#interface YourViewController()
//...
#end
#implementation
- (void)viewDidLoad
{
[super viewDidLoad];
//...
self.yourView.delegate = self;
}
//...
- (void)customView:(YourView *)view didSelectButton:(id)button
{
//do your stuff
}
#end
Objective-C uses Dynamic binding. Method to invoke is determined at runtime instead of at compile time. Which is why it is also referred to as late binding.
Reference link -
https://developer.apple.com/library/ios/documentation/general/conceptual/DevPedia-CocoaCore/DynamicBinding.html
So what will be the delegate and which method is being called is defined at runtime.

ARC and autoreleased object

I need a ViewController to be called modally to show some UIButton and other UIView on top of the current window. I want the background to be partially transparent and showing the current window below it - something similar to a UIActionSheet but with a custom design. I coded my VC to do the following: 1) during init the VC sets self.view.frame equals to [[UIApplication sharedApplication]keyWindow].frame 2) when show() is called the VC adds self.view on top of [[UIApplication sharedApplication]keyWindow] subViews 3) when an internal button calls the private method release() the VC remove self.view from its superview. Example with a single release button as follows:
#implementation ModalController
- (id)init
{
self = [super init];
if (self){
//set my view frame equal to the keyWindow frame
self.view.frame = [[UIApplication sharedApplication]keyWindow].frame;
self.view.backgroundColor = [UIColor colorWithWhite:0.3f alpha:0.5f];
//create a button to release the current VC with the size of the entire view
UIButton *releaseMyselfButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[releaseMyselfButton setTitle:#"Release" forState:UIControlStateNormal];
releaseMyselfButton.frame = CGRectMake(0, 0, 90, 20);
[releaseMyselfButton addTarget:self action:#selector(releaseMyself) forControlEvents:UIControlEventTouchDown];
//add the button to the view
[self.view addSubview:releaseMyself];
}
return self;
}
- (void) show
{
//add self.view to the keyWindow to make sure that it will appear on top of everything else
[[[UIApplication sharedApplication]keyWindow] addSubview:self.view];
}
- (void)releaseMyself
{
[self.view removeFromSuperview];
}
#end
If I create an instance of ModalController from another VC and I call show() everything goes as expected:
#interface CurrentVC ()
#property (strong, nonatomic) ModalController *myModalController;
#end
#implementation CurrentVC
- (void)viewDidLoad
{
[super viewDidLoad];
self.myModalController = [[ModalController alloc]init];
[self.myModalController show];
}
#end
To make it work I need to retain the ModalController in a property until release () is called. However I would like to have the same freedom I have with UIActionSheet and simply keep an instance of it in a local variable:
#implementation CurrentVC
- (void)viewDidLoad
{
[super viewDidLoad];
ModalController *myModalController = [[ModalController alloc]init];
[myModalController show];
}
#end
If I do this with the current code ARC will release myModalController straight after show() is called and the release button will be pointing to nil. How can I make this work without storing the object in a property? I've identified a work around but I'm not sure it's a good design option:
#interface ModalController ()
#property (strong, nonatomic) ModalController *myselfToAutorelease;
#implementation ModalController
- (id)init
{
self = [super init];
if (self){
... ... ...
self.myselfToAutorelease = self;
}
return self;
}
- (void) show
{
... ... ...
}
- (void)releaseMyself
{
[self.view removeFromSuperview];
self.myselfToAutorelease = nil;
}
What I've done is making ModalController "self sufficient" - it stores a pointer to itself during init and set it to nil when it's ready to release himself. It works but I have the feeling that this is against the ARC design principles! Is this approach correct? If not, how can I handle this differently?
Thanks in advance for your help.
Doesn't work like that.
You don't keep a reference to self.
In the main view controller you just create your object. If you need it to be around longer keep it in a property in the main view controller , when done, set the property to nil in the main view controller.

How to customize a ui item, and apply that to all viewcontrollers?

I have a UITextField, that I had customized in my firstViewController.
Now I wan't it to have the same behavior on the other ViewControllers.
Is there anyway to import all the properties on a IBOutlet?
simple create your own Customclass and set the property in the constructor.
#interface CustomTextField : UITextField
#end
#implementation CustomTextField
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//Customize here
self.autocapitalizationType = UITextAutocorrectionTypeDefault;
self.text = #"Blub";
....
}
return self;
}
#end
if your create a new Textfield, create the object with your custom class:
CustomTextField *field = [[CustomTextField alloc] initWithFrame: ...];
Why don't you define a base view controller, then derive all of your view controllers from it?
#interface MyBaseCustomViewController : UIViewController
...
#property(...) UITextField* ...
...
#end
#interface MyOtherCustomViewController : MyBaseCustomViewController
...
Yes you could make a subclass of UITextField, this subclass will have the all the custom code that you did in the viewcontroller as a function in your subclass.
For example
- (void) viewDidLoad
{
//UITextField *textField;
//change color, background, size etc..
}
Now create a new class called UICustomTextField that derives from UITextField
in this class create a method:
//in UICustomTextField.m
- (void) doCustomModifications
{
self.stuff = custom stuff;
other custom stuff
etc...
}
Call doCustomModifications in your code
- (void)viewDidLoad
{
[customTextField doCustomModifications];
}

Solution for tracking state changes of UIButton

UIButton provides many state-dependent settings (image, titleColor, etc.).
I have manually added a subview to a button which shall react to the buttons state changes.
How would I do that? Should I try to map UIControlEvents on state changes?
You could do it by adding KVO observers for the button's selected and highlighted properties, but that's a lot more complicated than creating a subclass of UIButton and overloading the setSelected and setHighlighted methods. You'd do that like this:
//MyCustomButton.h
#interface MyCustomButton : UIButton
#end
//MyCustomButton.m
#implementation MyCustomButton
- (void)setUp
{
//add my subviews here
}
- (id)initWithFrame:(CGRect)frame
{
//this is called when you create your button in code
if ((self = [super initWithFrame:frame]))
{
[self setUp];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
//this is called when you create your button in interface builder
if ((self = [super initWithCoder:aDecoder]))
{
[self setUp];
}
return self;
}
- (void)setSelected:(BOOL)selected
{
super.selected = selected;
//update my subviews here
}
- (void)setHighlighted:(BOOL)highlighted
{
super.highlighted = highlighted;
//update my subviews here
}
#end
You can then create your custom buttons in code, or them in interface builder by dragging a regular UIButton onto your view and then settings its class to MyCustomButton in the inspector.

Resources