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
}
Related
I want to add a banner to the navigation bar, but by increasing the height of it. I want to copy the design and behaviour of an artist page in the Apple Music app:
It behaves just like a normal Large Title would, except for that it has been moved down, it has a sticky UIImageView behind it and it returns its background when the user scrolls down far enough. You can fire up Apple Music, search for an artist and go to their page to try it out yourself.
I've tried a bunch of things like setting the frame on the UINavigationBarLargeTitleView, and the code from this answer: https://stackoverflow.com/a/49326161/5544222
I already got a hold of the UINavigationBarLargeTitleView and its UILabel using the following code:
func setLargeTitleHeight() {
if let largeTitleView = self.getLargeTitleView() {
if let largeTitleLabel = self.getLargeTitleLabel(largeTitleView: largeTitleView) {
// Set largeTitleView height.
}
}
}
func getLargeTitleView() -> UIView? {
for subview in self.navigationBar.subviews {
if NSStringFromClass(subview.classForCoder).contains("UINavigationBarLargeTitleView") {
return subview
}
}
return nil
}
func getLargeTitleLabel(largeTitleView: UIView) -> UILabel? {
for subview in largeTitleView.subviews {
if subview.isMember(of: UILabel.self) {
return (subview as! UILabel)
}
}
return nil
}
Initially put a view with image and label and play button. Then clear the navigation bar it will show the image below it by
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
self.navigationController!.navigationBar.isTranslucent = true
later when you scroll up you have to handle it manually and again show the
navigation with title.
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()
}
}
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
}
I'm trying to implement 6 lines high description label and I want it to be focusable. Ideally that would mean extending UILabel class to make a custom component. I tried that by implementing canBecomeFocused and didUpdateFocusInContext but my UILabel doesn't seem to get any focus.
I also tried replacing UILabel with UIButton, but buttons aren't really optimised for this sort of thing. Also that would mean I'd need to change buttonType on focus from custom to plain.. and buttonType seems to be a ready-only property.
In reality I'd like to have exact same text label implemented by Apple in Apple TV Movies app. For movie description they have a text label that displays a few lines of text and a "more". When focused it looks like a button (shadows around) and changed background color. When tapped - it opens up a modal window with entire movie description.
Any suggestions? Or maybe someone has already implemented this custom control for tvOS? Or event better - there is a component from Apple that does this and I'm missing something.
P.S: Swift solution would be welcome :)
Ok, answering my own question :)
So it appears that some some views are "focusable" on tvOS out-of-the-box, and other have to be instructed to do so.
I finally ended up using UITextView, which has a selectable property, but if not one of these focusable views by default. Editing of TextView has to be disabled to make it look like UILabel. Also, currently there is a bug which prevents you from using selectable property from Interface Builder but works from code.
Naturally, canBecomeFocused() and didUpdateFocusInContext had to be implemented too. You'll also need to pass a UIViewController because UITextView is not capable of presenting a modal view controller. Bellow is what I ended up creating.
class FocusableText: UITextView {
var data: String?
var parentView: UIViewController?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: "tapped:")
tap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.addGestureRecognizer(tap)
}
func tapped(gesture: UITapGestureRecognizer) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let descriptionView = storyboard.instantiateViewControllerWithIdentifier("descriptionView") as? DescriptionViewController {
if let view = parentView {
if let show = show {
descriptionView.descriptionText = self.data
view.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
view.presentViewController(descriptionView, animated: true, completion: nil)
}
}
}
}
override func canBecomeFocused() -> Bool {
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
} else if context.previouslyFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
}
}
As for making a UILabel focusable:
class MyLabel: UILabel {
override var canBecomeFocused: Bool {
return true
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator)
backgroundColor = context.nextFocusedView == self ? .blue:.red
}
}
IMPORTANT!!!
As stated on the apple developer portal:
The value of this property is true if the view can become focused; false otherwise.
By default, the value of this property is false. This property informs the focus engine if a view is capable of being focused. Sometimes even if a view returns true, a view may not be focusable for the following reasons:
The view is hidden.
The view has alpha set to 0.
The view has userInteractionEnabled set to false.
The view is not currently in the view hierarchy.
Use a collection view with just one cell and add transform to cell and change cell background color in didUpdateFocusInContext when focus moves to cell.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.focused {
self.transform = CGAffineTransformMakeScale(1.01, 1.01)
self.backgroundColor = UIColor.whiteColor()
self.textLabel.textColor = .blackColor()
}
else {
self.transform = CGAffineTransformMakeScale(1, 1)
self.backgroundColor = UIColor.clearColor()
self.textLabel.textColor = .whiteColor()
}
}, completion: nil)
}
As an additional step you could try to extract the color of the image if you are using the image as background like iTunes and use that for Visual effect view behind the cell.
Also you can apply transform to the collectionView in the video controller to make it look like in focus
You can use system button, and set the background image in storyboard to an image that contains the color you would like
I'm building a keyboard using Swift (XCode 6 with iOS 8.3 SDK) and when I load the xib, the keyboard is about 1000 pixels too wide and tall. I've, in the freeform xib file, put all my keys in a UIView and set the UIViews constraints to superview top, bottom, left and right.
As you can see, the result is annoying. Here's my code:
import UIKit
class KeyboardViewController: UIInputViewController {
var BlurBoardView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
loadInterface()
}
func loadInterface() {
//load the nib file
var blurboardNib = UINib(nibName: "BlurBoardView", bundle: nil)
// initiate the view
BlurBoardView = blurboardNib.instantiateWithOwner(self, options: nil)[0] as! UIView
// add the interface to the main view
view.addSubview(BlurBoardView)
// copy the background color
view.backgroundColor = BlurBoardView.backgroundColor
let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.Light))
blur.frame = view.frame
view.addSubview(blur)
}
#IBAction func didTapButton(sender: AnyObject?) {
let button = sender as! UIButton
let title = button.titleForState(.Normal)
var proxy = textDocumentProxy as! UITextDocumentProxy
switch title as String!{
case "<" :
proxy.deleteBackward()
case "RETURN" :
proxy.insertText("\n")
case " " :
proxy.insertText(" ")
case "CHG" :
self.advanceToNextInputMode()
default :
proxy.insertText(title!)
}
}
/*
override func textWillChange(textInput: UITextInput) {
// The app is about to change the document's contents. Perform any preparation here.
}
override func textDidChange(textInput: UITextInput) {
// The app has just changed the document's contents, the document context has been updated.
var textColor: UIColor
var proxy = self.textDocumentProxy as! UITextDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
textColor = UIColor.whiteColor()
} else {
textColor = UIColor.blackColor()
}
//self.nextKeyboardButton.setTitleColor(textColor, forState: .Normal)
}
*/
}
The keyboard, in it's precompiled xib looks like this:
You can also see the parent (of the keys) views constraints.
I'm mainly an Objective-C developer, so the code makes sense, but the issue might just be a huge SBE because of Swift, so sorry if it's ridiculously simple ;)
edit: The P key has a constraint to tie it 14 pixels to the left superview, and 0 pixels to the right (to the O key) which is how I know it's way too wide. Keyboard bottom constraint is to bottom of superview.
Look like you're using Autolayout and you're not setting constraints between your view and your BlurBoardView.
When ViewDidLoad is called, the view doesn't have its right frame. It's only when viewDidLayoutSubviews is called.
So set constraints between your view and your BlurBoardView in ViewDidLoad. Or put BlurBoardView.frame = the frame your wish in viewDidLayoutSubview.
Same thing for your blurView