UIView overrideUserInterfaceStyle is not working - ios

I want the app to adhere to the system light/dark modes, but one child view to be stuck to light mode.
So technically, this should work:
myChildView.overrideUserInterfaceStyle = .light
This does not work!
So there are a few methods to override the interface style:
UIApplication.current.windows.first.overrideUserInterfaceStyle = .light
This works where it locks the whole app to light mode.
myViewController.overrideUserInterfaceStyle = .light
This does not work in any of my view controllers.
myViewControlller.view.overrideUserInterfaceStyle = .light
This also doesn't work.
myChildView.overrideUserInterfaceStyle = .light
This is what I need to work, but doesn't.
I also do not have it specified in the info.plist to lock the whole app into any mode.
Am I missing something?
UPDATE
The issue is still present on iOS 15 or iOS 16 betas. Above methods don't seem to work:
https://developer.apple.com/documentation/uikit/appearance_customization/supporting_dark_mode_in_your_interface/choosing_a_specific_interface_style_for_your_ios_app

I use this with UITraitCollection(userInterfaceLevel: .elevated) but should work just as well with userInterfaceStyle. (You will get some logs saying you shouldn't override traitCollection, just ignore them)
override var traitCollection: UITraitCollection {
UITraitCollection(traitsFrom: [super.traitCollection, UITraitCollection(userInterfaceStyle: .light)])
}
The documentation for what you are trying to use mentions some exceptions, I don't know if that is what makes it not work for you: https://developer.apple.com/documentation/uikit/uiview/3238086-overrideuserinterfacestyle
If you assign a different value, the new style applies to the view and all of the subviews owned by the same view controller. (If the view hierarchy contains the root view of an embedded child view controller, the child view controller and its views do not inherit the interface style.)

Related

Custom UITabBar subclass without Interface Builder

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 ?
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.
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.
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.

watchOS 2: setting properties on initial Interface Controller

Starting with watchOS 2, we have an ExtensionDelegate object, which is analogous to UIApplicationDelegate (reacts to app lifecycle events).
I want to get a reference to the first Interface Controller object, which will be displayed upon launch, to set a property on it (e.g. pass in a data store object).
According to the docs, the rootInterfaceController property on WKExtension hands back the initial controller:
The root interface controller is located in the app’s main storyboard
and has the Main Entry Point object associated with it. WatchKit
displays the root interface controller at launch time, although the
app can present a different interface controller before the launch
sequence finishes.
So I try the following in ExtensionDelegate:
func applicationDidFinishLaunching() {
guard let initialController = WKExtension.sharedExtension().rootInterfaceController else {
return
}
initialController.dataStore = DataStore()
}
Even though the correct Interface Controller is displayed, rootInterfaceController is nil at this point. Interestingly if I query the same property in the willActivate() of my Interface Controller, the property is set correctly.
In an iOS app, you can already get the root view controller in applicationDidFinishLaunching(), and I thought it should work the same for watchOS.
Is there a way to set properties on my Interface Controller before it's displayed (from the outside)? Is this a bug?
Many thanks for the answer!
You might move your code to applicationDidBecomeActive.
This page describes the states of watch apps. When applicationDidFinishLaunching is invoked, the app is in an inactive state.
https://developer.apple.com/library/watchos/documentation/WatchKit/Reference/WKExtensionDelegate_protocol/index.html
If you are calling this from within another interface controller, try move the WKExtension.sharedExtension().rootInterfaceController to the willActivate() function. It seems like if it is in the awake() function it sometimes works but is unreliable.

XCode 6 - iOS 8: programmatically check if view is installed

I am using class sizes in interface builder, defining slightly different designs for different sizes.
One of my view is not installed for a specific size. That works as expected, but now I would like to programmatically be able to tell if that view is installed or not. Whether it is installed or not, it looks like the view is never nil, and I can't see any isInstalled flag to check.
What is the correct way to do this?
This isn't a great solution, but I've not found a better one yet:
The docs state that "A runtime object for an uninstalled view is still created. However, the view and any related constraints are not added to the view hierarchy and the view has a superview property of nil".
So a test for a valid superview works as a solution, but I've found that it has to come quite late - in viewDidAppear. The superviews are still nil in viewWillAppear, for example.
From Apple Docs:Installing and Uninstalling Views for a Size Class
A runtime object for an uninstalled view is still created. However, the view and any related constraints are not added to the view hierarchy and the view has a superview property of nil. This is different from being hidden. A hidden view is in the view hierarchy along as are any related constraints.
You could check by evaluating PossiblyUninstalledView.superView != nil. If it is true, then the class is properly installed.
You could make an extension to UIView and check to see if the view has a superview. If it's Installed it will return true, if it's no it will return false.
extension UIView {
func isInstalled() -> Bool{
return (self.superview != nil) ? true : false
}
}

Style elements defined in Interface Builder are not being applied when instantiating from Storyboard

I have an iOS app developed in Xamarin.iOS (C#, Monotouch) where the primary UI is NOT storyboard-based. (I do this because my app needs structurally different layouts in portrait and landscape orientations, and it is a great deal easier to achieve that programmatically than through IB and Storyboarding.)
My problem is that I'm now trying to use a Storyboard to develop a simple dialog, but when I instantiate the dialog the structure is there but the style elements defined in Interface Builder are not being applied. Everything I've read seems to suggest that this should just happen. This is particularly problematic as most of the style elements cannot be modified after the interface is initialized.
Here's the code where I do the instantiation:
UIStoryboard sb = UIStoryboard.FromName("StoryboardAppSettings", null);
var vc = sb.InstantiateViewController("TableViewControllerAppSettings");
UIViewController settingsVC = vc as UIViewController;
PresentViewController(settingsVC, true, null);
I have defined a UITableView that is to be "grouped", but it isn't. I have two UISwitch elements with colors defined that are not being applied. I have buttons whose tint colors are not being applied.
(If I set a breakpoint and drill down into the view controller data structures, I find that all these parameters ARE set correctly...they just seem to be ignored when the hierarchy is realized.)
What am I doing wrong?
I found that the problem was that I was setting UIView.Appearance.BackgroundColor. It seems that when you do that, the UIView.Appearance.BackgroundColor ALWAYS overrides UISwitch.OnTintColor. (In my mind that is a bug, probably in iOS, but I may misunderstand how the Appearance API is supposed to work. My expectation is that a generalized global setting (like UIView.Appearance) can be overridden at more specific levels (descendent views or specific instantiations), and I believe that is the way it works for most other Appearance properties. UISwitch.OnTintColor (as well as UISwitch.TintColor and UISwitch.BackgroundColor, but NOT UISwitch.ThumbTintColor) seems to be the exception (and hence bug), not the rule.

Global hook for showing UIViews

I have an iOS app I'm working on using Xamarin and MVVMCross, but I am also using a third-party native library which includes some views of it's own (loaded from .xib files with the implementation in the library). What I need to do is set some properties on those native views and I'm trying to see if there's a way to do it that doesn't involve jumping into xcode and trying to recompile that whole thing (because I can't get that working at the moment).
So my question is, is there a way to intercept, application-wide, all attempts to load a view so that I can examine the view and if it's one of those from the third-party library, set some properties on it before it's displayed?
MvvmCross has a MvxTouchViewPresenter which has a ChangePresentation property, but it seems to only apply to MvxViewController loaded by MvvmCross itself.
You can very easily intercept all attempts to access a viewmodel by overriding the Show() method on your MvxTouchPresenter. For example:
public override void Show(MvxViewModelRequest request)
{
IMvxTouchView view = this.CreateViewControllerFor(request);
UIViewController viewController = (UIViewController) view;
this.Show(view);
}
You can then examine all Views in the UIView heirarchy by using something similar to the Objective-C code in this post. You just need to walk through all the UIViews in the viewController property and identify your view (perhaps by "smelling it" with respondsToSelector; I can't figure out exactly how you'd use isKindOfClass if Xamarin doesn't know it).
I hope I understood your question. Let me know if there's anything else missing.

Resources