Skip to content

Instantly share code, notes, and snippets.

@pixelomer
Last active September 4, 2021 16:18
Show Gist options
  • Save pixelomer/e11d3ac74714e96de9486f2d9910cf3b to your computer and use it in GitHub Desktop.
Save pixelomer/e11d3ac74714e96de9486f2d9910cf3b to your computer and use it in GitHub Desktop.
Common mistakes in tweak development

Common mistakes in tweak development

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.

Using Logos directives improperly

%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

  1. using C-style functions instead of %new methods.
  2. prefixing method names with your own tweak's name, such as todaysQuote_addLabel.
  3. using more descriptive method names like addQuoteLabel instead of addLabel.

When using %new methods, it is a good idea to do both (2) and (3) at the same time.

Setting the setuid bit for a tweak .dylib

The setuid bit has no effect on dynamic libraries.

Using global variables to store instance data

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment