ios - How to change app color theme - ios

Hi I'm new to ios dev and I'm trying to make buttons, which are in my 'settings viewController', change the color theme of the entire app (i.e. background color).
The code below is currently in the appDelegate class, as I found that it works great for this purpose.
So now I'm not sure how to have the buttons in my 'settings viewController' activate the code in the appDelegate. Should I be setting a listener for the event? use closures?
Any help is appreciated. please post code solutions if possible it really helps!:)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
UINavigationBar.appearance().tintColor = UIColor.whiteColor()
UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName:UIColor.whiteColor()]
Style.greenTheme()
}

The solution I use:
extension UIColor{
#nonobjc static var clr_blue: UIColor!
static func initWithColorScheme(cs: ColorScheme){
switch cs{
case .Default:
clr_blue = blueColor()
case .Custom:
clr_blue = UIColor(hue: 0.6, saturation: 0.85, brightness: 1, alpha: 1)
}
}
}
enum ColorScheme{
case Default, Custom
}
Create as many custom colours and colour schemes as you wish. Usage:
UIColor.clr_blue
But don't forget to initialize your UIColor inside application:didFinishLaunchingWithOptions: as follows: UIColor.initializeWithColorScheme(.Custom)
To change the colour scheme of a current UIViewController without need to restart, add this method to your UIViewController:
func changeSchemeTo(cs: ColorScheme){
UIColor.initializeWithColorScheme(cs)
for subview in self.view.subviews{
subview.setNeedsDisplay()
}
}

The appDelegate is specifically for setting options for the whole app. If you're wanting to do these things when a button is tapped, you'll simply want to add those lines of code into an IBAction for the button that's connected to your interface:
#IBAction func greenButtonTapped(sender: AnyObject) {
<#Code to be run when button tapped#>
}
Then just connect it to your UI element and you're all set.

So now I'm not sure how to have the buttons in my 'settings viewController' activate the code in the appDelegate
Code in application:didFinishLaunchingWithOptions: runs automatically when your app launches. Thus this is the right place for the appearance proxy to be configured. There is nothing to "activate".
If you run other code to set the appearance proxy later, be careful. This is not a normal thing to do. You cannot use the appearance proxy to change the way existing interface elements look. You will need to change the existing interface manually when your "theme" changes.

Related

How to change app theme (Light/Dark) programmatically in swift 5

I have an app with up to 10 screens and in the settings screen, I have a toggle to switch between dark/light mode. I want to change the complete app theme on that toggle.
Currently, the app theme change when I change the theme in iOS default settings. But I want the same behavior to happen for my app only.
What I figure out till now is to set a theme variable in the user default and use this code conditional on each view controller
view.overrideUserInterfaceStyle = // .dark // .light
I don't want to use any third party library.
In your app delegate didFinishLaunchingWithOptions read the preference from shared defaults and do something like this:
UIApplication.shared.keyWindow.overrideUserInterfaceStyle = preference == "dark" ? .dark : .light
In your Settings view controller do the same when the user changes the preference.
Setting the override on your window should take care of all view controllers.
Here's how i have done the same approach on similar kind of use case. I have created an extension for AppDelegate like below:
extension AppDelegate {
func overrideApplicationThemeStyle() {
if #available(iOS 13.0, *) {
....
// Retrieve your NSUserDefaults value here
....
UIApplication.shared.keyWindow?.overrideUserInterfaceStyle = yourUserDefaultsValue ? .dark : .light
} else {
// Fallback on earlier versions
}
}
}
And, my theme change is device specific. So, i have created one boolean instance in NSUserDefaults to keep the toggle changes. And, whenever the toggle changes i will call the method:
#IBAction func changeApplicationTheme() {
....
// Your code to store the changes in NSUserDefaults
....
AppDelegate.shared.overrideApplicationStyle()
}
And, more importantly this theme has been introduced from iOS 13 onwards the best place to call it from func sceneDidBecomeActive(_ scene: UIScene) whenever app launches and would like to apply the user selected theme.
func sceneDidBecomeActive(_ scene: UIScene) {
AppDelegate.shared.overrideApplicationStyle()
}

Apply global changes on UIDatePicker and UITableView in SwiftUI

I'm working with SwiftUI at the moment and since I'm supporting dark mode I've declared a color set to which my view has to conform if it's in light/dark mode.
This of course requires a lot of changes in every component and right now I have a lot of Views that has this code in it
init() {
// Make Dividers the same color as the background to make them disappear
UITableView.appearance().separatorColor = UIColor(named: "background")
// Set List background color
UITableView.appearance().backgroundColor = UIColor(named: "background")
UIPickerView.appearance().backgroundColor = UIColor(named: "background")
}
I've tried extending single components but I must be doing something wrong, I've also tried to extend View but build is failing every time, is there a way to declare it in just one file and make it a global change so that I can clear all this redundant code?
This was actually a pretty easy question from what I can see but I've not been around a lot in the iOS world so...
The solution is to put all that code in the AppDelegate file, specifically in
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool

iOS: Default status bar style with UIViewControllerBasedStatusBarAppearance YES

Is there a way how to set the default status bar style while keeping the UIViewControllerBasedStatusBarAppearance enabled?
Here is the problem, I'm dealing with:
Nearly the whole app needs to be using UIStatusBarStyle.LightContent as the navigation bar has a dark background. Originally, UIViewControllerBasedStatusBarAppearance was disabled and the following was set in in Info.plist for while text status status bar:
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
This worked just fine until I found out that this .LightContent status bar is shown even for some of the share extensions, like Facebook Messenger, causing it to be unreadable:
This could be solved by using UIViewControllerBasedStatusBarAppearance, but then I would need to add the following method to all of view controllers which I want to avoid as the app is quite large.
Moreover, for the one screen in the app that has light nav bar background, I was switching to dark nav bar using UIApplication.sharedApplication().setStatusBarStyle() but this method in deprecated in iOS 9.
Any ideas how to solve this? Swizzling?
Solution
The easiest and cleanest way how to achieve that is to add the following line in AppDelegate's application:willFinishLaunchingWithOptions method:
UINavigationBar.appearance().barStyle = .Black
This will make .LightContent as the default status bar style thorough the app as long as your app uses UINavigationController.
Don't forget to keep the following setting in app's Info.plist if want to use .LightContent status bar style during launch for splash screen:
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
TL;DR
My current setup, which is very similar to many other apps, uses UITabBarController as the top most controller with UINavigationController stack for each tab.
UINavigationController takes care of the status bar style (as it should) and do not call preferredStatusBarStyle() on its child view controllers. Therefore, implementing the subclassing solution proposed by par does not work in my case.
Further subclassing the custom subclass of UINavigationController I'm using would not be a clean solution either.
Now, since the app has UIViewControllerBasedStatusBarAppearance enabled and correct status bar style everywhere in the app itself, SFSafariViewController and share extension like Messages, Mail, etc use the correct (.Default) status bar style too.
The only exception where the correct status bar style is not used is the Facebook Messenger's share extension mentioned in the question. However, it seems to be a bug in the extension itself as all apps I have tried that use .LightContent status bar style (like Twitter, for example) have the same issue - presented FB Messenger share extension from the app has a status bar with white color text.
A solution I use quite frequently is to create a base view controller class that all view controllers in my app derive from. This has the advantage of allowing use of the view-controller-based status bar style-setting functionality with a default (light or dark) style, which can then be overridden on a per-view-controller basis as necessary.
A base view controller is also really handy once you start getting into trait-collection based changes, custom transition animations that you want for most view controllers, a central point for analytics tracking, and other useful things.
Yes, you have to go through your potentially large source base and change all your UIViewControllers into BaseViewControllers, but this is often as easy as a global search-and-replace.
Here's what the BaseViewController looks like with status-bar related methods:
class BaseViewController: UIViewController {
var statusBarHidden: Bool = false { didSet { setNeedsStatusBarAppearanceUpdate() } }
var statusBarStyle: UIStatusBarStyle = .lightContent { didSet { setNeedsStatusBarAppearanceUpdate() } }
var statusBarUpdateAnimation: UIStatusBarAnimation = .fade { didSet { setNeedsStatusBarAppearanceUpdate() } }
override var preferredStatusBarStyle: UIStatusBarStyle { return statusBarStyle }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { return statusBarUpdateAnimation }
override var prefersStatusBarHidden: Bool { return statusBarHidden }
}
For all view controllers that use the default light style, you don't need to do anything special:
class ViewController: BaseViewController { }
In the cases where you need a dark status bar, do:
class DarkStatusBarViewController: BaseViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusBarStyle = .default
}
}
Note also that you could rename the DarkStatusBarViewController above to DarkStatusBarBaseViewController and derive from it instead of BaseViewController when you need a dark status bar. Then you don't need to duplicate the status bar code in every view controller that needs it and you maintain a nice linear relationship for all your BaseViewController functionality.

Customizing appearance of UISearchBar & UINavigationBar in AppDelegate? Why customize at class and not instance level?

