Custom UITabBar subclass without Interface Builder

2020-04-27 07:50发布

Using IB (be it with a Storyboard or a XIB), one can easily have the UITabBarController use a subclass of UITabBar, by editing the class name in the Identity Inspector -> Custom Class.

How to mimic this "custom class" feature of IB, without using it at all ?

I tried the following (in my subclass of UITabBarController) :

var customTabBar = MyCustomTabBarSubclass()

override var tabBar: UITabBar {
    return customTabBar
}

To no avail – the tab bar is displayed, but blank. The issue is not elsewhere since returning super.tabBar from the overriden var fixes it.

The issue, I guess, is that I'm not setting up my customTabBar (frame, position, adding it to the view hierarchy), but I'd like to have the UITabBarController loadView (I think that's the one, not sure) do it for me, the same way it sets up any other UITabBar.

Ideas ?

3条回答
放我归山
2楼-- · 2020-04-27 08:06

If you choose to disregard what Apple says about what's supported (using a custom UITabBar subclass with Interface Builder, and only with it), here's a dirty solution (that works) :

It requires mild knowledge of the ObjC runtime, because we're going to swizzle stuff around... Essentially, the issue is that I can't force UITabBarController to instantiate the class I want it to instantiate (here, MyCustomTabBarSubclass). Instead, it always instantiates UITabBar.

But I know how it instantiates it : by calling -[[UITabBar alloc] initWithFrame:]. And I also know that all functions belonging to the init family are allowed to return either an instance of their class, or of a subclass (that's the basis of Class Clusters).

So, I'm going to use this. I'm going to swizzle (= replace the implementation) of UITabBar's -initWithFrame: method with my custom version of it, that, instead of calling up (self = [super initWithFrame:]) will call "down" (self = [MyCustomTabBarSubclass.alloc initWithFrame:]). Thus, the returned object will be of class MyCustomTabBarSubclass, which is what I'm trying to achieve.

Note how I'm calling MyCustomTabBarSubclass.alloc – this is because my subclass potentially has ivars that UITabBar does not have, thus making it larger in its memory layout. I might have to release self before reallocating it, otherwise I could be leaking the allocated memory, but I'm not sure at all (and ARC "forbids" me to do call -release, so I'd have to use another step of trickery to call it).


EDIT

(First thing, this method would also work for any case where IB's custom classes are of use).

Also note that implementing this requires writing ObjC code, as Swift does not allow us to call alloc, for instance – no pun intended. Here's the code :

IMP originalImp = NULL;
id __Swizzle_InitWithFrame(id self, SEL _cmd, CGRect frame)
{
    Class c = NSClassFromString(@"MyBundleName.MyCustomTabBarSubclass");
    self = [c alloc]; //so that we'll return an instance of MyCustomTabBarSubclass
    if (self) {
        id (*castedImp)(id, SEL, CGRect) = (id (*)(id, SEL, CGRect))originalImp;
        self = castedImp(self, _cmd, frame); //-[super initWithFrame:]
    }
    return self;
}

You'll also have to ensure that the actual swizzling operation is only performed once (such as, dispatch_once). Here's the code that actually swizzles :

Method method = class_getInstanceMethod(NSClassFromString(@"UITabBar"), @selector(initWithFrame:));

IMP swizzleImp = (IMP)__Swizzle_InitWithFrame;
originalImp = method_setImplementation(method, swizzleImp);

So that's it for the ObjC side. Swift-side :

@objc class MyCustomTabBarSubclass: UITabBar {
    lazy var anIvar: Int = 0 //just a example obviously
    // don't forget to make all your ivars lazy or optional
    // because the initialisers WILL NOT BE CALLED, as we are
    // circumventing the Swift runtime normal init mechanism
}

And before you initialise your UITabBarController, don't forget to call the ObjC code that performs the swizzling.

That's it ! You have cheated UITabBarController into instantiating your own subclass of UITabBar, and not the vanilla one. If you're working in pure ObjC, things are even easier (no messing with bridging headers, a subject I didn't cover here).

Obligatory DISCLAIMER : Messing with the ObjectiveC runtime is obviously not something to do lightly. Ensure you have no better solution – IMHO, using a XIB only for the purpose of avoiding such tinkering is a better idea than implementing my suggestion.

A example of issue that could arise : if you're using multiple tab bars in your app, you might not want all of them to be MyCustomTabBarSubclass instances. Using my code above without modifications would result in all tab bars to be instances of MyCustomTabBarSubclass, so you'd have to find a way to tell __Swizzle_InitWithFrame directly call the original implementation, or not.

查看更多
▲ chillily
3楼-- · 2020-04-27 08:17

This works, but there's a chance you might not pass App Store review because it's setting a value that public API doesn't allow you to set.

Inside your UITabBarController's subclass' initWithNibName:bundle: or viewDidLoad, add this:

MyCustomTabBar *customTabBar = [[MyCustomTabBar alloc] initWithFrame:CGRectZero];
customTabBar.delegate = self;
[self setValue:customTabBar forKey:@"tabBar"];

Consider this just a proof of concept, not something you should necessarily use in your production app because it's technically using a private setTabBar: method.

查看更多
地球回转人心会变
4楼-- · 2020-04-27 08:19

I think you can't. For this question, I had some time to scan through the documentations of UITabBar and UITabBarController.

The tabBar property of UITabBarController is a get-only.

@available(iOS 3.0, *)
    open var tabBar: UITabBar { get } // Provided for -[UIActionSheet showFromTabBar:]. Attempting to modify the contents of the tab bar directly will throw an exception.

Furthermore, it is stated in UITabBarController's documentation that you shouldn't manipulate such property.

You should never attempt to manipulate the UITabBar object itself stored in this property. If you attempt to do so, the tab bar view throws an exception. To configure the items for your tab bar interface, you should instead assign one or more custom view controllers to the viewControllers property. The tab bar collects the needed tab bar items from the view controllers you specify.

The tab bar view provided by this property is only for situations where you want to display an action sheet using the show(from:) method of the UIActionSheet class.

Just wanna add: but unlike this UITabBarController, subclassing the UINavigationController gives you the power to init such subclass with a subclass of UINavigationBar:

UINavigationController(navigationBarClass: <#T##AnyClass?#>, toolbarClass: <#T##AnyClass?#>)

Unfortunately, UITabBarController does not have such kind of init method.

查看更多
登录 后发表回答