iOS 14 Tweak Development: Beginner Tutorial

iOS 14 Tweak Development: Beginner Tutorial

Tweak development can be hard, especially when you realize that all tutorials on it are from 2015. That's why I decided to make an updated tutorial.

I assume you know Swift or understand some programming concepts. If so, great!

If you feel stuck or get some errors that are not described in the tutorial, verify you've done everything correctly. If nothing works, try asking for help on Theos Discord server or reach me out on my own

Credits: Theos, Orion Docs

Introduction

Choose your Pokemon

Operating system

When it comes to tweak development, you can use multiple OSes: Windows, Linux, MacOS and iOS. And of course, we, with expensive iPhones don't have a Mac, and you most likely have either Windows or Linux. If you have a Mac, then you're in luck, because it's the platform where the least amount of problems arise.

If you're on Windows, I suggest setting up WSL or dual-booting Linux.

You can also create tweaks using iOS, but you won't be able to code a huge project using only your fingers :)

Language

You need to choose between Obj-C and Swift. Both work great, but do keep in mind Obj-C is harder for beginners. The key advantage of Obj-C is that it has lots of open-source tweaks written in it, while Swift only has a few. If you choose Swift, you'll have to translate a lot of stuff from Obj-C to Swift. But Swift is much easier to read and learn, so if you've programmed only a little/never, I would suggest using it. Also, if you have a MacOS machine, you have a bonus of being able to use Xcode - an IDE with auto-completion and other neat features. We'll use Swift in this tutorial.

IDE

If you use Mac, use Xcode. If you are using Linux / Windows, use VSCode or any other IDE you prefer. I will use VSCode throughout this tutorial.

Installation

Commands, commands, commands

First we need to install Theos - a tool for writing tweaks. Follow the instructions here.

If you are on Linux / WSL, make sure you have Swift installed.

