Digging into SpringBoard

Like many iOS and macOS developers, every year I wait for WWDC to see new APIs, new tools and improvements to existing ones. But besides everything related to development, I’m waiting for the iOS itself – I want to see what has changed for me as a regular iOS user.

This year there were two big changes related to the home screen that have been discussed a lot. Apple introduced widgets and Application Library. Right on the first day beta released I removed all the junk carefully placed in folders “Other” and “Utilities” away from the home screen, leaving only one screen with the stuff I use every day. This was a neat improvement for me: I have not been looking for the app icon I need with my eyes for a long time, but I used the Spotlight each time I need to find anything. Now I able to search it in App Library, that is beauty and fast way to find the app.

In besides with the convenience come with App Library, I paid attention to the top bar with the search field. There is an effect I had never seen before in iOS. This is a gradient blur with a radius that increases from 0 to some value. I thought I’m the only who wondered about it before I had listened to StackTrace podcast, where Guilherme Rambo have also shared his impressions of this UI element. His words challenged me: I could make it at my own?

Something about blur

Let’s get a little talk about blurs. Although Apple provides a rich API allows developer to make a blur and other effects for the image, the ability to do live blur with UI is severely limited. Of course, it is possible to render a layer into image, apply effects and filters on it and show in ImageView, but obviously the performance of such solution is poor. All that we can do is to use UIVisualEffectView, which provides an opportunity to make a frosted glass effect using blur and get some fun with vibrancy effect.

☝️

Although Apple provides a rich API allows developer to make a blur and other effects for the image, the ability to do live blur with UI is severely limited.

Dive in!

So we realized that there is no solution to implement it out of the box. What to do, where to start?

Without a jailbreak we can’t connect to arbitrary process to reveal the view hierarchy and find a view we need. We have to figure it out of the dark. Hopefully we have a simulator and the ability to reveal its file system.

The first step is to figure out what we are looking for. The App Library is located on the home screen, so we have to find something related to the SpringBoard process. SpringBoard is an application that responsible for the displaying home screen and icons on it, launching applications and everything related to it. We will try to find it:

Lucky we! Let’s load a binary in Hopper Disassembler to see what’s inside. Hopper Disassembler is a grate application to research application without a source code. Most likely there is no something interesting inside, as the binary is only 140 Kbytes. The disassembler meets us with a window askig us to choose the architecture, because the binary is fat.

☝️

So starting with Xcode 12, Apple supplies libraries compiled for the classic x86_64 and the new ARM (Apple Silicon) architectures. Now it is clear why Xcode 12 takes so much disk space!

As expected, there is nothing anything useful inside an executable file. Only a few procedures responsible for the launch. So, interesting to us is hiding somewhere else in system frameworks. Let’s see what the binary is linked to.

Private frameworks

There is not much to analyze: SpringBoard’s “guts” obviously are in a private framework with the same name. We have to go back up the file hierarchy to iOS.simruntime and look for it relative to this directory. find . -name ‘SpringBoard.framework’ -print is easily get us to the folder with the private libraries and the framework we need! Load it into the Hopper!

Let’s try to find any symbols related to AppLibrary. Something is there, but not much. Let’s play around with the search and try to find the View Controller of this library. We have a lead! By the string LibraryViewController we can find setters and getters of related VC in the root (I guess) SBIconController. But it wont help us. If it is not here, then it is somewhere else in another framework. Let’s try again a trick with otool.

Oh, no! This framework uses a lot (170!) of external dependencies! Going through all frameworks is a searching needle in haystack. We have to use another way, and there is such one! Mach-O file contain External Symbols segment, that will help us to find source of the original symbol. Let’s return to Hopper and look for something related with class and LibraryViewController keyword.

Here is the symbol _OBJC_CLASS_$_SBHLibraryViewController, and by clicking on it we can see it is declared in external SpringBoardHome framework which located nearly. Load it into Hopper! Now let’s find and explore the class SBHLibraryViewController.

