Override UILabel's setText method? - objective-c

I'd like to override UILabel's setText method but I'm not sure that:
A) it's possible, and
B) if maybe there's a better way to achieve what I'm trying to accomplish.
I have a subclass of UIView that has a UILabel property as one of its sub-views. I'd like to know when the UILabel's "text" property changes so I can adjust the size of the rounded rect behind it. If I owned UILabel I'd just override setText...but UILabel is Apple's and its implementation is hidden.
So, how should I be going about this?

Subclasses of UILabel can override the setText method quite easily. I'm not really sure why this hasn't yet been included as a legitimate answer on this 4 year old question.
- (void) setText:(NSString *)text
{
[super setText:text];
[self sizeToFit];
}

You can use Key-Value Observing to track changes to the UILabel.text property. The approach involves three steps:
1) Registering to observe the property, when you load the view
[label addObserver:inspector
forKeyPath:#"text"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
2) Receiving a notification about any changes:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:#"text"]) {
// put your logic here
}
// be sure to call the super implementation
// if the superclass implements it
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
3) De-registering the observation whenever you aren't interested any more:
[label removeObserver:inspector forKeyPath:#"text"];

Matt answer is good for Objective-C, but doesn't work in Swift (normal, it didn't existed when he answered), the accepted answer from notnoop does work in swift, even though it is more complicated, just to give another idea in swift you can use the didSet:
class MyLabel: UILabel {
override var text: String? {
didSet {
if let text = text {
println("the new text is: \(text)")
} else {
println("the text has been set to nil")
}
}
}

Based on Drix answer, I think this is a more correct approach (using set instead of didSet):
class UnreadCountLabel: UILabel {
override var text: String? {
set {
if let newValue = newValue where !newValue.isEmpty {
super.text = " \(newValue) "
self.hidden = false
} else {
super.text = newValue
self.hidden = true
}
}
get {
return super.text
}
}
}

Are you just using a rounded rectangle as the background for the Label? If that is the case, you can look into using UIIMage stretchableImageWithLeftCapWidth:topCapHeight. This will take an image you've created that has a left and top repeating section with a width you specify and automatically stretch it to your width.
If not, Key-Value observing is the way to go. Just to cover another option--this is like "playing with fire," as Apple programmer Evan Doll said in one of his Stanford lectures--you can use method swizzling to exchange one method implementation for another.
void method_exchangeImplementations(Method m1, Method m2);
In this case, you want to tweak the implementation of setText, but you also want to call the original setText in UILabel. So you could exchange setText with setTextAndUpdateSize, and inside setTextAndUpdateSize do what setText does originally plus add on a little more. If you are confused or think this is a bad idea, it probably is. You can get a Method object to pass into method_exchangeImplementations by calling class_getInstanceMethod([NSSTring class], #selector (methodName).
Once your method swizzle has been called once, inside your new method you can then call the old implementation of setText from within the new one by using, yes, setTextAndUpdateSize. It's confusing and not recommended, but it works. A good example can be found in the developer sample code.

I pulled of Method Swizzling in Swift 2.0. Changing the font of the entire application by swapping the implementation of setText method of the label.
Copy the code in app delegate and use the customSetText to make application level changes
// MARK: - Method Swizzling
extension UILabel {
public override class func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}
// make sure this isn't a subclass
if self !== UILabel.self {
return
}
dispatch_once(&Static.token) {
let originalSelector = Selector("setText:")
let swizzledSelector = Selector("customSetText:")
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
// MARK: - Custom set text method for UI Label
func customSetText(text: String) {
self.customSetText(text)
//set custom font to all the labels maintaining the size UILabel
self.font = UIFont(name: "Lato-LightItalic", size: self.font.pointSize)
}
}

Related

How to replace pickerView:numberOfRowsInComponent method for 1 instance of pickerView

I want to create a pickerView programmatically and have it use it's own version of a method like pickerView:numberOfRowsInComponent.
I create the instance at runtime like this:
UIPickerView *myPickerView = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 200, 320, 200)];
myPickerView.delegate = self;
myPickerView.dataSource = self;
myPickerView.showsSelectionIndicator = YES;
[self.view addSubview:myPickerView];
The standard method called would be:
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
NSUInteger numRows = 5;
return numRows;
}
What I want to do is replace this standard method with another method for this instance only.
-(NSInteger)xxxpickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{ // special method for this instance only
return 1;
}
I've been able to use method swizzle to do this with other things, but I can't seem to get it to work with UIPickerView.
#implementation UIPickerView (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = #selector(pickerView:numberOfRowsInComponent:);
SEL swizzledSelector = #selector(xxxpickerView:numberOfRowsInComponent:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
I've listed the methods to see if the 2nd method was added to the instance during runtime and it is in the list.
However, the 2nd method doesn't run, the 1st method does run.
Here's a link to the post that got me started on this, and I've confirmed it works, but I seem to be missing something about this.
http://nshipster.com/method-swizzling/
I'm open to other suggestions, the problem I'm trying to solve is that I want to create the instance of a UIPickerView object that won't be dependent on another instance that will be running at the same time. So I want a different method that will work only with the one instance and completely ignore any other instances that might be running and I want to do this programmatically.
At lest one reason for not using a tag/switch, is that I don't know what the conditions will be until runtime.
I don't know why swizzle would work with one object and not another, and I'm open to other way to replace stock methods with others at runtime.
Is there something about the method I'm trying to replace that won't allow it to be replaced?
EDIT: in order to try and make the question clear, the following code in the link works. It swaps one method for another method. What I need to do is the same thing for another object and I can't figure out what it works for 1 object and not another.
This works for another object: http://nshipster.com/method-swizzling/
Here's another link as well: http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/
One simple way to do it is keep a copy of the pointer in a property and then compare pointers in the datasource/delegate methods.
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
if ( pickerView == self.myPickerView )
return 1; // response for this particular picker view
else
return 5; // standard response
}

