I have a login screen which look like this:
Basically, i want to setup so that after the following actions: when in this login screen -> enter background -> toggle darkmode or lightmode in settings -> enter foreground ,the dropShadowView and its element are changed accordingly. Here down below is my code:
in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
setupForDarkmode()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
And function:
#objc func willEnterForeground() {
setupForDarkmode()
}
private func setupForDarkmode() {
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .unspecified
if traitCollection.userInterfaceStyle == .dark {
dropShadowView.backgroundColor = .black
userNameTextField.backgroundColor = .black
userNameTextField.textColor = .white
passwordTextField.backgroundColor = .black
passwordTextField.textColor = .white
} else {
dropShadowView.backgroundColor = .white
userNameTextField.backgroundColor = .gray
userNameTextField.textColor = .blue
passwordTextField.backgroundColor = .gray
passwordTextField.textColor = .blue
}
}
}
screen's background was already set as system's background color.
However these above code wasn't worked as i expected. The first time it enter foreground right after the change in setting, the dropShadowView and its element keep its appearance until the second time.
After debug a while, i found that right after the change in settings (lightmode -> darkmode for exp), at line if traitCollection.userInterfaceStyle == .dark {, it wasn't recognized current userInterfaceStyle at first (unlike system's background color).
My application's target is lower than iOS 11 so i also have some prolem using colorset.
The question is is there and posible way to update the UI for darkmode programmatically when enter foreground?
Thanks in advance.
The problem is solved. I used custom color and UI was automatically update:
How do I easily support light and dark mode with a custom color used in my app?
It should look like this:
extension UIColor {
static var dropShadowViewBackground: UIColor {
if #available(iOS 13.0, *) {
return UIColor { (traits) -> UIColor in
// Return one of two colors depending on light or dark mode
return traits.userInterfaceStyle == .dark ? .black : .white
}
} else {
// Same old color used for iOS 12 and earlier
return .white
}
}
}
This work perfectly!
Related
This question already has answers here:
Detecting iOS Dark Mode Change
(6 answers)
Closed last month.
I was creating the method in UIView extension and I needed to change UIColor according to UIUserInterfaceStyle i.e. separate UIColor for both Dark & Light mode Interface.
Usually, in UIViewController class traitCollectionDidChange method
is triggered whenever UIUserInterfaceStyle is changed and we can
determine the current user interface style by
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.changeUIWithUserInterface(style: self.traitCollection.userInterfaceStyle)
}
}
But the Extension of UIView does not have traitCollectionDidChange method that can be triggered
so how can I change the UIColor according to UIUserInterfaceStyle in UIView extension?
I figured it out and thought to post it for fellow devs.
Hope It Helps :)
You can set UIColor as simple variable, that automatically changes when traitCollection changes:
struct ColorPalette {
public static var subtitleColor: UIColor = {
if #available(iOS 13, *) {
return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in
if UITraitCollection.userInterfaceStyle == .dark {
return UIColor.lightText
} else {
return .darkGray
}
}
} else {
/// Return a fallback color for iOS 12 and lower.
return .darkGray
}
}()
}
Usage:
label.textColor = ColorPalette.subtitleColor
Below method works like a charm for me!
It is triggered whenever traitCollection changes.. It can work on any extension of various UI components
UIColor.init { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? darkModeColor : lightModeColor
}
You have to check for iOS 13+, if you are working with support of iOS versions below 13, which you can check easily
if #available(iOS 13.0, *) {
//Dark mode is supported
self.backgroundColor = UIColor.init { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? darkModeColor : lightModeColor
}
} else {
//Earlier version of iOS, which does not suppport dark mode.
self.backgroundColor = lightModeColor
}
Reference: Thanks to answer of #ElanoVasconcelos
I tested against iOS 14.5
We would like to provide ability to change the navigation bar color during runtime.
However, I notice the following code no longer has any effect.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
navigationController?.navigationBar.backgroundColor = UIColor.red
navigationController?.navigationBar.tintColor = UIColor.red
UINavigationBar.appearance().barTintColor = UIColor.red
}
}
But, if I did it directly in Storyboard, it works just fine.
We would like to have ability to change the various different color during runtime (via user button clicked)
Does anyone has any idea why the above code broken?
Thanks.
p/s I can confirm the navigationController is not nil.
On iOS 13 & above, you have to use new UINavigationBarAppearance api to get the correct color.
public extension UINavigationBar {
func applyPlainAppearanceFix(barTintColor: UIColor, tintColor: UIColor) {
if #available(iOS 13, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = barTintColor
self.standardAppearance = appearance
self.compactAppearance = appearance
self.scrollEdgeAppearance = appearance
}
self.isTranslucent = false
self.barTintColor = barTintColor
self.backgroundColor = barTintColor
self.tintColor = tintColor
}
}
From the call site it should look like
navigationController?.navigationBar.applyPlainAppearanceFix(barTintColor: .red, tintColor: .white)
Could you try adding this in your AppDelegate and I think it will work, if you didn't want it for all the app then your code is working fine, I've used it and it's working like in the image below:
Im using this pod for add dark mode to my app.
https://github.com/draveness/NightNight
Its working well when I restart app again but I want to change theme inside of app. So, I added UISwitch to my sidepanel for user can change theme.
I added this codes for it and some colors changing well but some colors does not affect. For example NavigationBar background color is changing well but title color is not changing.
UISwitch Action:
#IBAction func switchMode(_ sender: UISwitch) {
if sender.isOn {
switcher.isOn = true
NightNight.theme = NightNight.Theme.night
UITabBar.appearance().barTintColor = UIColor(hexString: "#141d27")
UITabBar.appearance().isTranslucent = true
UITabBar.appearance().tintColor = UIColor(hexString: "#6e00ff")
UINavigationBar.appearance().tintColor = UIColor(hexString: "#6e00ff")
UINavigationBar.appearance().isTranslucent = true
UINavigationBar.appearance().barTintColor = UIColor(hexString: "#141d27")
for window in UIApplication.shared.windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}
UserDefaults.standard.set("night", forKey: "colormode")
} else {
switcher.isOn = false
NightNight.theme = NightNight.Theme.normal
UITabBar.appearance().barTintColor = UIColor.white
UITabBar.appearance().isTranslucent = true
UITabBar.appearance().tintColor = UIColor(hexString: "#6e00ff")
UINavigationBar.appearance().tintColor = UIColor(hexString: "#6e00ff")
UINavigationBar.appearance().isTranslucent = true
UINavigationBar.appearance().barTintColor = UIColor.white
for window in UIApplication.shared.windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}
UserDefaults.standard.set("normal", forKey: "colormode")
}
}
Normally gray text colors(User`s names and navigation title) must to be black in light mode, but they does not changed.
Use callback when view mode changes.
Change your colors as expected.
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// do whatever you want to do
}
Check out RxSwift and RxCocoa. They can both be found here. They are Reactive Frameworks for Reactive Programming in Swift. You can create observables to change the color of views reactively, as needed.
I'm trying to set up one button that each time pressed would alternate between two colours (black and white).
Initially the app loads with a black background, with the first button tap it swaps to a white background, with the second tap it swaps back to a black background and so fourth.
I imagine it would be a simple if else statement, however I don't know how to create a variable that is the current UI background colour.
#IBAction func background_toggle(_ sender: UIButton) {
self.view.backgroundColor = UIColor.white
}
The button swaps it to white, but there isn't any implementation to swap back to black when there is a second press.
You can use backgroundColor as a condition to check which one you should pick next.
Approach using a backgroundColor attribute
#IBAction func background_toggle(_ sender: UIButton) {
if self.view.backgroundColor == .white {
self.view.backgroundColor = .black
} else {
self.view.backgroundColor = .white
}
}
In other words: If backgroundColor is .white, then set it .black
Otherwise, set it .white
Tips:
Since backgroundColor is an UIColor, Swift infers the type, so you can just type .white or .black when assigning it, without having to specify UIColor.white or UIColor.black.
You can have a state variable like a Bool, UIColor or Enum. Then you compare to that instead of the backgroundColor of the view.
Approach using a Bool variable
var isWhite = true
#objc func handleOpenTermsAndConditions() {
if self.isWhite {
self.isWhite = false
self.view.backgroundColor = .black
} else {
self.isWhite = true
self.view.backgroundColor = .white
}
}
Approach using an UIColor variable
var currentBackgroundColor : UIColor = .white
#objc func handleOpenTermsAndConditions() {
if currentBackgroundColor == .white {
self.currentBackgroundColor = .black
self.view.backgroundColor = .black
} else {
self.currentBackgroundColor = .white
self.view.backgroundColor = .white
}
}
Approach using an Enum state
If you want to restrict colors you can create an Enum with available colors. I think it's overkill to use it with some logic that is as simple as .white | .black.
Step 1: Create an enum.
enum AvailableColor {
case white
case black
func currentUIColor() -> UIColor {
switch self {
case .white:
return UIColor.white
case .black:
return UIColor.black
}
}
}
Step 2: Instantiate a state variable and use the enum logic
var currentColorState : AvailableColor = .white
#objc func handleOpenTermsAndConditions() {
if currentColorState == .white {
currentColorState = .black
} else {
currentColorState = .white
}
self.view.backgroundColor = currentColorState.currentUIColor()
}
Don't try to use the contents of the view to determine the next state by comparing the colors.
Separate out your model from your view and keep a separate property that has the current state. You could use a simple boolean flag isBlack, or you could create an enumeration:
enum BackgroundState {
case black, white
}
which has the slight advantage of being easier to expand if you ever want another color/state. (CaseIterable might be helpful then as well.)
I am unable to change the prompt color on my navigation bar. I've tried the code below in viewDidLoad, but nothing happens.
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
Am I missing something? Is the code above wrong?
I was able to make the prompt color white on iOS 11 was setting the barStyle to black. I set the other color attributes (like the desired background color) using the appearance proxy:
myNavbar.barStyle = UIBarStyleBlack; // Objective-C
myNavbar.barStyle = .black // Swift
It seems like you're right about this one. You need to use UIAppearance to style the prompt text on iOS 11.
I've filed radar #34758558 that the titleTextAttributes property just stopped working for prompt in iOS 11.
The good news is that there are a couple of workarounds, which we can uncover by using Xcode's view hierarchy debugger:
// 1. This works, but potentially changes *all* labels in the navigation bar.
// If you want this, it works.
UILabel.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).textColor = UIColor.white
The prompt is just a UILabel. If we use UIAppearance's whenContainedInInstancesOf:, we can pretty easily update the color the way we want.
If you look closely, you'll notice that there's also a wrapper view on the UILabel. It has its own class that might respond to UIAppearance...
// 2. This is a more precise workaround but it requires using a private class.
if let promptClass = NSClassFromString("_UINavigationBarModernPromptView") as? UIAppearanceContainer.Type
{
UILabel.appearance(whenContainedInInstancesOf: [promptClass]).textColor = UIColor.white
}
I'd advise sticking to the more general solution, since it doesn't use private API. (App review, etc.) Check out what you get with either of these two solutions:
You may use
for view in self.navigationController?.navigationBar.subviews ?? [] {
let subviews = view.subviews
if subviews.count > 0, let label = subviews[0] as? UILabel {
label.textColor = UIColor.white
label.backgroundColor = UIColor.red
}
}
It will be a temporary workaround until they'll fix it
More complicated version to support old and new iOS
func updatePromptUI(for state: State) {
if (state != .Online) {
//workaround for SOFT-7019 (iOS 11 bug - Offline message has transparent background)
if #available(iOS 11.0, *) {
showPromptView()
} else {
showOldPromptView()
}
}
else {
self.navigationItem.prompt = nil
if #available(iOS 11.0, *) {
self.removePromptView()
} else {
self.navigationController?.navigationBar.titleTextAttributes = nil
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor:UIColor.lightGray]
}
}
}
private func showOldPromptView() {
self.navigationItem.prompt = "Network Offline. Some actions may be unavailable."
let navbarFont = UIFont.systemFont(ofSize: 16)
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.font: navbarFont, NSAttributedStringKey.foregroundColor:UIColor.white]
}
private func showPromptView() {
self.navigationItem.prompt = String()
self.removePromptView()
let promptView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 18))
promptView.backgroundColor = .red
let promptLabel = UILabel(frame: CGRect(x: 0, y: 2, width: promptView.frame.width, height: 14))
promptLabel.text = "Network Offline. Some actions may be unavailable."
promptLabel.textColor = .white
promptLabel.textAlignment = .center
promptLabel.font = promptLabel.font.withSize(13)
promptView.addSubview(promptLabel)
self.navigationController?.navigationBar.addSubview(promptView)
}
private func removePromptView() {
for view in self.navigationController?.navigationBar.subviews ?? [] {
view.removeFromSuperview()
}
}
I suggest using a custom UINavigationBar subclass and overriding layoutSubviews:
- (void)layoutSubviews {
[super layoutSubviews];
if (self.topItem.prompt) {
UILabel *promptLabel = [[self recursiveSubviewsOfKind:UILabel.class] selectFirstObjectUsingBlock:^BOOL(UILabel *label) {
return [label.text isEqualToString:self.topItem.prompt];
}];
promptLabel.textColor = self.tintColor;
}
}
Basically I'm enumerating all UILabels in the subview hierarchy and check if their text matches the prompt text. Then we set the textColor to the tintColor (feel free to use a custom color). That way, we don't have to hardcode the private _UINavigationBarModernPromptView class as the prompt label's superview. So the code is be a bit more future-proof.
Converting the code to Swift and implementing the helper methods recursiveSubviewsOfKind: and selectFirstObjectUsingBlock: are left as an exercise to the reader 😉.
You can try this:
import UIKit
class ViewController: UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updatePrompt()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updatePrompt()
}
func updatePrompt() {
navigationItem.prompt = " "
for view in navigationController?.navigationBar.subviews ?? [] where NSStringFromClass(view.classForCoder) == "_UINavigationBarModernPromptView" {
if let prompt = view.subviews.first as? UILabel {
prompt.text = "Hello Red Prompt"
prompt.textColor = .red
}
}
navigationItem.title = "This is the title (Another color)"
}
}
Moshe's first answer didn't work for me because it changed the labels inside of system VCs like mail and text compose VCs. I could change the background of those nav bars but that opens up a whole other can of worms. I didn't want to go the private class route so I only changed UILabels contained inside of my custom navigation bar subclass.
UILabel.appearance(whenContainedInInstancesOf: [NavigationBar.self]).textColor = UIColor.white
Try this out:->
navController.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue: UIColor.red]
I've found next work around for iOS 11.
You need set at viewDidLoad
navigationItem.prompt = UINavigationController.fakeUniqueText
and after that put next thing
navigationController?.promptLabel(completion: { label in
label?.textColor = .white
label?.font = Font.regularFont(size: .p12)
})
extension UINavigationController {
public static let fakeUniqueText = "\n\n\n\n\n"
func promptLabel(completion: #escaping (UILabel?) -> Void) {
gloabalThread(after: 0.5) { [weak self] in
guard let `self` = self else {
return
}
let label = self.findPromptLabel(at: self.navigationBar)
mainThread {
completion(label)
}
}
}
func findPromptLabel(at view: UIView) -> UILabel? {
if let label = view as? UILabel {
if label.text == UINavigationController.fakeUniqueText {
return label
}
}
var label: UILabel?
view.subviews.forEach { subview in
if let promptLabel = findPromptLabel(at: subview) {
label = promptLabel
}
}
return label
}
}
public func mainThread(_ completion: #escaping SimpleCompletion) {
DispatchQueue.main.async(execute: completion)
}
public func gloabalThread(after: Double, completion: #escaping SimpleCompletion) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) {
completion()
}
}