theos: writing a tweak for iOS SpringBoard

  • Tutorial
Good New Year's Eve to dear habra-people!

Today I will talk about creating a tweak for iOS SpringBoard using theos . What for? As an interesting drawing and training. At the end of the tutorial, we get something like this right on the lock screen of our i-device:



Creating a project and setting up theos


We begin: we create an empty folder, we throw theos into it (I threw it in the form of a gig submodule).

Next, create a new project using the NIC:

iHabrTweak git:(master) theos/bin/nic.pl 
NIC 2.0 - New Instance Creator
------------------------------
  [1.] iphone/application
  [2.] iphone/library
  [3.] iphone/preference_bundle
  [4.] iphone/tool
  [5.] iphone/tweak
Choose a Template (required): 5
Project Name (required): iHabrTweak
Package Name [com.yourcompany.ihabrtweak]: com.silvansky.ihabr
Author/Maintainer Name [Valentine Silvansky]: silvansky
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: 
Instantiating iphone/tweak in ihabrtweak/...
Done.

Now we have the ihabrtweak folder, which contains the files we need.

iHabrTweak git:(master) ✗ cd ihabrtweak 
ihabrtweak git:(master) ✗ ls
Makefile Tweak.xm control iHabrTweak.plist theos

Now run make and see errors: it's not so simple! Our system is not completely ready for testing on theos.

Well, you need to enter the settings necessary for normal assembly:

export ARCHS=armv7
export TARGET=iphone:latest:4.3
export THEOS="`pwd`/theos"
export SDKVERSION=6.0
export THEOS_DEVICE_IP=192.168.2.2

ARCHSindicates to us that we will collect only for armv7, and on armv6 we will score. TARGETtells us that we will collect for iOS using the latest (in the system) SDK and with compatibility with version 4.3. The remaining three are self-evident.

ihabrtweak git:(master) ✗ make
Making all for tweak iHabrTweak...
 Preprocessing Tweak.xm...
 Compiling Tweak.xm...
 Linking tweak iHabrTweak...
 Stripping iHabrTweak...
 Signing iHabrTweak...
ihabrtweak git:(master) ✗ ls .theos/obj 
Tweak.xm.o iHabrTweak.dylib

Now we have our wonderful dynamic library, which so far does not know how to do anything! But we can install our tweak on the device:

ihabrtweak git:(master) ✗ make package
Making all for tweak iHabrTweak...
make[2]: Nothing to be done for `internal-library-compile'.
Making stage for tweak iHabrTweak...
dpkg-deb: building package `com.silvansky.ihabr' in `./com.silvansky.ihabr_0.0.1-1_iphoneos-arm.deb'.
ihabrtweak git:(master) ✗ make package install
Making all for tweak iHabrTweak...
make[2]: Nothing to be done for `internal-library-compile'.
Making stage for tweak iHabrTweak...
dpkg-deb: building package `com.silvansky.ihabr' in `./com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb'.
install.copyFile "./com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb" "com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb"
root@192.168.2.2's password: 
com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb 100% 1454 1.4KB/s 00:00    
install.exec "dpkg -i com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb"
root@192.168.2.2's password: 
Selecting previously deselected package com.silvansky.ihabr.
(Reading database ... 2516 files and directories currently installed.)
Unpacking com.silvansky.ihabr (from com.silvansky.ihabr_0.0.1-2_iphoneos-arm.deb) ...
Setting up com.silvansky.ihabr (0.0.1-2) ...
install.exec "timeout 10s sbreload || ( ( respring || killall -9 SpringBoard ) && launchctl load /System/Library/LaunchDaemons/com.apple.SpringBoard.plist )"
root@192.168.2.2's password: 
launchctl unload SpringBoard.plist
waiting for kill(29) != 0...

Actually, the tweak is ready! It is set, but does nothing. We will rule it. Let's start with the theory of theos and his tweaks.
As you already noticed, in the project we have the Tweak.xm file, which is our main source.

At the moment, everything is commented out in it, and the comment itself is a partial documentation. Actually, this file is a template for generating the final .mm file. Consider some useful macros of this template:

% hook and% end


Theos tweaks are based on hooks. They are based on the richest runtime of the Objective-C language, which allows the substitution of methods in an arbitrary class. Actually, it is used like this:

%hook SomeClass
-(void)someMethod
{
     // some code goes here
}
%end

Here We introduce (replace) the “someMethod” method of the “SomeClass” class. For example, we can embed our code in SpringBoard, for example, we can add our views to the lock screen.

% orig and% new


Well, we redefined the method, but how to call the original one? Yes, also very simple! There is a% orig macro for this. Being called without parameters, this macro redirects to the original function the same parameters as it came to our hook. But you can pass on any of your own:

