Solution for tracking state changes of UIButton - ios

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.

Related

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

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.

View Controller: how to correctly initialize values that depend on NIB

I'm wondering what the best way is to initialize values that depend on objects in a NIB. For example, let's say I have a UIView that gets a custom cornerRadius and borderColor.
Right now what I do is
#interface MyViewController ()
#property (nonatomic, weak) IBOutlet UIView *roundyView;
#end
#implementation MyViewController
- (id)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// (!) Can't assign to roundyView, hasn't been loaded from NIB yet
// ...
}
return self;
}
-(void)viewDidLoad {
// Ahh, NIB loaded, roundyView has a value
self.roundyView.layer.cornerRadius = 5.0f;
self.roundyView.layer.borderColor = [UIColor redColor].CGColor;
}
#end
So far, so good. Next I add a setter, so I can change the border color from elsewhere in the program.
#property (nonatomic, strong) UIColor *roundBorderColor;
And
-(void)setRoundBorderColor:(UIColor*)roundBorderColor {
_roundBorderColor = roundBorderColor;
self.roundyView.layer.borderColor = roundBorderColor.CGColor;
}
The problem is that I usually call that accessor while instantiating the class, but before it is presented. Something like
MyViewController *vc = [[MyViewController alloc] initWithNibName:#"MyViewController"
bundle:nil];
// Setting the color, NIB hasn't loaded though (!)
vc.roundBorderColor = [UIColor yellowColor];
[self presentViewController:vc animated:YES completion:nil];
This doesn't work, since the setter runs before viewDidLoad. So instead I usually put cruft like this:
-(void)viewDidLoad {
// ...
if (_roundBorderColor != nil) {
// The setter was already called somewhere,
// call it again now that we have the NIB
[self setRoundBorderColor:_roundBorderColor];
}
}
Is there a cleaner way to deal with this?
Goto the identity inspector in the Interface Builder (third option from the left in the interface builder view), and for your view - set the "User Defined Runtime Attributes" and add the following:
Keypath: layer.cornerRadius Value: number, 5.
Unfortunately - this doesn't seem to work for the border color, as the UI allows only "Color" but not CGColor.
A simple workaround to keep your code clean from colors would be to set a user defined property to your view controller and set it via your nib.
In your view controller:
#property (nonatomic,strong) UIColor* borderColor;
And then in the interface builder, set the color via User Defined Runtime Attributes to whatever you want.
Then set the color to the layer's border in viewDidLoad.
Involves some code and not just UI Builder, but at least there is no need to specify the color in the code.
Or, if you just want to set the color using code and don't want to wait for "viewDidLoad" you can do something like:
-(void)setRoundBorderColor:(UIColor*)roundBorderColor {
[self view]; // force view load from nib
self.roundyView.layer.borderColor = roundBorderColor.CGColor;
}
This problem is elegantly solved starting in Xcode 6 by using the IBInspectable feature. For example, in a UIButton subclass:
#IBInspectable var borderColor : UIColor? {
get {
let cg = self.layer.borderColor
return cg == nil ? nil : UIColor(CGColor: cg!)
}
set {
self.layer.borderColor = newValue?.CGColor ?? nil
}
}
An instance of this button in the storyboard / xib now shows a "borderColor" color pop-up menu in the Attributes inspector.
This sets a UIColor, but our property is just a facade for the layer's borderColor and translates between UIColor and CGColor.
You can make it a little cleaner by initialising your property in initWithNibName - that way you can safely access the property in viewDidLoad without the check.
-(id)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
_roundBorderColor = [UIColor redColor];
}
return self;
}
-(void)viewDidLoad {
// ...
[self setRoundBorderColor:_roundBorderColor];
}
You should use isViewLoaded method.
- (void)setRoundBorderColor:(UIColor *)roundBorderColor {
_roundBorderColor = roundBorderColor;
// would be launched only if view is initialised
if ([self isViewLoaded]) {
self.roundyView.layer.borderColor = roundBorderColor.CGColor;
}
}
- (void)setBorderRagius:... // same idea as before
- (void)viewDidLoad {
self.roundyView.layer.cornerRadius = self.cornerRadius;
self.roundyView.layer.borderColor = self.borderColor;
}
And you need to set some default values to cornerRadius and borderColor properties. And do nothing if nil value is going to be set.

How do I set a nonzero initial value in iOS?

I have an ivar which is mentioned in my header
#interface MyClass : UIView{
int thistone;}
- (IBAction)toneButton:(UIButton *)sender;
#property int thistone;
#end
and I have synthesized it in the implementation:
#implementation MyClass
#synthesize thistone;
- (IBAction)toneButton:(UIButton *)sender {
if(thistone<4)
{thistone=1000;} // I hate this line.
else{thistone=thistone+1; }
}
I cannot find (or find in any manual) a way to set a nonzero initial value. I want it to start at 1000 and increase by 1 each time I press the button. The code does exactly what I intend, but I'm guessing there's a more proper way to do it that saves me the if/else statement above. Code fixes or pointers to specific lines in online documentation greatly appreciated.
Every object has a variant of the init method called at instantiation. Implement this method to do such setup. UIView in particular have initWithFrame: and initWithCoder. Best to override all and call a separate method to perform required setup.
For example:
- (void)commonSetup
{
thisTone = 1000;
}
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
[self commonSetup];
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder
{
if (self = [super initWithCoder:coder])
{
[self commonSetup];
}
return self;
}

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];
}

Resources