I am trying to find a way to change the tint color of the backBarButtonItem based on the scroll position. In the example below, the button should be yellow by default, but then at a certain threshold it should change to red.
Although using breakpoints I can see the code triggering in each block, but unfortunately backBarButtonItem never changes to red and always remains yellow. Any suggestions on why this might be the case? I'm assuming that you might not be able to change the back button in the navigation bar once it's already set.
CGFloat totalHeight = CGRectGetMaxY(self.frame);
CGFloat barHeight = CGRectGetHeight(self.frame);
CGFloat offsetHeight = (self.scrollview.contentOffset.y - self.scrollViewMinimumOffset) + totalHeight;
offsetHeight = MAX(offsetHeight, 0.0f);
offsetHeight = MIN(offsetHeight, totalHeight);
if (offsetHeight > barHeight * 1.0f) {
[self.backBarButtonItem setTintColor:[UIColor redColor]];
} else {
[self.backBarButtonItem setTintColor:[UIColor yellowColor]];
}
Let me provide the following example that can help you figure out or gain some ideas to better address the issue.
So in the storyboard (can be done programmatically), I have the following scenario:
That backBarButtonItem is actually 1stVC button in the NavigationBar.
In order to change the color of backBarButtonItem, you may implement the following code (or take a look):
import UIKit
class ViewController2: UIViewController {
var counter = 0 //any conditions you want to play with
override func viewDidLoad() {
super.viewDidLoad()
var color: UIColor = UIColor.purple //or yellow, by default
if(counter == 0){
color = UIColor.red
}
self.navigationController?.navigationBar.tintColor = color
}
}
It is done in the viewDidLoad() method of ViewController2 so that it can get configured as soon as this ViewController is opened.
Here, I just used counter variable as a simple example to create some condition based on which the color of backBarButtonItem should be changed. In your case, you have another condition.
So this is the output:
I have tried EVERYTHING to get this to work. I setup a custom class like so.
override func layoutSubviews() {
super.layoutSubviews()
clearBackgroundColor() // function in the question
}
private func clearBackgroundColor() {
guard let UISearchBarBackground: AnyClass = NSClassFromString("UISearchBarBackground") else { return }
for view in self.subviews {
for subview in view.subviews {
if subview.isKind(of: UISearchBarBackground) {
subview.alpha = 0
}
}
}
}
I set backgroundColor, barTintColor to .clear. Style to minimal. Im losing my mind. I set breakpoints to make sure we are finding the search bar background. Ive tried subview.removeFromSuperview() as well. Nothing. I think Im going insane. Am I missing something?
This is on iOS 10 and am using storyboard. Any help would be greatly appreciated.
I had to do this in a client's app a while ago. Here's what worked for me:
I had a UISearchBar subclass:
#property (nonatomic, strong) UITextField* textField;
I called the following from init:
self.textField = [self findViewOfClass:[UITextField class] inView:self];
self.translucent = NO;
self.barTintColor = ...;
self.textField.backgroundColor = ...;
- (id)findViewOfClass:(Class)class inView:(UIView*)view
{
if ([view isKindOfClass:class])
{
return view;
}
else
{
for (UIView* subview in view.subviews)
{
id foundView = [self findViewOfClass:class inView:subview];
if (foundView != nil)
{
return foundView;
}
}
}
return nil;
}
The essential part is finding the UITextField. (I did a similar thing to allow me to custom style the cancel button.) I vaguely remember that disabling translucent was really needed; easy to try.
That should be it. Let me know if this works for you.
I only have Obj-C code, but this is easy to convert.
I finally ignored previous answers from all the posts about this subject and did my own Debug View Hierarchy. I spotted a ImageView that serves as the background which I guess is now called "_UISearchBarSearchFieldBackgroundView". This helped me find a single function that fixes the problem at least for iOS 9+.
searchBar.setSearchFieldBackgroundImage(UIImage(), for: .normal)
One thing to note is that this isn't the only way to fix this problem. However, I used it because it requires no looping and because the image is empty the additional view is never added giving the same end result as other methods.
One thing to note is that this may only work for iOS 9+. So, your milage may vary. I tested with iOS 10 with a Deployment Target of 9.3.
I've been looking at the same problem for so long I'm probably missing a simple solution here.
I created a small library to provide a custom UIView that sticks to the keyboard like the one for iMessage does (aka doesn't hide with keyboard): https://github.com/oseparovic/MessageComposerView
Basically the problem I'm experiencing is that when the user init's custom view I want a view with the following default rect initialized:
CGFloat defaultHeight = 44.0;
CGRect frame = CGRectMake(0,
[self currentScreenSize].height-defaultHeight,
[self currentScreenSize].width,
defaultHeight)
This requires that the currentScreenSize is calculated within the UIView. I've tried multiple implementations all of which have their downsides. There doesn't seems to be a good solution due to this breaking principles of MVC.
There are lots of duplicate questions on SO but most assume you have access to the rest of the code base (e.g. the app delegate) which this custom view does not so I'm looking for a self contained solution.
Here are the two leading implementations I'm using:
NextResponder
This solution seems to be fairly successful in a wide variety of scenarios. All it does is get the next responder's frame which very conveniently doesn't include the nav or status bar and can be used to position the UIView at the bottom of the screen.
The main problem is that self.nextResponder within the UIView is nil at the point of initialization, meaning it can't be used (at least not that I know) to set up the initial frame. Once the view has been initialized and added as a subview though this seems to work like a charm for various repositioning uses.
- (CGSize)currentScreenSize {
// return the screen size with respect to the orientation
return ((UIView*)self.nextResponder).frame.size;
}
ApplicationFrame
This was the solution I was using for a long time but it's far more bulky and has several problems. First of all, by using the applicationFrame you have to deal with the nav bar height as it will otherwise offset the position of your view. This means you have to determine if it is visible, get its height and subtract it from your currentSize.
Getting the nav bar unfortunately means you need to access the UINavigationController which is not nearly as simple as accessing the UIViewController. The best solution I've had so far is the below included currentNavigationBarHeight. I recently found an issue though where this will fail to get the nav bar height if a UIAlertView is present as [UIApplication sharedApplication].keyWindow.rootViewController will evaluate to _UIAlertShimPresentingViewController
- (CGSize)currentScreenSize {
// there are a few problems with this implementation. Namely nav bar height
// especially was unreliable. For example when UIAlertView height was present
// we couldn't properly determine the nav bar height. The above method appears to be
// working more consistently. If it doesn't work for you try this method below instead.
return [self currentScreenSizeInInterfaceOrientation:[self currentInterfaceOrientation]];
}
- (CGSize)currentScreenSizeInInterfaceOrientation:(UIInterfaceOrientation)orientation {
// http://stackoverflow.com/a/7905540/740474
// get the size of the application frame (screensize - status bar height)
CGSize size = [UIScreen mainScreen].applicationFrame.size;
// if the orientation at this point is landscape but it hasn't fully rotated yet use landscape size instead.
// handling differs between iOS 7 && 8 so need to check if size is properly configured or not. On
// iOS 7 height will still be greater than width in landscape without this call but on iOS 8
// it won't
if (UIInterfaceOrientationIsLandscape(orientation) && size.height > size.width) {
size = CGSizeMake(size.height, size.width);
}
// subtract the height of the navigation bar from the screen height
size.height -= [self currentNavigationBarHeight];
return size;
}
- (UIInterfaceOrientation)currentInterfaceOrientation {
// Returns the orientation of the Interface NOT the Device. The two do not happen in exact unison so
// this point is important.
return [UIApplication sharedApplication].statusBarOrientation;
}
- (CGFloat)currentNavigationBarHeight {
// TODO this will fail to get the correct height when a UIAlertView is present
id nav = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([nav isKindOfClass:[UINavigationController class]]) {
UINavigationController *navc = (UINavigationController *) nav;
if(navc.navigationBarHidden) {
return 0;
} else {
return navc.navigationBar.frame.size.height;
}
}
return 0;
}
Does anyone have suggestion about how I can best calculate the UIViewController size from within this UIView. I'm totally open to other suggestions on how to stick the UIView to the bottom of the screen upon initialization that I may have overlooked. Thank you!
+ (id) getCurrentUIViewController : (id)res {
if([res isKindOfClass:[UIViewController class]]) {
return res;
}
else if ([res isKindOfClass:[UIView class]]) {
return [Function getCurrentUIViewController:[res nextResponder]];
}
else {
return nil;
}
}
I am making an iOS 7 app, I know that Apple's new design guidelines call for a bunch of flat design minimalist stuff, but this app is not really targeted at the tech-savvy crowd, so apple's guidelines pose a problem. I have a regular button, with just text in it, and I would like to put an outline around it, and I would also like for the button to react to being pressed, so that people actually know it is a button, and so that they do not freak out when the button takes them to a different app? So how do I
Put an outline around a regular iOS button?
Make a regular iOS Button give some simple visual feedback to being pressed?
Simplest way: make the UIButton's type be "System", rather than "Custom". A system button's image and/or text will highlight when touched.
You should do this in Interface Builder, by changing button's "Type" to be "System"
However, if you need to do it programmatically, you can do:
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
As for the UIButton's border, you can do:
- (void)viewDidLoad {
[super viewDidLoad];
self.button.layer.cornerRadius = 5;
self.button.layer.borderColor = [UIColor blackColor];
self.button.layer.borderWidth = 1;
}
If you are using a storyboard (interface builder) for designing your app it's quite easy:
Create a subclass of UIButton. Let's call it XYBorderButton.
In XYBorderButton.m add the methods:
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self makeBorder];
}
return self;
}
- (void)makeBorder {
self.layer.cornerRadius = 10.0;
self.layer.borderColor = [UIColor blueColor];
self.layer.borderWidth = 1.0;
}
Then in interface builder select the button and change its class to XYBorderButton
You can give visual feedback for example by changing the button's background color and/or by changing its font color.
Setting these attributes is quite easy with Interface Builder:
Just select the button, then choose the state "Highlighted" in the state config dropdown menu and set the background color and font color as desired.
extension UIButton {
func provideVisualFeedback4press()
{
backgroundColor = cyan
alpha = 0
UIView .animate(withDuration: 0.1, animations: { [weak self] in
guard let s = self else {
return
}
s.alpha = 1
}, completion: { [weak self] completed in
if completed {
guard let s = self else {
return
}
s.backgroundColor = UIColor.white
}
})
}}
usage:
#objc func backAction(_ sender:UIButton!)
{
sender.provideVisualFeedback4press()
If you set the text or image properties of a UIButton, it'll automatically give feedback when pressed (the font color and image will go darker). However if you simply placed the button on top of some other controls, then you'll have to wire up to the Touch Down event and manually change the appearance to any control you want.
I have a bunch of buttons on the screen which are positioned intuitively visually but are not read in an intuitive order by VoiceOver. This is because certain buttons like Up and Down are placed above and below each other. However, voiceover starts reading from Left to Right, from Top to Bottom, it seems.
This results in voiceover reading the button to the right of "Up" after "Up", instead of reading "Down" immediately afterward.
How do I force voiceover to read the button that I want to read? I should mention that I'm using the swipe-to-cycle-through-elements feature on voiceover.
All my buttons are subclassed versions of UIView and UIButton. Here's an example of a button initiator I use. Ignore the pixel count - I know that's bad form but I'm in a pinch at the moment:
UIButton* createSpecialButton(CGRect frame,
NSString* imageName,
NSString* activeImageName,
id target,
SEL obClickHandler)
{
UIButton* b = [UIButton buttonWithType:UIButtonTypeCustom];
[b setImage:[GlobalHelper nonCachedImage:imageName ofType:#"png"]
forState:UIControlStateNormal];
[b setImage:[GlobalHelper nonCachedImage:activeImageName ofType:#"png"]
forState:UIControlStateHighlighted];
[b addTarget:target action:obClickHandler forControlEvents:UIControlEventTouchUpInside];
b.frame= frame;
return b;
}
- (UIButton *) createSendButton {
CGFloat yMarker = 295;
UIButton* b = createSpecialButton(CGRectMake(160, yMarker, 70, 45),
#"Share_Btn",
#"Share_Selected_Btn",
self,
#selector(sendAction));
b.accessibilityHint = #"Send it!";
b.accessibilityLabel = #"Stuff for voiceover to be added";
[self.view addSubview:b];
return b;
}
You can change the order by setting the view's accessibilityElements array:
self.view.accessibilityElements = #[self.view1, self.view2, self.view3, self.view4];
or
self.anotherView.accessibilityElements = #[self.label1, self.txtView1, self.label2, self.txtView2];
If you need to enable user interaction programmatically:
[self.view1 setUserInteractionEnabled:YES];
Note: If the view is hidden, VoiceOver will not pass through it.
The easiest answer to this lies in creating a UIView subclass that contains your buttons, and responds differently to the accessibility calls from the system. These important calls are:
-(NSInteger)accessibilityElementCount
-(id)accessibilityElementAtIndex:
-(NSInteger)indexOfAccessibilityElement:
I've seen a few of these questions, and answered one before, but I've not seen a generic example of how to reorder the VoiceOver focus. So here is an example of how to create a UIView subclass that exposes its accessible subviews to VoiceOver by tag.
AccessibilitySubviewsOrderedByTag.h
#import <UIKit/UIKit.h>
#interface AccessibilitySubviewsOrderedByTag : UIView
#end
AccessibilitySubviewsOrderedByTag.m
#import "AccessibilityDirectional.h"
#implementation AccessibilitySubviewsOrderedByTag {
NSMutableArray *_accessibilityElements;
}
//Lazy loading accessor, avoids instantiating in initWithCoder, initWithFrame, or init.
-(NSMutableArray *)accessibilityElements{
if (!_accessibilityElements){
_accessibilityElements = [[NSMutableArray alloc] init];
}
return _accessibilityElements;
}
// Required accessibility methods...
-(BOOL)isAccessibilityElement{
return NO;
}
-(NSInteger)accessibilityElementCount{
return [self accessibilityElements].count;
}
-(id)accessibilityElementAtIndex:(NSInteger)index{
return [[self accessibilityElements] objectAtIndex:index];
}
-(NSInteger)indexOfAccessibilityElement:(id)element{
return [[self accessibilityElements] indexOfObject:element];
}
// Handle added and removed subviews...
-(void)didAddSubview:(UIView *)subview{
[super didAddSubview:subview];
if ([subview isAccessibilityElement]){
// if the new subview is an accessibility element add it to the array and then sort the array.
NSMutableArray *accessibilityElements = [self accessibilityElements];
[accessibilityElements addObject:subview];
[accessibilityElements sortUsingComparator:^NSComparisonResult(id obj1, id obj2){
// Here we'll sort using the tag, but really any sort is possible.
NSInteger one = [(UIView *)obj1 tag];
NSInteger two = [(UIView *)obj2 tag];
if (one < two) return NSOrderedAscending;
if (one > two) return NSOrderedDescending;
return NSOrderedSame;
}];
}
}
-(void)willRemoveSubview:(UIView *)subview{
[super willRemoveSubview:subview];
// Clean up the array. No check since removeObject: is a safe call.
[[self accessibilityElements] removeObject:subview];
}
#end
Now simply enclose your buttons in an instance of this view, and set the tag property on your buttons to be essentially the focus order.
In Swift you just have to set view's accessiblityElements array property:
view.accessibilityElements = [view1, view2, view3] // order you wish to have
I know this is an old thread, but I found that the easiest way to do it is to subclass UIView. Then simply modify your main UIView type in storyboard to AccessibiltySubviewsOrderedByTag and update the tags in each subview you want to read in order.
class AccessibilitySubviewsOrderedByTag: UIView {
override func layoutSubviews() {
self.accessibilityElements = [UIView]()
for accessibilitySubview in self.subviews {
if accessibilitySubview.isAccessibilityElement {
self.accessibilityElements?.append(accessibilitySubview)
}
}
self.accessibilityElements?.sort(by: {($0 as AnyObject).tag < ($1 as AnyObject).tag})
}
}
This doesn’t directly answer the original question, but it answers the title of the question:
When I want VoiceOver to swipe down a column, I have been using a containing view for the column with shouldGroupAccessibilityChildren set.
I wish I had known this earlier, because it can be a pain to retroactively insert containers into an autolayout situation…
I tried Wesley's answer of setting the array of the accessibilityElements but it didn't work for me.
Apple has some documentation Enhancing the Accessibility of Table View Cells with an example in code. Basically you set the accessibility label of the cell (the parent view) to the values of the accessibility labels of the child views.
[cell setAccessibilityLabel:[NSString stringWithFormat:#"%#, %#", cityLabel, temperatureLabel]];
This is what worked for me.
I found a convenience way yesterday. Similar to #TejAces ' answer.
Make a new swift file, then copy these things into it.
import UIKit
extension UIView {
func updateOrder(_ direction: Bool = true) {
var tempElements: [Any]? = [Any]()
let views = (direction) ? subviews : subviews.reversed()
for aView in views {
tempElements?.append(aView)
}
accessibilityElements = tempElements
}
}
class ReorderAccessibilityByStoryBoardView: UIView {
override func didAddSubview(_ subview: UIView) {
updateOrder()
super.didAddSubview(subview)
}
}
Set the UIView(contains views you want to reorder)'s class as ReorderAccessibilityByStoryBoardView. Then you can reorder them by reordering storyboard's view list.
Because subview doesn't contain views in StackView/ScrollView, you need to make a independent class in this file. Such as the ReorderAccessibilityByStoryBoardStackView down below.
class ReorderAccessibilityByStoryBoardStackView: UIStackView {
override func didAddSubview(_ subview: UIView) {
updateOrder(false)
super.didAddSubview(subview)
}
}
With these codes, you can also reorder view's added in code by adding them in a specific order.
I think you can do it in the storyboard. The VoiceOver order is determined by the order of the views in the document outline.
Just drag and drop the views in the view hierarchy in the right order.
Edit:
Sorry I can not post screenhots until 10 reputation. In the storyboard, the document outline is the area on the left where your scenes with their subviews are listed. Here, subviews are ordered one below each other. When you change this order, the reading-order of VoiceOver will change.
Swift 5.x
Following the advice of ChrisJF , I've wrote a little extension to bypass the Apple bug around the correct order reading items.
extension UIView {
func setAccessibilityOrder(_ arrayViews:[Any]?){
self.accessibilityElements = arrayViews
let arrayStrings:[String] = arrayViews?.map { String(($0 as AnyObject).accessibilityLabel ?? "") } ?? []
let formatList = arrayStrings.map { _ in "%#" }.joined(separator: ", ")
self.accessibilityLabel = String(format: formatList, arguments:arrayStrings)
self.isAccessibilityElement = true
}
}
Usage:
view1.accessibilityLabel = "my view 1"
label2.accessibilityLabel = "my label 2"
button3.accessibilityLabel = "my button 3"
let order = [view1, label2, button3]
self.setAccessibilityOrder(order) // or self.view.setAccessibilityOrder(order) if you are on a parent controller