Using UIAppearance and switching themes - ios

I'm looking to theme my iOS app and have been reading up on UIAppearance. I want the user to be able to switch between a number of different visual themes from within the app. Changing a theme would then be shown in the UI.
I'm thinking I could have a theme file that is a singleton loaded within the appDelegate.m. But after that i'm a little stuck on how this could be implemented?

UIKit sets properties from UIAppearance proxy after view is added to views hierarchy.
In UISS I use method like this:
- (void)reloadAppearance {
NSArray * windows = [UIApplication sharedApplication].windows;
for (UIWindow *window in windows) {
for (UIView *view in window.subviews) {
[view removeFromSuperview];
[window addSubview:view];
}
}
}
Another trick is to remove rootViewController from main window and add it again. Though I prefer the first solution, because it covers wider range of cases.

This works for me in Swift:
let windows = UIApplication.sharedApplication().windows
for window in windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}

The solution suggested by Robert, doesn't work for me . I'm using iOS9 and UITabbarController .
This suggestion is the only that worked from answer here
It basically explained the problem well, when we use UIAppearance, it basically is applied and will not change until the views is redrawn again which will not happen especially with rootViewController like UITabbarControllers and new colors will only have effect ONLY AFTER the App is removed from memory and opened again, if you want an immediate change , you will have to make it by hand and setting each property like described in the answer in attached link

swift 4:
let windows = UIApplication.shared.windows
for window in windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}

Related

Covering window of UIAppDelegate with an overlay make user can't touch on extensions of UIActivityViewController when it's displayed