Complaining that your MVC is a MassiveViewController? Take a look at the header of this class: 147 methods and it is normal for Apple. Best practices in best companies, isn’t it? :)

Okay, let’s take a look at the  init method. We will use a switching to pseudo-code mode — the Hopper is so good that it can try to make a human-readable code from a machine code.

The first clue

The init method calls the initWithCategoryMapProvider: which initializes the parent SBNestingViewController via initWithNib:bundle: and creates a special queue for some events. In initWithNib:bundle: two nils are passed, that tell us that initialization of the view occurs in the code. Remember the controller’s lifecycle — to load a view we usually use a loadView method, let’s see what it is.

Ok, we can see the method is overridden and _setupIconTableViewController is called.

Let’s see what’s inside. The method is massive, we don’t want to spent a lot of time to analyze it. Let’s see what kind of views or View Controllers are created there — just search alloc keyword.

And why didn’t I just search the Blur line before?! Ok, it’s not time to blame myself, let’s see what’s inside. The first thing we see is initWithFrame, where SBHFeatherBlurView is created using the initWithRecipe: method with the parameter 0x2.

We are done with Hopper for now, it is time to experiment. Let’s create a new project in Xcode and choose the ObjectiveC as a primary language. Then create UIScrollView and put some content for the experiment. We won’t link the framework directly, we will load it at runtime.

After it, let’s take benefits of ObjectiveC’s runtime to create an instance of class that we didn’t have in our code. Add it to our view and check if our guess is correct. Especially for this article, I made an example similar to the original screen to compare it with. Well, let’s run it on a simulator.


#import "FeatherBlurView.h"
#import 

#define SBH_PATH @"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/SpringBoardHome.framework/SpringBoardHome"

// Объявим интерфейс класса из SpribngBoardHome для удобной инициализации
@interface SBHFeatherBlurView : UIView
- (instancetype) initWithRecipe:(int)arg1;
@end

@implementation FeatherBlurView

+ (void)load {
    const char *path = [SBH_PATH cStringUsingEncoding:NSUTF8StringEncoding];
    dlopen(path, RTLD_NOW);
}

- (instancetype) init {
    self = [super init];
    if (self) {
        SBHFeatherBlurView *view = [NSClassFromString(@"SBHFeatherBlurView") alloc];
        view = [view initWithRecipe:0x2];
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:view];
        [[view.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
        [[view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
        [[view.leftAnchor constraintEqualToAnchor:self.leftAnchor] setActive:YES];
        [[view.rightAnchor constraintEqualToAnchor:self.rightAnchor] setActive:YES];
    }
    return self;
}

@end

Yeah! Everything turned out and works as I expected. You can download sample project and play with it.

🖖

Time to wrap up and highlight interesting facts:

  • Looking under the hood of the OS is not so difficult and very interesting
  • Objective-C and controller life cycle knowledge can be useful in 2k21.
  • All frameworks and utilities inside Xcode come in bold binary compiled for ARM and x86_64, this partly explains the almost doubled Xcode size.
  • Despite the 2020 year and the current Swift version 5.4, Apple actively uses Objective-C in system frameworks. Even the code for managing widgets (which, for a second SwiftUI-only) is written in Objective-C.
  • Apple’s engineers aren’t afraid of Massive View Controller and deep inheritance


But is it really that simple and anyone can use it in his own projects?! Unfortunately, no. We cant use private libraries in our code, not because it prohibited by Apple, but because of the fact we have no possibility to use external frameworks while app is running in the sandbox.

That is, for now we can’t run it on a device. But this is not a reason to give up! Right in the next article we will go deeper and research how does this view works and try to implement it our code in a way that it will run on a device! Stay tuned!

iOS 15 update

SBHFeatherBlurView has been moved to new private framework SpringBoardFoundation and renamed to SBFFeatherBlurView.

Share
Send
Pin
2022