I am following this tutorial. In its AppDelegate it has a customizeAppearance() where UISearchBar & UINavigationBar are type/class properties. Shouldn't they be a property of something like the window or the current viewController we are in?! How can we just message a class and then have it change our UI?
FWIW when I cmmd click...obviously it just takes it to the class definition.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var backgroundSessionCompletionHandler: (() -> Void)?
var window: UIWindow?
let tintColor = UIColor(red: 242/255, green: 71/255, blue: 63/255, alpha: 1)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
customizeAppearance()
return true
}
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
// MARK - App Theme Customization
private func customizeAppearance() {
window?.tintColor = tintColor
UISearchBar.appearance().barTintColor = tintColor // shouldn't UISearchBar be a property of some other object?
UINavigationBar.appearance().barTintColor = tintColor // shouldn't UINavigationBar be a property of some other object?
UINavigationBar.appearance().tintColor = UIColor.whiteColor()
UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName:UIColor.whiteColor()]
}
}
(I'll add my comments—answering the OP's question regarding class level customization—as an answer, as comments are not persistent. Possibly the OP himself/herself can add an alternative thorough answer based on trying out the queries discussed in the comments)
Quoting the language reference for UISearchBar:
Customizing Appearance
You can customize the appearance of search bars one at a time, or you
can use the appearance proxy ([UISearchBar appearance]) to customize
the appearance of all search bars in an app.
The appearance proxy is covered e.g. in the UIKit User Interface Catalog - About Views:
Appearance Proxies
You can use an appearance proxy to set particular appearance
properties for all instances of a view in your application. For
example, if you want all sliders in your app to have a particular
minimum track tint color, you can specify this with a single message
to the slider’s appearance proxy.
There are two ways to customize appearance for objects: for all
instances and for instances contained within an instance of a
container class.
...
As well as in the language reference for the UIAppearance protocol
Use the UIAppearance protocol to get the appearance proxy for a
class. You can customize the appearance of instances of a class by
sending appearance modification messages to the class’s appearance
proxy.
...
To customize the appearance of all instances of a class, use appearance() to get the appearance proxy for the class.
In the tutorial you follow, they've chosen to use of the appearance proxy approach, making use of the static appearance() method as blueprinted in the UIAppearance protocol (to which e.g. UISearchBar conforms to, via UIView inheritance) to get and modify the appearance proxy of all UISearchBar (and UINavigationBar) instances, from a class level.
The following blog post covers the subject of the appearance proxy. An instructive read, even though being slightly outdated and using Obj-C rather than Swift:
NSHipster - UIAppearance

Disabling Dynamic Type in Swift

I have a Sprite Kit based game that uses a UIView within one of the scenes, and I do that so that I can take advantage of the UITableViewController to present a game settings screen.
The difficulty I am running into is that when a user sets their iPad system accessibility settings to use (extra) large type, the text within the UITableView is too large for the cells and it looks just plain silly.
What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
I have found another similar posting (here) but the response offers an Objective-C response:
#import <objc/runtime.h>
#implementation AppDelegate
NSString* swizzled_preferredContentSizeCategory(id self, SEL _cmd) {
return UIContentSizeCategoryLarge; // Set category you prefer, Large being iOS' default.
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
Method method = class_getInstanceMethod([UIApplication class], #selector(preferredContentSizeCategory));
method_setImplementation(method, (IMP)swizzled_preferredContentSizeCategory);
...
}
I need to do this in Swift.
What is the correct way to do this same thing in Swift in Xcode 7+ ?
Thanks #zeeple for the solution.
Here is the answer to the original question:
"preferredContentSizeCategory" in Objective-C is a method, but in Swift it is a read-only variable.
So in your AppDelegate is like this:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// MARK: - UIApplicationDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.classInit
self.window = UIWindow(frame: UIScreen.main.bounds)
...
self.window?.makeKeyAndVisible()
return true
}
}
// MARK: - Fix Dynamic Type
extension UIApplication {
static let classInit: Void = {
method_exchangeImplementations(
class_getInstanceMethod(UIApplication.self, #selector(getter: fixedPreferredContentSizeCategory))!,
class_getInstanceMethod(UIApplication.self, #selector(getter: preferredContentSizeCategory))!
)
}()
#objc
var fixedPreferredContentSizeCategory: UIContentSizeCategory {
return .large
}
}
Okay, first let me say this: while I am happy that I was able to quickly find a way to accommodate the dynamic text provided by the iOS accessibility settings (which I will show the code for in a sec) I think it is still important to get an answer to the original question.
That said, here is what I did to the table view code to respect the larger type that some users need. It was a two step process. First, add:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
to the viewDidLoad method. Then, in the cellForRowAtIndexPath method, add the following before you return the cell:
cell.textLabel!.numberOfLines = 0
Good luck folks, and please add an answer to the original question if you have one :)
What I would like to do is straight up disable the dynamic type within the app so it always displays the same sized type in the cells.
Dynamic Type only works for text with implemented text styles.
So, if you always want to disable Dynamic Type and display the same sized type in the cells, don't use text styles nor image size adjustment in them.
However, if you do want to use the text styles, never tick Automatically Adjusts Font for each text element in Interface Builder (equivalent to adjustsFontForContentSizeCategoryin code).
Hi All Frustrated Devs,
Here is perfect solution to disable Dynamic Type is :
Since iOS 15 you can set limits on the minimum and maximum sizes of dynamic type. It works for both UIKit and SwiftUI.
// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityExtraLarge
// SwiftUI
ContentView()
.dynamicTypeSize(.medium ... .accessibility3)
Also if you want to directly disable throughout the whole Application, You should create a Base Class which should be parent of your all VCs. In BaseVC you can set up code.

Resources