Since we are using Swift, we also need a tool called "Orion". On your jailbroken device add the Chariz repo (https://repo.chariz.com/) and install the Orion Runtime package (pick iOS 12-13 or iOS 14 depending on your version).

Then, on your computer, inside your Theos installation directory (to change the current directory run cd $THEOS) and switch to the orion branch by running git fetch && git checkout orion && git submodule update --init.

Sometimes, builds will fail due to an old toolchain. Make sure to install it:

rm -r $THEOS/toolchain/*
curl -#L https://github.com/kabiroberai/swift-toolchain-linux/releases/download/v2.1.0/swift-5.6.1-ubuntu20.04.tar.xz | tar -xvJ -C $THEOS/toolchain
Make sure to check for the latest toolchain provided and replace the link appropriately.

You should have all tools installed to finally create a new project!

New project

I thought I would just need to click "Install"

Now that you have all the required tools installed, you can finally create a new project. To do so, run the nic.pl script located at $THEOS/bin/nic.pl( just run $THEOS/bin/nic.pl inside the directory you want the project to be created in, use cd to switch directories).

πŸ’‘
You shouldn't create projects inside the $THEOS directory. Create project ins a permanent directory, such as ~/Developer.

You should see a message like this:

Select iphone/tweak_swift, I had to enter 18 in my case.

Next, name the project StatusTimeFormatter, we'll create a tweak that changes the time in Status Bar to a date, like 8-23-22 5:31 PM. For package name use this template com.yourname.statustimeformatter (of course replace yourname with Β  your Β  name). I have net.sourceloc.statustimeformatter, because I own a domain sourceloc.net.

For author you can press enter to use a default value or enter your own. Leave the default value of MobileSubstrate Bundle filter (press enter) and same for apps to terminate. You should end up this something similar:

And the project should be generated in the directory you were just in.

Let's take a look. Open VSCode / Xcode and open the project folder.

Sources contain your source files for the tweak. But wait, why are there two folders: StatusTimeFormatter and StatusTimeFormatterC? I thought I'll be writing in Swift?

That is because some stuff requires importing Obj-C headers. Basically, headers define names of functions, but they don't tell what exactly the function does.

BUT, we can also write "headers" in Swift, though it's harder that Ctrl+C Ctrl+V from some open-source project on GitHub :) So you generally want to write headers in Tweak.h, but more on that later.

StatusTimeFormatter contains Swift code. This is the place you will spend 90% of your time while working on a tweak.


.gitignore you can read more about it here.

.theos is the folder required for Theos, you don't need to touch it.

control file describes what name should the package use and what version it's on.

Makefile tells Theos how to compile your tweak, how to install, which SDK to use and more.

Package.swift is a file that tells Swift of what targets you project consists. Otherwise Swift wouldn't be able to tell which files need to be compiled. We'll modify this file in the future to support Preferences.

StatusTimeFormatter.plist determines in which apps should the tweak inject to. In this tutorial we'll only target SpringBoard, so you don't need to change it.

sourcekit-lsp (optional)

Scary words!

β€ŒTheos has a trick up its sleeve: with some basic configuration, you can get full code completion while working on Orion tweaks on any OS, in most editors! Enabling this is simple:

  1. Install the sourcekit-lsp plugin corresponding to your editor of choice. (If you’re using Xcode, it has built-in SourceKit support so skip this step.)
  2. Run make spm inside your project to generate metadata about your project, which is needed by Swift Package Manager/SourceKit.
  3. Open the project folder in your chosen editor.

Just like that, you should have code completion!

Compiling

Code is cool and all, but how do I turn it into a tweak?

Great question. Remember you installing Theos and Orion? These are the tools responsible for compiling the tweak. To start compilation run make do inside the project directory (If you use VSCode, you can use it's terminal to do so). But oh no! Errors!

make do tries to install the tweak to device, but would it know on which device it needs to install it onto? We need to tell Theos the IP and port of your device. Get the IP of your iPhone:

  1. Go to Settings > Wi-Fi.
  2. Tap the information icon to the right of the Wi-Fi.
  3. Set the THEOS_DEVICE_IP environment variable on your computer in your "shell configuration file" (like .bashrc, .zshrc). You can do so by adding the following line to the file at the bottom: export THEOS_DEVICE_IP=enter_ip_here. Replace enter_ip_here with phone's ip. Restart the terminal to apply changes
πŸ’‘
(Not recommended) You can also set the variable by adding a new line at top in Makefile: THEOS_DEVICE_IP=enter_ip_here.
πŸ’‘
Make sure you have OpenSSH installed on your mobile device

Try running make do again and after a few seconds it will ask for password. Enter alpine (Unless you changed it on your device). It might ask it two times.

πŸ’‘
To not enter it every time you run make do, you can save the ssh password with ssh-keygen and ssh-copy-id root@IPHONE_IP_HERE

And here you go, your first "An awesome MobileSubstrate tweak"! Well, not quite, it says "An awesome Orion tweak", but I assume you understood the joke.

Now it's FINALLY time for some research and coding

Research

Sherlock Holmes Simulator 2022

Before you want to modify something, you first need to know how and what exactly. In our case we want to modify the status bar time text to something else. But how? We can try to check every single text in the whole iOS if it matches the current time and if it does, then only apply the needed changes. But that's just awful. What if we could somehow get a class name of a specific object and hook it? What if ... :) Introducing, FLEXall. Install it on your device from this repo https://dgh0st.github.io/.

This tweak allows you to view how elements (UIViews) are placed on SpringBoard and any other app. Try long tapping the status bar and you will see a menu appear like this:

Tap on select and tap on something, you will notice a border around the item you selected appear.

Pretty cool, right?

We want to change the time label, let's click it

Notice the _UIStatusBarStringView, it's the class name we want to hook. But before jumping to coding, we also need to know which methods and properties it has. Tap on views. This will bring the hierarchy of UIViews rendered on the screen. The time label you selected before will be highlighted in the list. Click the info button.

You will see all properties this specific class has, as well as properties of it's subclasses. Great. Now let's find something useful we will use in our code.

(The following image has an error, these are called "Superclasses", not "Subclasses")

Every minute iOS will try to update the time add one minute to text. It should probably call some method on the label to "update the text" to something else. Let's try looking for a variable that stores the text. Oh wait, it's right there - text, with current value 22:25

While we don't use Obj-C, we still need to understand some of it's concepts, one of which is setter. If a class has a non-read-only @property, it also has a method named setNAME. We want to replace the method setText. We'll do all of that in the next chapter!

Hooking

Just like fishing, but not at all

Let's write our first class hook. Inside of Tweak.x.swift add the following code below the already existing imports:

class StatusBarTimeHook: ClassHook<_UIStatusBarStringView> {

}

Here, we create a new hook on _UIStatusBarStringView, allowing us to override some of it's methods. The name StatusBarTimeHook can be whatever you please, but naming them with an ending Hook is the best practice.

Let's try replacing the previously mentioned setText method by creating a function inside the hook:

func setText(_ text: String) {
    orig.setText("Hello world!")
}
πŸ’‘
Notice the _ text. Why is it written like so? Because when overriding an Obj-C method you still need to match it's signature. The signature for this method is - (void)setText:(id) - you can check the signature for any method in FLEXall.
πŸ’‘
I have a new tutorial in progress on how to translate Obj-C signatures to Swift, so I recommend joining my Discord server not to miss it! :)

Another mysterious thing in this code is orig.setText("Hello world!"). It calls the original method of the instance, but with different arguments. So to call the original method with the same arguments you can use orig.setText(text), where text is the passed parameter from the hooked setText.

Try building and see the result! Haha, I got you there, didn't I :)? It fails:

How would Swift know what is _UIStatusBarStringView? We need to add an @interface inside Tweak.h:

#import <UIKit/UIKit.h>

@interface _UIStatusBarStringView : UILabel
@end

Here, we also import UIKit and tell, a _UIStatusBarStringView class and that it is a subclass of UILabel.

Now run make do, it should install it on the device and this time it will work:

... sort of. A label we want to change successfully changes, but so do the other status bar labels. Why so? Let's see why.

The only reason why our code would change the labels is if they are a subclass of _UIStatusBarStringView and if their method "setText" was called. Use FLEXall to check if our theory is true: active the menu, click on the carrier text, and you will see something like this:

Underlined the class name with white

Turns out, that's the case! Now the question is: how do we separate those labels? The recommended way is to check if the superview is some specific class, but we are here for educational purposes, so why not just check if there's a colon (:). Stupid, but it will work.

func setText(_ text: String) {
    if text.contains(":") {
        orig.setText("Hello world!")
    } else {
        orig.setText(text)
    }
}

Cool! Now all we have to do is replace the Hello world with the actual data. Using https://nsdateformatter.com/ we can generate any formatter we want. And to use it in Swift we can use the code I found after one google search:

func setText(_ text: String) {
    if text.contains(":") {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm"
        orig.setText(formatter.string(from: Date()))
    } else {
        orig.setText(text)
    }
}

And here's the final result:

The final thing is changing the Control file to prepare the tweak for distribution. Open it up and edit the fields you want. Now run make do FINALPACKAGE=1 and you should now have the .deb inside the packages/ folder.

πŸ’‘
Keep in mind, Orion does not yet support arm64e building on Linux / WSL, so the tweak will crash if run on a device with >=A12 processor.
If you want to support arm64e, you have to use MacOS.
πŸ’‘
Great job! You finished!

Bonus challenge

Change the color of text to yellow.

Using FLEXall we can view methods of _UIStatusBarStringView's superclass - UILabel. Tap on UILabel in the top bar. (Because the subclasses have all methods from their superclass, we might be able to find something useful and use it in our code). Let's try searching for color, and you will find a method called setTextColor with signature - (void)setTextColor:(id). Override this method and we get this result:

import Orion
import StatusTimeFormatterC

class StatusBarTimeHook: ClassHook<_UIStatusBarStringView> {
    // orion:new
    func isDateLabel(_ text: String) -> Bool {
        return text.contains(":")
    }

    func setText(_ text: String) {
        if isDateLabel(text) {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd HH:mm"
            orig.setText(formatter.string(from: Date()))
        } else {
            orig.setText(text)
        }
    }

    func setTextColor(_ color: UIColor) {
        if isDateLabel(target.text ?? "") {
            orig.setTextColor(.yellow)
        } else {
            orig.setTextColor(color)
        }
    }
}

// orion:new is not just a comment, it tells Orion, that this method is user defined and should not be used as an override of an existing method.

πŸ’‘
Bravo, you've just completed the bonus challenge. Awesome!

Conclusion

πŸŽ‰ πŸŽ‰ πŸŽ‰

Congrats, you made it to the end! We learned how to setup the project, analyze the structure of iOS views, write hooks, troubleshoot, understand some concepts of Obj-C and got a final result - a tweak ready for distribution.

Go ahead, post the source code on GitHub so everybody now knows that you are now a tweak developer :)

You can check the source code for this project here.

Credits: thissupermegaperson on discord (typo)