This gist contains a list of mistakes I've made or seen people make in the past. These are my opinions, so don't treat them as facts. If you disagree with anything you see here, please let me know.
Adding subviews from -[UIView layoutSubviews]
-[UIView layoutSubview]
is a method that is responsible for adjusting the positions of subviews after something caused the existing layout to be invalidated. There can be various reasons for this. For example, the bounds of the view may have changed or some code may have called -[UIView setNeedsLayout]
. The point is, regardless of why this method got called, this method gets called a lot. Its only purpose is adjusting the layout of the view to match the new conditions. It is not for adding new subviews to a view.
%new
, %property
and other similar Logos directives are used for adding new methods to an existing class. This can be used for many things such as adding new delegate methods to a class or extending the functionality of a class with helper methods. What most developers don't realize is their tweak isn't the only tweak installed on the device. Consider the situation below.
Imagine a fitness-related app. This app has a homepage but it lacks some content so two independent developers made "ShowMyProgress" and "Today's Quote" to add more content to the homepage. They didn't know each other while making these tweaks.
/*-- "ShowMyProgress" Tweak.xm --*/
%hook APPHomeViewController
- (void)viewDidLoad {
%orig;
[self addLabel];
}
// Adds the progress label
%new
- (void)addLabel {
UILabel *progressLabel = [[UILabel alloc] initWithFrame:CGRectMake(30, 30, self.view.bounds.width - 30, 50)];
progressLabel.text = [[[APPUser sharedUser] progress] description];
[self.view addSubview:progresLabel];
}
%end
/*-- "Today's Quote" Tweak.xm --*/
static void FetchQuote(void(^callback)(NSString *)) {
/* ... */
}
%hook APPHomeViewController
- (void)viewDidLoad {
%orig;
[self addLabel];
}
// Adds the quote label
%new
- (void)addLabel {
UILabel *quoteLabel = [UILabel new];
quoteLabel.translatesAutoresizingMaskIntoConstraints = NO;
quoteLabel.numberOfLines = 0;
[self.view addConstraints:@[
[quoteLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:15.0],
[quoteLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:15.0],
[quoteLabel.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:15.0],
[quoteLabel.heightAnchor constraintEqualToConstant:30.0]
]];
quoteLabel.text = "Loading quote...";
[self.view addSubview:quoteLabel];
FetchQuote(^(NSString *quote) {
quoteLabel.text = quote;
});
}
%end
Now let's think of a user who wanted both a progress label and a quote in the homepage. You might think that since these tweaks add new labels that are very close to each other, the user would be unhappy about the overlapping text. While the user will be disappointed, this is not the reason.
The important detail here is that both of these tweaks create their own -[APPHomeViewController addLabel]
method. Since there can't be two methods with the same name in a class, only one of them will be available. Which one? Nobody knows. When these two tweaks are used together, the user will see two quote labels or two progress label instead of one quote label and one progress label since both of the tweaks call -addLabel
from -viewDidLoad
.
These conflicts can be resolved by
- using C-style functions instead of
%new
methods. - prefixing method names with your own tweak's name, such as
todaysQuote_addLabel
. - using more descriptive method names like
addQuoteLabel
instead ofaddLabel
.
When using %new
methods, it is a good idea to do both (2) and (3) at the same time.
The setuid bit has no effect on dynamic libraries.
UIImageView *batteryIconView;
%hook _UIBatteryView
- (id)initWithFrame:(CGRect)frame {
id view = %orig;
if (view) {
batteryIconView = [UIImageView new];
/* ... initialization code ... */
[view addSubview:batteryIconView];
/* ... layout code ... */
}
return view;
}
%end
Even if only one instance of a class is ever created by the application, it is still not a good idea to store instance data in global variables.
- Instance variables and properties exist for this purpose. Make use of them.
- The app may only create one instance of this class now, but maybe it will create multiple instances of it in the next version. It is never a bad idea to write future-proof code.