Receiving NSDraggingDestination messages with a WKWebView

I'm porting an OS X app which was using WebView to using WKWebView, the new "modern WebKit API" introduced in OS X Yosemite. My previous WebView subclass supported dropping files onto it by first calling [self registerForDraggedTypes:#[NSFilenamesPboardType]] and then simply implementing - (BOOL)performDragOperation:(id < NSDraggingInfo >)sender.
This doesn't work with the new WKWebView, as performDragOperation never gets called, nor do any of the NSDraggingDestination protocol methods that I tried.
I also tried making a parent NSView implement the protocol, and I'm still not getting the messages. Removing the WKWebView from the hierarchy makes the parent NSView receive those messages.
I also tried implementing the WKNavigationDelegate protocol to prevent the default drop behaviour of WKWebView to happen and this didn't change a thing either.
Edit: Upon further inspection (suggested by Scott Kyle / #appden on twitter), a private class WKView that implements the NSDraggingDestination protocol is a subview of the WKWebView. My code should likely try to get the dragging notifications before the WKView sees them and acts on them.
The only solution I found, thanks to Scott Kyle, was to replace the performDragOperation: method defined on WKView with my own implementation using object_getClass, class_getInstanceMethod and method_exchangeImplementations from the Objective-C runtime. Hopefully, in the future, the WKUIDelegate protocol will be extended to support custom hooks into the drag-and-drop protocol as implemented privately by WKWebView.
Here's an example showing how we exchange the implementation of the nested view's performDragOperation: with our own from our WKWebView subclass:
// Override the performDragOperation: method implemented on WKView so that we may get drop notification.
var originalMethod = class_getInstanceMethod(object_getClass(subviews[0]), "performDragOperation:")
var overridingMethod = class_getInstanceMethod(object_getClass(self), "performDragOperation:")
method_exchangeImplementations(originalMethod, overridingMethod)
And then the implementation where we delegate to a dropDelegate object.
override func performDragOperation(sender: NSDraggingInfo) -> Bool {
let myWebView = superview as MyWebView
if let dropDelegate = myWebView.dropDelegate {
return dropDelegate.webView(myWebView, performDragOperation: sender)
}
return false
}
I needed to retain WKWebView's handling of drags, but be able to introspect and modify pasteboards before it received them. Subclassing WKView is not really helpful since all of WKWebView's classes would still use the original, so I used the runtime swizzle proposed earlier + a class extension.
import WebKit
import Foundation
import ObjectiveC
extension WKView {
func shimmedPerformDragOperation(sender: NSDraggingInfo) -> Bool {
var pboard = sender.draggingPasteboard()
if let items = pboard.pasteboardItems {
for item in items {
if let types = item.types? {
for type in types {
if let value = item.stringForType(type.description) {
NSLog("DnD type(\(type)): \(value)")
}
}
}
}
}
return self.shimmedPerformDragOperation(sender) //return pre-swizzled method
}
}
var webview = WKWebView(frame: CGRectZero, configuration: WkWebViewConfiguration())
var wkview = (webview.subviews.first as WKView) // 1 per frame?
var origDnD = class_getInstanceMethod(WKView.self, "performDragOperation:")
var newDnD = class_getInstanceMethod(WKView.self, "shimmedPerformDragOperation:")
method_exchangeImplementations(origDnD, newDnD)
You do need to add a small snippet to your bridging header to extend WKView:
#import WebKit;
#interface WKView : NSView <NSTextInputClient> {
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)draggingInfo;
#end

How to refresh UICollectionViewCell in iOS 7?

I am trying to develop my app in Xcode 5 and debug it under iOS 7 environment.
I have a customized UICollectionViewLayoutAttributes.
I plan to do something after long pressing on UICollectionViewCell, so I override the method in UICollectionViewCell.m
- (void)applyLayoutAttributes:(MyUICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
if ([(MyUICollectionViewLayoutAttributes *)layoutAttributes isActived])
{
[self startShaking];
}
else
{
[self stopShaking];
}
}
In iOS 6 or below, - applyLayoutAttributes: is called after I call the statements below.
UICollectionViewLayout *layout = (UICollectionViewLayout *)self.collectionView.collectionViewLayout;
[layout invalidateLayout];
However, in iOS 7, - applyLayoutAttributes: is NOT being called even if I reload the CollectionView.
Is that a bug which is gonna be fixed by Apple later on, or I have to do something?
In iOS 7, you must override isEqual: in your UICollectionViewLayoutAttributes subclass to compare any custom properties that you have.
The default implementation of isEqual: does not compare your custom properties and thus always returns YES, which means that -applyLayoutAttributes: is never called.
Try this:
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([((MyUICollectionViewLayoutAttributes *) other) isActived] != [self isActived]) {
return NO;
}
return YES;
}
Yes. As Calman said you must override isEqual: method to compare custom properties that you have. See the apple documentation here
If you subclass and implement any custom layout attributes, you must also override the inherited isEqual: method to compare the values of your properties. In iOS 7 and later, the collection view does not apply layout attributes if those attributes have not changed. It determines whether the attributes have changed by comparing the old and new attribute objects using the isEqual: method. Because the default implementation of this method checks only the existing properties of this class, you must implement your own version of the method to compare any additional properties. If your custom properties are all equal, call super and return the resulting value at the end of your implementation.
In this case, the most efficient method would be
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if(![super isEqual:other]) {
return NO;
}
return ([((MyUICollectionViewLayoutAttributes *) other) isActived] == [self isActived]);
}

How to check if a view controller can perform a segue

