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()
}
}
Related
I have a view controller where I need to display multiline title on the navigation bar. For this, I have written a protocol like this -
import UIKit
protocol CustomNavigationBar {
func setupNavigationMultilineTitle(titleText: String, prefersLargeTitles: Bool, largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode)
}
And then extended it -
extension CustomNavigationBar where Self : UIViewController {
func setupNavigationMultilineTitle(titleText: String, prefersLargeTitles: Bool = true, largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode = .automatic ) {
self.navigationController?.navigationBar.prefersLargeTitles = prefersLargeTitles
self.navigationController?.navigationItem.largeTitleDisplayMode = largeTitleDisplayMode
self.navigationController?.navigationBar.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: UIColor.black,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .semibold)
]
self.title = titleText
if let navBarSubViews = self.navigationController?.navigationBar.subviews {
for navItem in navBarSubViews {
for itemSubView in navItem.subviews {
if let largeLabel = itemSubView as? UILabel {
largeLabel.text = self.title
largeLabel.numberOfLines = 0
largeLabel.lineBreakMode = .byWordWrapping
largeLabel.sizeToFit()
}
}
}
}
}
}
In my view controller, I conform to this protocol and inside viewDidAppear method, I call setupNavigationMultilineTitle method as follows -
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.setupNavigationMultilineTitle(titleText: "This is created for testing This is created for testing This is created for testing This is created for testing This is created for testing")
}
**
This works good on an iPhone running lesser than iOS13.
**
**
However, on an iPhone running greater than iOS 13, it just displays
one line and then truncates.
**
Has there been any changes in the UINavigationBar in iOS13? I researched and found something about background color but nothing related to multi line title using prefersLargeTitles and largeTitleDisplayMode.
Can someone please help me getting this up on iOS13?
Thanks!!
I'd like to update the UIKeyboardAppearance within a ViewController. By this I mean let's say the VC loads with UIKeyboardAppearance.default. If I press a button, I'd like the keyboard to update to .dark and have the keyboard now show in that same VC as .dark.
As far as I can tell, iOS checks the value for UIKeyboardAppearance while loading the VC, and doesn't check again until it loads the VC again. Even if you change the value of UIKeyboardAppearance and hide/show the keyboard.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// creating a simple text box, and making the placeholder text the value of the keyboardAppearance
myTextBox.backgroundColor = UIColor.lightGray
myTextBox.frame = CGRect(x: 30, y: 200, width: 300, height: 50)
view.addSubview(myTextBox)
UITextField.appearance().keyboardAppearance = .dark
myTextBox.becomeFirstResponder()
myTextBox.placeholder = "Keybaoard Appearance is: \(UITextField.appearance().keyboardAppearance.rawValue)"
// a simple button to toggle the keyboardAppearance
toggleButton.frame = CGRect(x: 30, y: 300, width: 300, height: 50)
toggleButton.setTitle("Toggle Keyboard", for: .normal)
toggleButton.backgroundColor = UIColor.red
toggleButton.addTarget(self, action: #selector(toggleButtonFunction), for: .touchUpInside)
view.addSubview(toggleButton)
}
// toggles the keyboardAppearance. Hides the keyboard, and a second later shows it again.
#objc func toggleButtonFunction() {
if UITextField.appearance().keyboardAppearance == .dark {
UITextField.appearance().keyboardAppearance = .default
}
else {
UITextField.appearance().keyboardAppearance = .dark
}
myTextBox.resignFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
self.myTextBox.becomeFirstResponder()
self.myTextBox.placeholder = "Keybaoard Appearance is: \(UITextField.appearance().keyboardAppearance.rawValue)"
})
}
let myTextBox = UITextField()
let toggleButton = UIButton()
}
I was hoping that after changing the UIKeyboardAppearance and hiding/showing the keyboard it would show with the new appearance (.dark or .default), but it continually shows with the same appearance until the VC is loaded again. You can see the value of UIKeyboardAppearance changes, but iOS seems to not check for that update until the VC loads again.
Is there any way to force a recheck within a VC?
Thanks for your help!
You can change the keyboard appearance of all text fields recursively on your screen (the allSubviewsOf(type:) extension is from this great answer by Mohammad Sadiq):
func changeTextFieldKeyboardAppearance() {
UITextField.appearance().keyboardAppearance = .dark
let textFields = view.allSubviewsOf(type: UITextField.self)
let firstResponder = textFields.first { $0.isFirstResponder }
firstResponder?.resignFirstResponder()
textFields.forEach { $0.keyboardAppearance = .dark }
firstResponder?.becomeFirstResponder()
}
[...]
extension UIView {
func allSubviewsOf<T: UIView>(type: T.Type) -> [T] {
var all = [T]()
func getSubview(view: UIView) {
if let aView = view as? T {
all.append(aView)
}
guard !view.subviews.isEmpty else {
return
}
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}
If your view controller is embedded in a UITabBarController, you can trigger an update by changing its selectedIndex and changing it back to the original index immediately:
guard let tabBarController = tabBarController else {
return
}
let selectedIndex = tabBarController.selectedIndex
UITextField.appearance().keyboardAppearance = .dark
tabBarController.selectedIndex = selectedIndex == 1 ? 0 : 1
tabBarController.selectedIndex = selectedIndex
Thanks to Tamás for the answer!
It led me down the path to discover what I needed.
It looks like if you change the keyboardAppearance for UITextField
UITextField.appearance().keyboardAppearance = .dark
the system only checks on VC load. If you change it for each textField
myTextBox.keyboardAppearance = .dark
the system will check each time firstResponder changes and load the correct keyboard.
Thanks again Tamás!
I am experiencing an annoying problem testing the newest iOS 11 on the iPhone X simulator.
I have an UITabBarController and inside each tab there is a UINavigationController, each UINavigationBar has defined also a bottom toolBar (setToolbarHidden:), and by default they show up at the bottom, just over the tabBar.
It has been working fine so far and seems to work fine also in the upcomming iPhone 8 and 8 Plus models, but on the iPhone X there is a gap between the toolBar and the tabBar. My guess is that the toolBar doesn't realize that is displayed inside a tabBar and then leaves the accommodating space at the bottom.
I guess the only way to fix it would be using a custom toolbar and display/animate it myself instead of using the defaults UINavigationBar, but I would like to hear other options :)
This is how it looks on iPhone 8.
And here is the problem on iPhone X.
I filed this as radr://problem/34421298, which was closed as a duplicate of radr://problem/34462371. However, in the latest beta of Xcode 9.2 (9C32c) with iOS 11.2, this seems to be fixed. Here's an example of my app running in the simulator of each device, with no changes in between.
This isn't really a solution to your problem, other than that some patience may solve it without needing to resort to UI trickery. My assumption is that iOS 11.2 will be out before the end of the year, since it's needed to support HomePod.
If you don't consider rotations you can try to manipulate the layer of the toolbar as a very hacky yet fast workaround.
class FixNavigationController: UINavigationController
{
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateTollbarPosition()
}
func updateTollbarPosition() {
guard let tabbarFrame = tabBarController?.tabBar.frame else {
return
}
let gapHeight = tabbarFrame.origin.y-toolbar.frame.origin.y-toolbar.frame.size.height
var
frame = toolbar.layer.frame
frame.origin.y += gapHeight
toolbar.layer.frame = frame
}
}
Unfortunately, rotation animation doesn't look good when it comes to this approach. In this case, adding the custom toolbar instead of the standard one will be a better solution.
I have found only one workaround: add toolbar directly to the view controller
iOS 11.1 and iPhone X are released and this bug/feature isn't fixed yet. So I implemented this workaround. This code works in iOS 9.0+.
Just set this class in your storyboard as navigation controller's class. It will use custom toolbar in iPhone X with correct layout constraints, and falls back to native one in other devices. The custom toolbar is added to navigation controller's view instead of your view controller, to make transitions smoother.
Important Note: You have to call updateItems(animated:) manually after setting toolbarItems of your view controller to update interface. If you set toolbarItems property of navigation controller, you can ignore this step.
It simulates all native toolbar behavior (including changing toolbar height in portrait/landscape modes), except push/pop animations.
import UIKit
class FixNavigationController: UINavigationController {
private weak var alterToolbarHeightConstraint: NSLayoutConstraint?
private var _alterToolbar: UIToolbar?
private func initAlretToolbar() {
_alterToolbar = UIToolbar()
_alterToolbar!.isTranslucent = true
_alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(_alterToolbar!)
if view.traitCollection.verticalSizeClass == .compact {
alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
} else {
alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
}
let bottomAnchor: NSLayoutConstraint
if #available(iOS 11.0, *) {
bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
} else {
bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
}
NSLayoutConstraint.activate([
_alterToolbar!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
_alterToolbar!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomAnchor,
alterToolbarHeightConstraint!
])
self.view.updateFocusIfNeeded()
self.view.layoutIfNeeded()
}
private var alterToolbarInSuper: UIToolbar? {
var superNavigationController = self.navigationController as? FixNavigationController
while superNavigationController != nil {
if superNavigationController?._alterToolbar != nil {
return superNavigationController?._alterToolbar
}
superNavigationController = superNavigationController?.navigationController as? FixNavigationController
}
return nil
}
private var alterToolbar: UIToolbar! {
get {
if let t = alterToolbarInSuper {
return t
}
if _alterToolbar == nil {
initAlretToolbar()
}
return _alterToolbar
}
}
// This is the logic to determine should use custom toolbar or fallback to native one
private var shouldUseAlterToolbar: Bool {
// return true if height is iPhone X's one
return UIScreen.main.nativeBounds.height == 2436
}
/// Manually call it after setting toolbar items in child view controllers
func updateItems(animated: Bool = false) {
if shouldUseAlterToolbar {
(_alterToolbar ?? alterToolbarInSuper)?.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: animated)
}
}
override var isToolbarHidden: Bool {
get {
if shouldUseAlterToolbar {
return _alterToolbar == nil && alterToolbarInSuper == nil
} else {
return super.isToolbarHidden
}
}
set {
if shouldUseAlterToolbar {
if newValue {
super.isToolbarHidden = newValue
_alterToolbar?.removeFromSuperview()
_alterToolbar = nil
self.view.updateFocusIfNeeded()
self.view.layoutIfNeeded()
// TODO: Animation when push/pop
alterToolbarHeightConstraint = nil
var superNavigationController = self.navigationController as? FixNavigationController
while let superNC = superNavigationController {
if superNC._alterToolbar != nil {
superNC._alterToolbar?.removeFromSuperview()
superNC._alterToolbar = nil
superNC.view.updateFocusIfNeeded()
superNC.view.layoutIfNeeded()
}
superNavigationController = superNC.navigationController as? FixNavigationController
}
} else {
alterToolbar.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: false)
}
} else {
super.isToolbarHidden = newValue
}
}
}
override func setToolbarItems(_ toolbarItems: [UIBarButtonItem]?, animated: Bool) {
super.setToolbarItems(toolbarItems, animated: animated)
updateItems(animated: animated)
}
override var toolbarItems: [UIBarButtonItem]? {
get {
return super.toolbarItems
}
set {
super.toolbarItems = newValue
updateItems()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
guard let _alterToolbar = _alterToolbar else {
return
}
self.alterToolbarHeightConstraint?.isActive = false
let height: CGFloat = (view.traitCollection.verticalSizeClass == .compact) ? 32.0 : 44.0
let alterToolbarHeightConstraint = _alterToolbar.heightAnchor.constraint(equalToConstant: height)
alterToolbarHeightConstraint.isActive = true
self.alterToolbarHeightConstraint = alterToolbarHeightConstraint
}
}
Apple still has not yet fixed this bug in iOS 11.2. Derived from Mousavian's solution, here is a simpler approach that I took.
I took this approach because I have just one UITableViewController where this bug happens. So in my case, I just added the following code listed below to my ViewController (which is UITableViewController) where this bug happens.
Advantages are:
This fix just takes over in case of an iPhone X. No side effects to expect on other devices
Works with any transition
Works regardless of other parent/child controllers having Toolbars or not
Simple
And here is the code:
1.Add startFixIPhoneXToolbarBug to your viewWillAppear like this:
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
startFixIPhoneXToolbarBug()
}
2.Add endFixIPhoneXToolbarBug to your viewWillDisappear like this:
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
endFixIPhoneXToolbarBug()
}
3.Implement start/endFixIPhoneXToolbarBug in your viewController like this:
private var alterToolbarHeightConstraint: NSLayoutConstraint? = nil
private var alterToolbar: UIToolbar? = nil
func startFixIPhoneXToolbarBug()
{
// Check if we are running on an iPhone X
if UIScreen.main.nativeBounds.height != 2436
{
return // No
}
// See if we have a Toolbar
if let tb:UIToolbar = self.navigationController?.toolbar
{
// See if we already added our own
if alterToolbar == nil
{
// Should always be the case
if let tbView = tb.superview
{
// Create a new Toolbar and apply correct constraints
alterToolbar = UIToolbar()
alterToolbar!.isTranslucent = true
alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
tb.isHidden = true
tbView.addSubview(alterToolbar!)
if tbView.traitCollection.verticalSizeClass == .compact
{
alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
}
else
{
alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
}
let bottomAnchor: NSLayoutConstraint
if #available(iOS 11.0, *)
{
bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: tbView.safeAreaLayoutGuide.bottomAnchor)
}
else
{
bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
}
NSLayoutConstraint.activate([
alterToolbar!.leadingAnchor.constraint(equalTo: tbView.leadingAnchor),
alterToolbar!.trailingAnchor.constraint(equalTo: tbView.trailingAnchor),
bottomAnchor,
alterToolbarHeightConstraint!
])
tbView.updateFocusIfNeeded()
tbView.layoutIfNeeded()
}
}
// Add the original items to the new toolbox
alterToolbar!.setItems(tb.items, animated: false)
}
}
func endFixIPhoneXToolbarBug()
{
if alterToolbar != nil
{
alterToolbar!.removeFromSuperview()
alterToolbar = nil
alterToolbarHeightConstraint = nil
if let tb:UIToolbar = self.navigationController?.toolbar
{
tb.isHidden = false
}
}
}
I'm using UIAlertController for some actions.
But I'm not a big fan of the Blurry View Effect in the actions group view (see screenshot below).
I'm trying to remove this blurry effect. I made some research online, and I couldn't find any API in UIAlertController that allows to remove this blurry effect. Also, according to their apple doc here :
The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.
I see that Instagram also removes this blurry view effect :
The only way I could find to remove it is to update the view hierarchy myself via an extension of UIAlertController.
extension UIAlertController {
#discardableResult private func findAndRemoveBlurEffect(currentView: UIView) -> Bool {
for childView in currentView.subviews {
if childView is UIVisualEffectView {
childView.removeFromSuperview()
return true
} else if String(describing: type(of: childView.self)) == "_UIInterfaceActionGroupHeaderScrollView" {
// One background view is broken, we need to make sure it's white.
if let brokenBackgroundView = childView.superview {
// Set broken brackground view to a darker white
brokenBackgroundView.backgroundColor = UIColor.colorRGB(red: 235, green: 235, blue: 235, alpha: 1)
}
}
findAndRemoveBlurEffect(currentView: childView)
}
return false
}
}
let actionSheetController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
actionSheetController.view.tintColor = .lightBlue
actionSheetController.removeBlurryView()
This worked fine, it removed my blurry view effect:
What I'm wondering... Is my solution the only way to accomplish that? Or there is something that I'm missing about the Alert Controller appearance?
Maybe there is a cleaner way to accomplish exactly that result? Any other ideas?
It is easier to subclass UIAlertController.
The idea is to traverse through view hierarchy each time viewDidLayoutSubviews gets called, remove effect for UIVisualEffectView's and update their backgroundColor:
class AlertController: UIAlertController {
/// Buttons background color.
var buttonBackgroundColor: UIColor = .darkGray {
didSet {
// Invalidate current colors on change.
view.setNeedsLayout()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Traverse view hierarchy.
view.allViews.forEach {
// If there was any non-clear background color, update to custom background.
if let color = $0.backgroundColor, color != .clear {
$0.backgroundColor = buttonBackgroundColor
}
// If view is UIVisualEffectView, remove it's effect and customise color.
if let visualEffectView = $0 as? UIVisualEffectView {
visualEffectView.effect = nil
visualEffectView.backgroundColor = buttonBackgroundColor
}
}
// Update background color of popoverPresentationController (for iPads).
popoverPresentationController?.backgroundColor = buttonBackgroundColor
}
}
extension UIView {
/// All child subviews in view hierarchy plus self.
fileprivate var allViews: [UIView] {
var views = [self]
subviews.forEach {
views.append(contentsOf: $0.allViews)
}
return views
}
}
Usage:
Create alert controller.
Set buttons background color:
alertController.buttonBackgroundColor = .darkGray
Customise and present controller.
Result:
Answer by Vadim works really well.
What I missed in it (testing on iOS 14.5) is lack of separators and invisible title and message values.
So I added setting correct textColor for labels and skipping separator visual effect views in order to get correct appearance. Also remember to override traitCollectionDidChange method if your app supports dark mode to update controls backgroundColor accordingly
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subview in view.allViews {
if let label = subview as? UILabel, label.textColor == .white {
label.textColor = .secondaryLabel
}
if let color = subview.backgroundColor, color != .clear {
subview.backgroundColor = buttonBackgroundColor
}
if let visualEffectView = subview as? UIVisualEffectView,
String(describing: subview).contains("Separator") == false {
visualEffectView.effect = nil
visualEffectView.contentView.backgroundColor = buttonBackgroundColor
}
}
popoverPresentationController?.backgroundColor = buttonBackgroundColor
}
I have been search a while for this issue , I want my search bar display like BBC News App
I try all related method
for view in searchBar.subviews {
if view.isKindOfClass(NSClassFromString("UISearchBarBackground")!) {
view.removeFromSuperview()
break;
}
}
self.searchBar.tintColor = UIColor.clearColor()
self.searchBar.backgroundColor = UIColor.clearColor()
self.searchBar.translucent = true
here is my output
Am I miss something ??? Please Help me , thx !
Swift 3
To remove the background altogether, set backgroundImage to an empty image:
searchBar.backgroundImage = UIImage()
To set a custom background color, use barTintcolor property:
searchBar.barTintColor = .green
Thx all , I solve the question by setting the background image to 'nil' , which is a nonexistent image in my app
my final output
==================== Update Final Solution ====================
After read more documents . Finally I found a better solution ,
for subView in searchBar.subviews {
for view in subView.subviews {
if view.isKindOfClass(NSClassFromString("UINavigationButton")!) {
let cancelButton = view as! UIButton
cancelButton.setTitle("取消", forState: UIControlState.Normal)
cancelButton.setTitleColor(UIColor.whiteColor(), forState: .Normal)
}
if view.isKindOfClass(NSClassFromString("UISearchBarBackground")!) {
let imageView = view as! UIImageView
imageView.removeFromSuperview()
}
}
}
==================== Update Swift4 ====================
for subView in searchBar.subviews {
for view in subView.subviews {
if view.isKind(of: NSClassFromString("UINavigationButton")!) {
let cancelButton = view as! UIButton
cancelButton.setTitleColor(.white, for: .normal)
cancelButton.setTitle("取消", for: .normal)
}
if view.isKind(of: NSClassFromString("UISearchBarBackground")!) {
let imageView = view as! UIImageView
imageView.removeFromSuperview()
}
}
}
Alternate version as an extension
extension UISearchBar {
func removeBackgroundImageView(){
if let view:UIView = self.subviews.first {
for curr in view.subviews {
guard let searchBarBackgroundClass = NSClassFromString("UISearchBarBackground") else {
return
}
if curr.isKind(of:searchBarBackgroundClass){
if let imageView = curr as? UIImageView{
imageView.removeFromSuperview()
break
}
}
}
}
}
}
In my case it helped:
searchView.backgroundImage = UIImage()
searchView.searchTextField.backgroundColor = .white
The current answers will cause runtime errors if run within iOS 13:
Terminating app due to uncaught exception 'NSGenericException', reason:
'Missing or detached view for search bar layout. The application must not remove
<UISearchBarBackground: 0x102d05050; frame = (0 0; 414 56); alpha = 0; hidden = YES;
userInteractionEnabled = NO; layer = <CALayer: 0x280287420>> from the hierarchy.'
If the code must be run by devices between iOS 9 and iOS 13, then the below is a possible solution.
First, create an extension that allows for the recursive finding of a subview based on a class name:
extension UIView {
/// Find the first subview of the specified class.
/// - Parameter className: The class name to search for.
/// - Parameter usingRecursion: True if the search should continue through the subview tree until a match is found; false otherwise
/// - Returns: The first child UIView of the specified class
func findSubview(withClassName className: String, usingRecursion: Bool) -> UIView? {
// If we can convert the class name until a class, we look for a match in the subviews of our current view
if let reflectedClass = NSClassFromString(className) {
for subview in self.subviews {
if subview.isKind(of: reflectedClass) {
return subview
}
}
}
// If recursion was specified, we'll continue into all subviews until a view is found
if usingRecursion {
for subview in self.subviews {
if let tempView = subview.findSubview(withClassName: className, usingRecursion: usingRecursion) {
return tempView
}
}
}
// If we haven't returned yet, there was no match
return nil
}
}
Then, instead of removing the subview, make it fully transparent. The backgroundColorView view is the color that shows up directly underneath the text, but adjusting it is not a necessary part of the solution.
// On iOS 9, there is still an image behind the search bar. We want to remove it.
if let backgroundView = searchBar.findSubview(withClassName: "UISearchBarBackground", usingRecursion: true) {
backgroundView.alpha = 0
}
// The color on iOS 9 is white. This mimics the newer appearance of the post-iOS 9
// search controllers
if let backgroundColorView = searchBar.findSubview(withClassName: "_UISearchBarSearchFieldBackgroundView", usingRecursion: true) as? UIImageView {
backgroundColorView.backgroundColor = UIColor.lightGray
backgroundColorView.layer.cornerRadius = 8
backgroundColorView.alpha = 0.3
backgroundColorView.image = nil
}