%hook SomeClass
- (id)initWithFrame:(CGRect)frame
{
    id result = %orig;
    // some custom code
    return result;
}
- (id)initWithName:(NSString *)name
{
    id result = %orig(@"customName");
    // some custom code
    return result;
}
%end

If the simple method definitions inside the hooks override the existing ones, then you can use the% new macro to add new methods. In fact, it is a separator between the methods that we substitute and the methods that we add. ALL methods coming after% new will be added exactly. Example:

%hook SomeClass
- (void)someOldMethod
{
    // some code here
}
%new
- (void)someNewMethod
{
    // some more code here
}
%end

But with this approach, we will not be able to call our new method from the overridden one: theos treats the vorings as errors and will not allow to assemble the project. After all, we have not announced our method! But this is fixable, just add this to our file:

@interface SomeClass(NewMethods)
- (void)someNewMethod;
@end;

% log


The% log macro allows you to write the fact of a function call to the system log. Commonly used for debugging.

Other macros can be viewed here .

Writing something useful


In the set for theos, we get the headers of the system frameworks. In our project they lie in theos/include. If they don’t lie, do not forget to do so:

cd theos
git submodule init
git submodule update

There we find the SpringBoard folder, and in it - a bunch of headers. Well, let's go through the class names. Let's take an interesting SBAwayView class, which is just the main view of the lock screen. Well, we will put hooks in it. First you need to catch the moment of its creation:

#import 
#import 
%hook SBAwayView
-(id)initWithFrame:(CGRect)frame
{
	id result = %orig;
	if (result)
	{
		// here goes the code...
	}
	return result;
}
%end

We can put% log and make sure after assembly-installation that this method is actually called. Now we can add new views! Just where? Let's add them to the background image. We find ivar UIImageView *_backgroundViewin the class SBSlidingAlertDisplayfrom which it is inherited SBAwayView, in the same place we find the method -(CGRect)middleFrame;. But how do we get the ivar value? Google it. Let's find the MSHookIvar function, which will do everything:

#import 
#import 
#import 
%hook SBAwayView
-(id)initWithFrame:(CGRect)frame
{
	id result = %orig;
	if (result)
	{
		CGRect labelRect = [self middleFrame];
		labelRect.origin.y = labelRect.origin.y + 20.f;
		labelRect.size.height = 50.f;
		UILabel *habrLabel = [[[UILabel alloc] initWithFrame:labelRect] autorelease];
		habrLabel.text = @"Hello, Habr!";
		habrLabel.textColor = [UIColor colorWithRed:155.f/255.f green:182.f/255.f blue:206.f/255.f alpha:1.f];
		habrLabel.opaque = NO; 
		habrLabel.textAlignment = UITextAlignmentCenter;
		habrLabel.font = [UIFont boldSystemFontOfSize:36];
		habrLabel.backgroundColor = [UIColor clearColor];
		UIImageView *backgroundView = MSHookIvar(self, "_backgroundView");
		[backgroundView addSubview:habrLabel];
	}
	return result;
}
%end

We launch and enjoy the sight!

Now let's complicate the task. We will upload a picture! In theory, everything is simple: instead of UILabel, create a UIImageView. And where does the picture come from?

The picture should be put in the SpringBoard.app bundle, and it is better if the picture is copied there during the installation of the package. To do this, we will reorganize the project structure: create the Layout folder, in it the DEBIAN folder, where we will move the existing control file, next to the DEBIAN folder we will make System / Library / CoreServices / SpringBoard.app, where we will place our picture:

SpringBoard.app git:(master) pwd
/Users/silvansky/Projects/iHabrTweak/ihabrtweak/Layout/System/Library/CoreServices/SpringBoard.app
SpringBoard.app git:(master) ls
habr_logo_hat.png


Now you can write the final New Year code:

#import 
#import 
#import 
#define IMG_WIDTH   150.f
#define IMG_HEIGHT  186.f
%hook SBAwayView
-(id)initWithFrame:(CGRect)frame
{
	id result = %orig;
	if (result)
	{
		CGRect imageRect = [self middleFrame];
		imageRect.origin.y = imageRect.origin.y + 20.f;
		imageRect.origin.x = (imageRect.size.width - IMG_WIDTH) / 2.f;
		imageRect.size.width = IMG_WIDTH;
		imageRect.size.height = IMG_HEIGHT;
		UIImageView *habrLogoView = [[[UIImageView alloc] initWithFrame:imageRect] autorelease];
		habrLogoView.image = [UIImage imageNamed:@"habr_logo_hat"];
		UIImageView *backgroundView = MSHookIvar(self, "_backgroundView");
		[backgroundView addSubview:habrLogoView];
	}
	return result;
}
%end

And - we admire the resulting beauty:


Complete source, as usual, please take on a github .

All with the coming! Joy and good luck next year! =)

Also popular now: