问题
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 return
ing 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 ?
回答1:
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.
回答2:
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.
来源:https://stackoverflow.com/questions/54314577/custom-uitabbar-subclass-without-interface-builder