| name | ios-logos-tweak |
|---|---|
| description | Best practices for iOS jailbreak tweaks using Theos and Logos (%hook, %hookf, %new, %orig, %init). Prefer Logos and Substrate helpers (MSHookIvar) over manual hook/ivar runtime APIs. Covers API availability (respondsToSelector vs @available) and bracket messaging. Use when writing or reviewing .x/.xm tweak code, MobileSubstrate/libhooker hooks, swizzling, or Theos Tweak projects. |
iOS Logos Tweak Best Practices
Core rule
Put hooking code in Logos files (.x, .xm, .xi) and express hooks with Logos directives. Let the Logos preprocessor emit MSHookMessageEx, MSHookFunction, class_addMethod, constructors, and class registration.
Do not hand-write Substrate hook boilerplate in tweak source unless Logos cannot express the case (rare; see reference.md).
File layout
Default: .x. Most tweaks use a single Tweak.x (Logos + Objective-C). Theos runs Logos on the file, then compiles the result as ObjC.
| Extension | Compiles as | When to use |
|---|---|---|
.x | Objective-C | Default. %hook, %hookf, %new, %orig, %property, blocks, and normal Foundation/UIKit code. |
.xm | Objective-C++ | Only when this translation unit needs C++ or Substrate’s C++ APIs (see below). |
.xi / .xmi | Included fragments | Rare: Logos inside #define macros or #include chains; prefer splitting into separate .x/.xm files with extern instead (Theos docs). |
Use .xm when this file contains
MSHookIvar<T>orMSIvarHook(implemented as C++ templates in<substrate.h>).- C++ only valid in ObjC++:
std::,namespace, templates, lambdas, C++ types in hook bodies. %origforms that embed block literals in the hook implementation when your toolchain/setup requires ObjC++ for those expansions (uncommon; most hooks stay in.x).
Stay on .x when
- Hooks are plain Objective-C (even with blocks,
@autoreleasepool, etc.). - You do not call
MSHookIvarin that file.
If only one hook file needs ivar access, keep Tweak.x and add a small Something.xm (or move just those hooks to .xm) rather than renaming the whole project to .xm without reason.
Keep interfaces and constants in normal .h / .m when they are not hooks. Keep all hooks in Logos (.x / .xm, not raw .m with MSHook*).
Do not #include <substrate.h> in Logos files; Theos/Logos adds it in generated code.
Objective-C methods: %hook
%hook YTPlayerViewController
- (void)layoutSubviews {
%orig;
// tweak logic
}
%end
- Match the exact selector signature (class vs instance
+/-, types, argument names). - Call the original with
%origor%orig(arg1, arg2); never passselfor_cmd. - If you use
%orig(...), pass every user argument (Logos fillsself/_cmd). - Always close with
%end.
Avoid: MSHookMessageEx, method_setImplementation, method_exchangeImplementations, or IMP trampolines for methods Logos can hook.
Objective-C messaging
Send messages with [receiver message] / [receiver foo:bar] syntax. Do not use objc_msgSend, objc_msgSendSuper, or similar runtime send helpers for calls you can express as normal Objective-C.
If a symbol is undeclared in the public SDK, add a private header or class/category @interface with the method, then call it with brackets. That keeps type checking and ARC rules; objc_msgSend needs manual casts, is easy to get wrong on arm64, and reads worse than idiomatic ObjC.
Avoid: ((void (*)(id, SEL, ...))objc_msgSend)(obj, @selector(foo), arg) when [obj foo:arg] (after a proper declaration) works.
C functions: %hookf
%hookf(BOOL, MGGetBoolAnswer, CFStringRef string) {
if (CFEqual(string, CFSTR("SomeKey"))) return YES;
return %orig;
}
- First argument is return type; second is the symbol name; rest are parameters (no implicit
self/_cmd). - For private symbols, resolve at init time with
MSFindSymbol(MSGetImageByName(...), "_symbol"). Pass the image that owns the symbol as the first argument toMSFindSymbol; do not passNULL.
%ctor {
void *gestalt = MSGetImageByName("/usr/lib/libMobileGestalt.dylib");
%init(MGGetBoolAnswer = MSFindSymbol(gestalt, "_MGGetBoolAnswer"));
}
MSFindSymbol(NULL, name)searches every loaded image. That pattern is discouraged: some runtime modification / injection setups can crash or behave badly during global symbol lookup. AlwaysMSGetImageByName("/path/to/Image.dylib")(framework binary path as Substrate expects) and pass thatMSImageReftoMSFindSymbol.- If lookup fails, guard before
%init(skip the group or log) rather than passing a null pointer into%init.
Avoid: MSHookFunction + manual replacement when %hookf + %init suffices; MSFindSymbol(NULL, ...) for dynamic %hookf targets.
New methods and storage: %new and %property
Add methods on an existing class inside %hook:
%hook MyTargetClass
%new(v@:@)
- (void)myTweak_didTap:(id)sender {
// no %orig — method did not exist before
}
%property (nonatomic, strong) UIView *myTweak_overlay;
%end
- Use
%newfor any method that does not exist on the class (including helpers only your tweak calls). - Always write
%new(typeencoding)with the exact Objective-C method type encoding (e.g.%new(v@:)for- (void)foo,%new(v@:@)for- (void)foo:(id)x,%new(@@:)for- (id)bar). Bare%newwithout parentheses makes Logos generate an encoding from the declaration; that extra work is unnecessary when you already know the signature. - Match the encoding to the method you declare; when unsure, compile a stub and read
@encode/method_getTypeEncoding, or copy from a class dump for the same selector shape. - Use
%propertyfor associated state on hooked classes instead ofobjc_setAssociatedObjectin every hook (unless you need fine-grained control).
Avoid: class_addMethod + static C functions for tweak-only ObjC methods on hooked classes.
Instance variables: MSHookIvar
In tweak dylib code linked against Cydia Substrate (Logos .x/.xm, or .m/.xm helpers in the tweak target), read and write the target app’s existing ivars with MSHookIvar from <substrate.h> (included automatically in Logos output). Do not reimplement access with class_getInstanceVariable, ivar_getOffset, manual ((char *)self + offset), object_getIvar, or object_setIvar unless Substrate is unavailable.
%hook SomeViewController
- (void)viewDidLoad {
%orig;
UIView *content = MSHookIvar<UIView *>(self, "_contentView");
content.hidden = NO;
}
%end
Inside a hook body, the MSIvarHook(type, name) macro (Substrate) is equivalent to a typed reference for an ivar whose name matches the stringized token (e.g. MSIvarHook(UIView *, _contentView)).
- Use
.xmfor this file if it usesMSHookIvar/MSIvarHook; otherwise keep ivar-free hooks in.x. - For state your tweak adds, prefer
%propertyon the hooked class, not reading/writing foreign ivars you invent. - In targets without Substrate (preference bundles, standalone tools, libraries not injected into processes), use the Objective-C runtime ivar APIs or public accessors; do not assume
MSHookIvaris linked.
Avoid: offset hunting duplicated in every call site when the tweak already links Substrate.
When hooks run: %group, %ctor, %init
- Ungrouped
%hook/%hookf→ default group; Logos emits a constructor that calls%initautomatically. - Use
%groupwhen hooks must be conditional or delayed:
%group iOS17
%hook SomeNewClass
- (void)foo { %orig; }
%end
%end
%ctor {
if (@available(iOS 17, *)) %init(iOS17);
}
- Call
%initor%init(GroupName)from%ctor(or another function) for grouped hooks; do not rely on auto-init for grouped code. - Use
%init(ClassName = objc_getClass("Swift.Module.Class"))for Swift/mangled or awkward class names.
API availability: version checks vs respondsToSelector
Pick one primary strategy per code path. Mixing @available, %group, and respondsToSelector on the same feature adds noise without extra safety.
When the deployment floor already includes the API
If the tweak only runs on iOS versions where the selector always exists (declared minimum in the project, or hooks live in a %group initialized only on that OS), call the API directly. Do not wrap every use in respondsToSelector: / instancesRespondToSelector: / +[Class respondsToSelector:].
Version gating at init (%group + %init, @available in %ctor) is enough; inside those hooks, treat the method as present.
When runtime checks make sense
Use respondsToSelector: (and class-side equivalents) when:
- The tweak supports a wide OS range with few explicit version checks, and
- A selector appears only on some releases (Apple added a method mid-cycle, private API drift, optional app-internal helpers), and
- You are calling across classes you do not hook (not choosing whether to install a Logos hook).
Example: one optional call on a shared code path without adding @available(iOS X, *) at every call site.
if ([obj respondsToSelector:@selector(newStyleLayout)]) {
[obj newStyleLayout];
} else {
[obj legacyLayout];
}
Prefer class checks (+[SomeClass instancesRespondToSelector:]) when the method is implemented on the class, not overridden per instance.
What to avoid
respondsToSelector:before every message send when%init(iOS17)or the package minimum already guarantees the selector.respondsToSelector:and@availablefor the same branch unless they guard different risks (e.g. SDK compile vs runtime on an old binary).- Using runtime checks instead of
%groupfor entire hook sets that only apply on one OS; use%group+%initso hooks are not installed on unsupported versions.
Other Logos tools (prefer over manual runtime)
| Need | Logos / Substrate |
|---|---|
| Runtime class reference | %c(ClassName) or %c(+ClassName) for metaclass |
| Log hook calls | %log in a method body |
| Subclass at runtime | %subclass + %property + %c() to allocate |
| Target class private ivar | MSHookIvar / MSIvarHook in tweak code (not raw runtime offset math) |
| Generator backend | %config(generator=MobileSubstrate) (default), libhooker, or internal |
Note: %hookf is not supported with generator=internal. Pick the generator to match the target jailbreak environment.
Code organization
- One
%hook ClassNameblock per class; multiple methods inside one block. - Split large tweaks with
%groupin separate Logos files when using hook splitting (%initfrom a shared constructor); use.xunless that file needs.xmfor C++/MSHookIvar. - Declare classes you hook in headers (
@interface) or private headers; Logos does not replace the need for correct types at compile time. - Use
logos_interface/ empty@interfacein.xwhen you need typed calls to%newAPIs from plain.mfiles.
Common mistakes
- Hooking in
.mwith Substrate while the rest of the project uses Logos — move hooks intoTweak.x(or.xmonly if that file needs ObjC++). - Missing
%endor nesting%hookinside a method (invalid; Logos errors). - Calling
%origin a%newmethod — unsupported; call super or another selector explicitly if needed. - Forgetting
%initfor%group— hooks never run. - Duplicate hooks for the same class/method in one group — undefined behavior; merge blocks.
- Using
%hookfinside%hook—%hookfis top-level (except inside%group). objc_msgSendinstead of[obj msg]— declare the API and use bracket syntax.ivar_getOffset/object_getIvarin tweak code — useMSHookIvarwhen Substrate is linked.- Bare
%newwithout type encoding — use%new(v@:)(etc.) matching the method signature. MSFindSymbol(NULL, …)— useMSFindSymbol(MSGetImageByName("/path/to/dylib"), "_symbol").
Review checklist
When writing or reviewing tweak code:
- Hooks live in
.x/.xmwith%hook/%hookf, not rawMSHook* - New tweak methods use
%new(typeencoding)with exact encodings; ivar-like state uses%propertywhere appropriate %origusage is correct; grouped hooks have explicit%init- Dynamic
%hookfsymbols resolved withMSGetImageByName+MSFindSymbol(image, …), neverMSFindSymbol(NULL, …) - Default
Tweak.xunless the file uses C++ orMSHookIvar; do not use.xm“just because” - No redundant
respondsToSelector:where OS floor or%groupalready guarantees the API; runtime checks only where version checks are intentionally light - No
objc_msgSendfor normal calls; use[receiver …]with headers for private selectors - Private ivar access in the tweak dylib uses
MSHookIvar, not manual runtime offset/object_getIvar(runtime OK only in non-Substrate targets)
Additional resources
- Directive details and edge cases: reference.md
- Official syntax: https://theos.dev/docs/logos-syntax