This might be a very simple question but didn't yield any results when searching for it so here it is...
I am trying to work out a way to check if a certain view controller can perform a segue with identifier XYZ before calling the performSegueWithIdentifier: method.
Something along the lines of:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
Possible?
To check whether the segue existed or not, I simply surrounded the call with a try-and-catch block. Please see the code example below:
#try {
[self performSegueWithIdentifier:[dictionary valueForKey:#"segue"] sender:self];
}
#catch (NSException *exception) {
NSLog(#"Segue not found: %#", exception);
}
Hope this helps.
- (BOOL)canPerformSegueWithIdentifier:(NSString *)identifier
{
NSArray *segueTemplates = [self valueForKey:#"storyboardSegueTemplates"];
NSArray *filteredArray = [segueTemplates filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"identifier = %#", identifier]];
return filteredArray.count>0;
}
This post has been updated for Swift 4.
Here is a more correct Swift way to check if a segue exists:
extension UIViewController {
func canPerformSegue(withIdentifier id: String) -> Bool {
guard let segues = self.value(forKey: "storyboardSegueTemplates") as? [NSObject] else { return false }
return segues.first { $0.value(forKey: "identifier") as? String == id } != nil
}
/// Performs segue with passed identifier, if self can perform it.
func performSegueIfPossible(id: String?, sender: AnyObject? = nil) {
guard let id = id, canPerformSegue(withIdentifier: id) else { return }
self.performSegue(withIdentifier: id, sender: sender)
}
}
// 1
if canPerformSegue("test") {
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
}
// 2
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
As stated in the documentation:
Apps normally do not need to trigger segues directly.
Instead, you configure an object in Interface Builder associated with
the view controller, such as a control embedded in its view hierarchy,
to trigger the segue. However, you can call this method to trigger a
segue programmatically, perhaps in response to some action that cannot
be specified in the storyboard resource file. For example, you might
call it from a custom action handler used to process shake or
accelerometer events.
The view controller that receives this message must have been loaded
from a storyboard. If the view controller does not have an associated
storyboard, perhaps because you allocated and initialized it yourself,
this method throws an exception.
That being said, when you trigger the segue, normally it's because it's assumed that the UIViewController will be able to respond to it with a specific segue's identifier. I also agree with Dan F, you should try to avoid situations where an exception could be thrown. As the reason for you not to be able to do something like this:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
I am guessing that:
respondsToSelector: only checks if you are able to handle that message in runtime. In this case you can, because the class UIViewController is able to respond to performSegueWithIdentifier:sender:. To actually check if a method is able to handle a message with certain parameters, I guess it would be impossible, because in order to determine if it's possible it has to actually run it and when doing that the NSInvalidArgumentException will rise.
To actually create what you suggested, it would be helpful to receive a list of segue's id that the UIViewController is associated with. From the UIViewController documentation, I wasn't able to find anything that looks like that
As for now, I am guessing your best bet it's to keep going with the #try #catch #finally.
You can override the -(BOOL)shouldPerformSegueWithIdentifier:sender: method and do your logic there.
- (BOOL) shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"someSegue"]) {
if (!canIPerformSegue) {
return NO;
}
}
return YES;
}
Hope this helps.
Reference CanPerformSegue.swift
import UIKit
extension UIViewController{
func canPerformSegue(identifier: String) -> Bool {
guard let identifiers = value(forKey: "storyboardSegueTemplates") as? [NSObject] else {
return false
}
let canPerform = identifiers.contains { (object) -> Bool in
if let id = object.value(forKey: "_identifier") as? String {
return id == identifier
}else{
return false
}
}
return canPerform
}
}
Swift version of Evgeny Mikhaylov's answer, which worked for me:
I reuse a controller for two views. This helps me reuse code.
if(canPerformSegueWithIdentifier("segueFoo")) {
self.performSegueWithIdentifier("segueFoo", sender: nil)
}
else {
self.performSegueWithIdentifier("segueBar", sender: nil)
}
func canPerformSegueWithIdentifier(identifier: NSString) -> Bool {
let templates:NSArray = self.valueForKey("storyboardSegueTemplates") as! NSArray
let predicate:NSPredicate = NSPredicate(format: "identifier=%#", identifier)
let filteredtemplates = templates.filteredArrayUsingPredicate(predicate)
return (filteredtemplates.count>0)
}
It will be useful, before call performSegue, check native storyboard property on base UIViewController (for example screen was from StoryBoard or Manual Instance)
extension UIViewController {
func performSegueWithValidate(withIdentifier identifier: String, sender: Any?) {
if storyboard != nil {
performSegue(withIdentifier: identifier, sender: sender)
}
}
}
enter image description here
There is no way to check that using the standard functions, what you can do is subclass UIStoryboardSegue and store the information in the source view controller (which is passed to the constructor). In interface builder select "Custom" as the segue type as type the class name of your segue, then your constructor will be called for every segue instantiated and you can query the stored data if it exists.
You must also override the perform method to call [source presentModalViewController:destination animated:YES] or [source pushViewController:destination animated:YES] depending on what transition type you want.

Iterate over all subviews of a specific type

Iterating over all UIViews in the subviews of a class and then checking the type in the loop using isKindOfClass made my code look redundant. So I wrote the following method which executes a block for each subview.
#implementation Util
+ (void)iterateOverSubviewsOfType:(Class)viewType
view:(UIView*)view
blockToExecute:(void (^)(id subview))block
{
for (UIView* subview in view.subviews) {
if ([subview isKindOfClass:viewType]) {
block(subview);
}
}
}
#end
The block passed to this method takes an argument of type id. The type used here should be of course the same as passed as the first argument. However so far I haven't figured out a way to make this more type safe.
Try it like this, should be safe enough.
for (id subview in view.subviews) {
if ([subview isMemberOfClass:viewType]) {
block(subview);
}
}