I'm creating an overlay which will cover all displaying views on screen. This overlay always appears even in case rootViewController changes, pushing or presenting.
My idea is
Create CustomWindow which is a subclass of UIWindow. After that replacing default window of UIApplication with CustomWindow, create a new rootViewController for my new window.
In CustomWindow, I have an overlay (is an UIView). Overlay have light gray color with an alpha and every event on overlay will be pass through to below view.
Whenever CustomWindow add a new subview, i will bring overlay to front. It's make sure overlay will be on the top in every case.
CustomWindow
#implementation CustomWindow
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_overlay = [[UIView alloc] initWithFrame:self.bounds];
_overlay.userInteractionEnabled = NO;
_overlay.backgroundColor = [UIColor lightGrayColor];
_overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:_overlay];
}
return self;
}
- (void)didAddSubview:(UIView *)subview {
[super didAddSubview:subview];
[self bringSubviewToFront:_overlay];
}
#end
Everything works fine in every case even when pushing, presenting or changing rootViewController.
Problem
But when i show an UIActivityViewController, I can't click on any extensions which are displayed on UIActivityViewController.
Magically
When i click outside of UIActivityViewController or click on Cancel Button, UIActivityViewController is dismissed normally.
If i change color of overlay to clearColor, it works fine too.
My question is
How can i touch on extensions when i have overlay on window and overlay have a color ?
If i can't, can anyone tell me why it happens ? It's perfect when you can quote the reason from a document.
I'm pretty sure this doesn't relate to how i initialize UIActivityViewController or the way i show UIActivityViewController.
MORE
I found a problem quite similar to this problem on Android. But i'm not sure because i haven't seen any official document about it from Apple. One more thing is when changing color to clearColor can affect touch. So actually, i don't think they are same.
This is due to a UIRemoveView (private) in the hierarchy. As best I can determine, your app cannot forward events directly to remote views. I suspect this is a security measure to prevent you from presenting the share dialog and automatically sending a touch event to it to do an external action the user didn't request. Remote views don't run in your application's process. The "Copy" button is interacted with across an XPC link.
This all means that if the remote view is covered by one of your views, there's no way (at least that I've found) to interact with it. You have to ensure that you're not covering it.
Actually doing that is simple. The thing that holds the remote view is called a UITransitionView and is used for other OS-level things that you probably shouldn't be covering either. So don't:
- (void)didAddSubview:(UIView *)subview {
[super didAddSubview:subview];
if ([subview isKindOfClass:NSClassFromString(#"UITransitionView")]) {
// To raise it above your overlay;
// otherwise it's immediately above the view controller (below the overlay)
[self bringSubviewToFront:subview];
} else {
[self bringSubviewToFront:self.overlay];
}
}
But.... This requires you to talk about UITransitionView in your code. This is both fragile, and possibly a forbidden use of private APIs.
Otherwise you'll have to wrap your UIActivityViewController requests with some call/notification that tells the window not to cover views until we're done (which you'll have to clear in the completion handler).

Remove pageIndicator on UIPageViewController on rotation

When in landscape, I would like the content of the UIPageViewController to be full screen, and I want to hide the page indicators. In portrait, I want to show the page indicators.
I know that implementing the data source methods are what make the page indicators show/not show (see https://stackoverflow.com/a/20749979/1103584), but as I stated earlier, I want to be able to selectively hide them depending on the orientation of my app.
How can I do this? I've seen it in other apps before (the graphs in App Annie) so I know it's possible. An answer where you iterate through subviews of the UIPageViewController to find an instance of UIPageControl sounds like a very hacky solution to me...there must be a more "official" way to do it.
Thanks in advance.
Currently, there is no way to change the default behavior of UIPageViewController's page indicator after the setViewControllers:direction:animated:completion: method is called. Hopefully, a similar method to hiding a navigation bar will be added in a future update.
Try adding this to your code...
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
[super willAnimateRotationToInterfaceOrientation:orientation duration:duration];
UIPageControl *pageControl = [UIPageControl appearance];
if (UIInterfaceOrientationIsLandscape(orientation))
{
pageControl.alpha = 0;
}
else if (UIInterfaceOrientationIsPortrait(orientation))
{
pageControl.alpha = 1;
}
}

Force view controller to reload to refresh UIAppearance changes

I have been searching for quite a while and can't find an answer. I am working on an iOS app and have a modal settings page that appears on the tap of a button and returns with a segue.
One of the options I would like to implement is a color scheme setting. I really want to avoid manually changing the color for every element on the page.
Apple has a UIAppearance protocol for this sort of thing (so I can set the text color of all buttons, etc.
Their documentation says:
Note: iOS applies appearance changes when a view enters a window, it doesn’t change the appearance of a view that’s already in a window. To change the appearance of a view that’s currently in a window, remove the view from the view hierarchy and then put it back.
My question is how to do this. I have tried calling viewWillAppear and setNeedsDisplay without luck.
Try to use this snippet :
NSArray *windows = [UIApplication sharedApplication].windows;
for (UIWindow *window in windows) {
for (UIView *view in window.subviews) {
[view removeFromSuperview];
[window addSubview:view];
}
}
http://snipplr.com/view/75259/refresh-uiappearance-after-application-loaded/
It works perfect for me after changing app theme using UIAppearance
Please note that the top answer will have adverse effects on your system keyboard behavior.
It turns out that iOS creates a new system window with UITextEffectsWindow class under the hood whenever the keyboard is displayed. If you remove it, your keyboard behavior may be negatively affected. For example, the input accessory views will be detached from the keyboard and will not be visible, except for brief flashes in the navigation controllers.
You can workaround this issue by using an additional check, like so:
for window in UIApplication.shared.windows {
// Whenever a system keyboard is shown, a special internal window is created in application
// window list of type UITextEffectsWindow. This kind of window cannot be safely removed without
// having an adverse effect on keyboard behavior. For example, an input accessory view is
// disconnected from the keyboard. Therefore, a check for this class is needed. In case this class
// that is indernal is removed from the iOS SDK in future, there is a "fallback" class check on
// NSString class that always fails.
if !window.isKind(of: NSClassFromString("UITextEffectsWindow") ?? NSString.classForCoder()) {
window.subviews.forEach {
$0.removeFromSuperview()
window.addSubview($0)
}
}
}
Note that the UITextEffectsWindow is internal and may change in the future. This is why I do not unwrap the variable using ! but provide a fallback negative NSString class instead (no type of window is of NSString class).
Note: For simple apps, you can probably live by using UIApplication.shared.keyWindow for the workaround.
Specifically, to get the current view and it's superview, try:
UIView *currentview = self.window.rootViewController.view;
UIView *superview = currentview.superview;
[currentview removeFromSuperview];
[superview addSubview:currentview];
Works for me.
For Swift:
let windows = UIApplication.sharedApplication().windows
for window in windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}
For Swift 3.0.2:
for window in UIApplication.shared.windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
// update the status bar if you change the appearance of it.
window.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}
Here's a Swift 5 one-liner:
UIApplication.shared.windows.forEach { $0.subviews.forEach { $0.removeFromSuperview(); self.window?.addSubview($0) }}
Try
[self.yourView removeFromSuperView];
[self addSubView:yourView];
For swift 4:
let windows = UIApplication.shared.windows
for window in windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}
The most answers are very good and perfect for changing language from LTR to RTL but sometimes tab bar navigation titles and navigation bar titles will not get translated. I fixed the problem with the following code
if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window {
window.rootViewController = TabNavigationController()
let tab = window.rootViewController as? UITabBarController
tab?.selectedIndex = 3
window.makeKeyAndVisible()
}
Objective c
self.view.window.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
I use this code if I want change overrideUserInterfaceStyle in all view controllers

How can I modify PKRevealController slide-out menus to deal with iOS 7?

I have an application using PKRevealController which implements a slide-out menu similar to the ones in the popular Facebook and GMAIL apps on iOS. The app is built in XCode 5, and runs on iOS 6 and iOS 7. I need to figure out how to have it work sanely in both places, so a simple .XIB hack that makes it look okay in iOS 7 but makes it look worse in iOS 6 is not okay.
The code works great for iOS 6, where the status bar is opaque and the top view is not alpha-blended with the status bar.
However, on iOS 7, just for example, I have created this view in my .xib file, here is how it appears running in ioS 6 simulator, shown here with the slide out menu opened:
The same .xib file running on ios 7, when the slide-out menu open, the top of the slide out menu's .xib content is now under the status bar, as Apple said it would be in their ios 7 transition guide:
The class I need to modify in PKRevealController is probably the presenting view controller that is creating and presenting the contained view, the contained view is called PKRevealControllerContainerView, I think. I think I probably need to create
some kind of view hierarchy like this:
[ Outermost View container
[ some kind of blob to occupy the header area ]
[ the client view I want to appear the way it did in iOS 6]
]
I've been reading around, and there may be much simpler approaches, but I don't quite understand them, approaches like adding properties to my info.plist, like View controller-based status bar appearance = YES. I tried that it did not have the desired effect.
How do I go about fixing this? I have read the Fine Guide published by Apple and it has not provided code, only general guidance like this page on the status bar.
It's easy to replicate this problem, just clone the git repo https://github.com/pkluz/PKRevealController, build and run.
The code that brings up the pop-up view looks like this:
- (void)addLeftViewControllerToHierarchy
{
if (self.leftViewController != nil && ![self.childViewControllers containsObject:self.leftViewController])
{
[self addChildViewController:self.leftViewController];
self.leftViewContainer.viewController = self.leftViewController;
if (self.leftViewContainer == nil)
{
self.leftViewContainer = [[PKRevealControllerContainerView alloc] initForController:self.leftViewController shadow:NO];
self.leftViewContainer.autoresizingMask = [self autoresizingMaskForLeftViewContainer];
}
self.leftViewContainer.frame = [self leftViewFrame];
[self.view insertSubview:self.leftViewContainer belowSubview:self.frontViewContainer];
[self.leftViewController didMoveToParentViewController:self];
}
}
The above is invoked by PKRevealController.m, like this:
- (void)showLeftViewControllerAnimated:(BOOL)animated
completion:(PKDefaultCompletionHandler)completion
{
__weak PKRevealController *weakSelf = self;
void (^showLeftViewBlock)(BOOL finished) = ^(BOOL finished)
{
[weakSelf removeRightViewControllerFromHierarchy];
[weakSelf addLeftViewControllerToHierarchy]; // HELLO LEFT Slide-out menu.
....
Is there a better approach than my idea? Did Apple provide some way to make this easy or does trying to support iOS 6 and iOS 7 in a single codebase leave me doing hacks like the above I'm considering?
Here, for instance, is a really ugly hack where I don't bother placing any view underneath the apple system status bar, leaving a black bar at the top, which is no good, but it shows I'm modifying the right area in the code, at least:
- (void)addLeftViewControllerToHierarchy
{
CGRect lvFrame;
if (self.leftViewController != nil && ![self.childViewControllers containsObject:self.leftViewController])
{
[self addChildViewController:self.leftViewController];
self.leftViewContainer.viewController = self.leftViewController;
if (self.leftViewContainer == nil)
{
self.leftViewContainer = [[PKRevealControllerContainerView alloc] initForController:self.leftViewController shadow:NO];
self.leftViewContainer.autoresizingMask = [self autoresizingMaskForLeftViewContainer];
}
lvFrame = [self leftViewFrame];
lvFrame.origin.y += 20; // ugly hack demo code only! don't really do it this badly!
lvFrame.size.height -= 20;
self.leftViewContainer.frame = lvFrame;
[self.view insertSubview:self.leftViewContainer belowSubview:self.frontViewContainer];
[self.leftViewController didMoveToParentViewController:self];
}
}
The above hack is almost enough, if I also add this to UIViewController+PKRevealController.m:
-(UIStatusBarStyle)preferredStatusBarStyle{
return UIStatusBarStyleBlackOpaque;
}
The above code, when added, causes the following hint/warning:
Category is implementing a method that will also be implemented by its primary class.
I'm including the above notes to show what I've tried, and I welcome some idea of how the real experts are doing this.
My own modified copy of the PKRevealController code, including the hack above, in a slightly improved form, is found here: https://github.com/wpostma/PKRevealController
I've been struggling with PKRevealController as well. While I'm still looking for better solutions I will share what I came up with until now.
My two problems were:
Status bar style was always the same and I wanted a different style for the front view and the menu;
The menu view top cell (it's a table view controller) showed up behind the status bar.
1. Dynamic status bar style
First I had my own PKRevealController subclass where I was having a custom initialiser and some custom methods to load new view controllers into the front view navigation view controller. But that's not relevant for now.
I used this subclass to implement preferredStatusBarStyle as follows so that the reveal controller can provide the right style for each state:
- (UIStatusBarStyle)preferredStatusBarStyle {
switch (self.state) {
case PKRevealControllerFocusesLeftViewController:
return [self.leftViewController preferredStatusBarStyle];
break;
case PKRevealControllerFocusesRightViewController:
return [self.rightViewController preferredStatusBarStyle];
break;
case PKRevealControllerFocusesFrontViewController:
return [self.frontViewController preferredStatusBarStyle];
break;
case PKRevealControllerFocusesLeftViewControllerInPresentationMode:
return [self.leftViewController preferredStatusBarStyle];
break;
case PKRevealControllerFocusesRightViewControllerInPresentationMode:
return [self.rightViewController preferredStatusBarStyle];
break;
default:
return UIStatusBarStyleDefault;
break;
}
}
This alone doesn't work however. You still have to say that the status bar style needs to change with setNeedsStatusBarAppearanceUpdate. As Apple says this should be called from inside an animation loop and you can find one in PKRevealController's setFrontViewFrameLinearly method. This is how it looks after I've modified it:
- (void)setFrontViewFrameLinearly:(CGRect)frame
animated:(BOOL)animated
duration:(CGFloat)duration
options:(UIViewAnimationOptions)options
completion:(PKDefaultCompletionHandler)completion
{
[UIView animateWithDuration:duration delay:0.0f options:options animations:^
{
self.frontViewContainer.frame = frame;
if ([[[UIDevice currentDevice] systemVersion] compare:#"7.0" options:NSNumericSearch] != NSOrderedAscending) {
[self setNeedsStatusBarAppearanceUpdate];
}
}
completion:^(BOOL finished)
{
safelyExecuteCompletionBlockOnMainThread(completion, finished);
}];
}
If you try it out at this point the styles will be mixed up. You can quickly conclude that by the time preferredStatusBarStyle is called the reveal controller state is still not changed. For that go to every method that sets the state, e.g. enterPresentationModeForRightViewControllerAnimated and set the state before it calls any change to the frame (the one is going to trigger the animation loop). I did it in 5 different places.
2. Left/Right menu with inset
For this one I have to say I used a workaround: I've just set a header view on the table view (tableHeaderView property).
Put this in viewDidLoad of your UITableViewController:
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.tableView.frame.size.width, 20.f)];
headerView.backgroundColor = [UIColor whiteColor];
self.tableView.tableHeaderView = headerView;
Don't forget to add some condition so it doesn't get executed in iOS 6. Use this other answer to know how to do it.
If you don't need iOS 5- support, you could use autolayout and align topmost views to topLayoutGuide.
So, for example, if your left view controller is a UIViewController with a UITableView in it, you could snap UITableView's top edge to the topLayoutGuide.
You can do it in (1) IB (storyboard) or (2) from code.
I personally prefer the first approach, as far as it removes the need of unnecessary code. You just open your storyboard and snap your table view's top edge to topLayoutGuide. In iOS 7 you'll end up with topLayoutGuide constraint, in iOS6 topLayoutGuide constraint is converted to a common to-container-view constant.
If you use second approach, you'll have to make sure you don't use topLayoutGuide in iOS6, something like this:
// assume you'r in your UIViewController subclass
if (![self respondsToSelector:#selector(topLayoutGuide)])
{
// topLayoutGuide is not supported, probably iOS6
// add constraints to snap tableview's top edge to superview's top edge
}
else
{
// cool, topLayoutGuide is supported, probably iOS7
// add constraints to snap tableview's top edge to topLayoutGuide
}

Hide or temporarily remove a child ViewController from a parentViewController?

(asking and self-answering, since I found no hits on Google, but managed to find a solution in the end by trial and error)
With iOS 5 and 6, Apple added some ugly hacks to make InterfaceBuilder support "embedded" viewcontrollers. They didn't document how those work, they only give code-level examples, and they only cover a limited subset of cases.
In particular, I want to have an embedded viewcontroller that is sometimes hidden - but if you try the obvious approach it doesn't work (you get a white rectangle left behind):
childViewController.view.hidden = TRUE;
Why don't you just create an IBOutlet to your container view and do
self.containerView.hidden = YES;
How they've done it appears to be a variation on the manual way that worked since iOS 2 (but which only supported views, not viewcontrollers) - there is a real, genuine UIView embedded into the parent (not mentioned in the source code examples - it's only added when you use InterfaceBuilder!).
So, instead, if you do:
childViewController.view.superview.hidden = TRUE;
...it works!
Also, counterintuitively, you can call this method at any time from viewDidLoad onwards - the "embed segue" hack from Apple is executed before viewDidLoad is called.
So you can do this on startup to have your childViewController start off invisible.
Use This [self.childviewController setHidden:YES];
In case somebody will need to hide/show all child views or iterate over them:
func hideChildrenViews() {
for view in self.view.subviews {
(view as! UIView).hidden = true
}
}
func showChildViews() {
for view in self.view.subviews {
(view as! UIView).hidden = false
}
}

Resources