When my app gets back to its root view controller, in the viewDidAppear: method I need to remove all subviews.
How can I do this?
Edit: With thanks to cocoafan: This situation is muddled up by the fact that NSView and UIView handle things differently. For NSView (desktop Mac development only), you can simply use the following:
[someNSView setSubviews:[NSArray array]];
For UIView (iOS development only), you can safely use makeObjectsPerformSelector: because the subviews property will return a copy of the array of subviews:
[[someUIView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
Thank you to Tommy for pointing out that makeObjectsPerformSelector: appears to modify the subviews array while it is being enumerated (which it does for NSView, but not for UIView).
Please see this SO question for more details.
Note: Using either of these two methods will remove every view that your main view contains and release them, if they are not retained elsewhere. From Apple's documentation on removeFromSuperview:
If the receiver’s superview is not nil, this method releases the receiver. If you plan to reuse the view, be sure to retain it before calling this method and be sure to release it as appropriate when you are done with it or after adding it to another view hierarchy.
Get all the subviews from your root controller and send each a removeFromSuperview:
NSArray *viewsToRemove = [self.view subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
In Swift you can use a functional approach like this:
view.subviews.forEach { $0.removeFromSuperview() }
As a comparison, the imperative approach would look like this:
for subview in view.subviews {
subview.removeFromSuperview()
}
These code snippets only work in iOS / tvOS though, things are a little different on macOS.
If you want to remove all the subviews on your UIView (here yourView), then write this code at your button click:
[[yourView subviews] makeObjectsPerformSelector: #selector(removeFromSuperview)];
This does only apply to OSX since in iOS a copy of the array is kept
When removing all the subviews, it is a good idea to start deleting at the end of the array and keep deleting until you reach the beginning. This can be accomplished with this two lines of code:
for (int i=mySuperView.subviews.count-1; i>=0; i--)
[[mySuperView.subviews objectAtIndex:i] removeFromSuperview];
SWIFT 1.2
for var i=mySuperView.subviews.count-1; i>=0; i-- {
mySuperView.subviews[i].removeFromSuperview();
}
or (less efficient, but more readable)
for subview in mySuperView.subviews.reverse() {
subview.removeFromSuperview()
}
NOTE
You should NOT remove the subviews in normal order, since it may cause a crash if a UIView instance is deleted before the removeFromSuperview message has been sent to all objects of the array. (Obviously, deleting the last element would not cause a crash)
Therefore, the code
[[someUIView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
should NOT be used.
Quote from Apple documentation about makeObjectsPerformSelector:
Sends to each object in the array the message identified by a given
selector, starting with the first object and continuing through the
array to the last object.
(which would be the wrong direction for this purpose)
Try this way swift 2.0
view.subviews.forEach { $0.removeFromSuperview() }
view.subviews.forEach { $0.removeFromSuperview() }
Use the Following code to remove all subviews.
for (UIView *view in [self.view subviews])
{
[view removeFromSuperview];
}
Using Swift UIView extension:
extension UIView {
func removeAllSubviews() {
for subview in subviews {
subview.removeFromSuperview()
}
}
}
In objective-C, go ahead and create a category method off of the UIView class.
- (void)removeAllSubviews
{
for (UIView *subview in self.subviews)
[subview removeFromSuperview];
}
In order to remove all subviews Syntax :
- (void)makeObjectsPerformSelector:(SEL)aSelector;
Usage :
[self.View.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
This method is present in NSArray.h file and uses NSArray(NSExtendedArray) interface
If you're using Swift, it's as simple as:
subviews.map { $0.removeFromSuperview }
It's similar in philosophy to the makeObjectsPerformSelector approach, however with a little more type safety.
For ios6 using autolayout I had to add a little bit of code to remove the constraints too.
NSMutableArray * constraints_to_remove = [ #[] mutableCopy] ;
for( NSLayoutConstraint * constraint in tagview.constraints) {
if( [tagview.subviews containsObject:constraint.firstItem] ||
[tagview.subviews containsObject:constraint.secondItem] ) {
[constraints_to_remove addObject:constraint];
}
}
[tagview removeConstraints:constraints_to_remove];
[ [tagview subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
I'm sure theres a neater way to do this, but it worked for me. In my case I could not use a direct [tagview removeConstraints:tagview.constraints] as there were constraints set in XCode that were getting cleared.
In monotouch / xamarin.ios this worked for me:
SomeParentUiView.Subviews.All(x => x.RemoveFromSuperview);
In order to remove all subviews from superviews:
NSArray *oSubView = [self subviews];
for(int iCount = 0; iCount < [oSubView count]; iCount++)
{
id object = [oSubView objectAtIndex:iCount];
[object removeFromSuperview];
iCount--;
}
Related
I have a Xcode project and in it i have dragged two views and both of them inherit from a class LabelsView. However when I try and run the code to find out number of subviews, I get 4. Can anyone explain why is this happening.
The code is
NSLog(#"no. of subviews:%#",[NSString stringWithFormat:#"%d",[self.superview.subviews count]]);
You're probably getting a weird subview count because you're accessing self.superview.subviews. You likely just want self.subviews.
If, like you said, you only care about subviews of type LabelsView, you can filter those out like this:
int labelViewCount = 0;
for(LabelsView *subview in self.subviews) {
if([subview isKindOfClass:[LabelsView class]]) {
labelViewCount++;
}
}
NSLOG(#"label count: %d", labelViewCount);
If you want the amount of all subviews in swift, you can just go with
self.subviews.count
Are there any techniques to cause a UIWebView to redraw itself? I've tried setNeedsDisplay and setNeedsLayout on the UIWebView and its UIScrollView, but neither have worked.
Literally found the answer right after asking. The key was to tell the subviews of UIWebView's scrollView to redraw themselves - particularly the UIWebBrowserView.
- (void) forceRedrawInWebView:(UIWebView*)webView {
NSArray *views = webView.scrollView.subviews;
for(int i = 0; i<views.count; i++){
UIView *view = views[i];
//if([NSStringFromClass([view class]) isEqualToString:#"UIWebBrowserView"]){
[view setNeedsDisplayInRect:webView.bounds]; // Webkit Repaint, usually fast
[view setNeedsLayout]; // Webkit Relayout (slower than repaint)
// Causes redraw & relayout of *entire* UIWebView, onscreen and off, usually intensive
[view setNeedsDisplay];
[view setNeedsLayout];
// break; // glass in case of if statement (thanks Jake)
//}
}
}
I've commented out the if statement to be safe and avoid reliance on UIWebBrowserView's class name not changing. Without it, it hits all UIViews that are in the scrollview, which isn't really a problem at this point (no significant overhead incurred) but could always change.
EDIT:
In some cases, the following snippet of JavaScript will accomplish the same/similar thing:
window.scrollBy(1, 1); window.scrollBy(-1, -1);
You'd think UIScrollView's contentOffset would do this too, but that's not always the case in my experience - for some reason window.scrollTo is special in this regard.
Gist: https://gist.github.com/matt-curtis/5843862
This save my life:
self.wkWebView.evaluateJavaScript("window.scrollBy(1, 1);window.scrollBy(-1, -1);", completionHandler: nil)
Add this line on webview did load delegate event or wkwebview did finish navigation
Thanks MAN!!!!
Is there a way to set the color of the activity indicator (probably UIActivityIndicatorView) of a UIRefreshControl?
I was able to set the color of the 'rubber' and the indicator:
[_refreshControl setTintColor:[UIColor colorWithRed:0.0f/255.0f green:55.0f/255.0f blue:152.0f/255.0f alpha:1.0]];
But I want to have the 'rubber' blue and the activity indicator white, is this possible?
This is not officially supported, but if you want to risk future iOS changes breaking your code you can try this:
Building off ayoy's answer, I built a subclass of UIRefreshControl, which sets the color of the ActivityIndicator in beginRefresing. This should be a better place to put this, since you may call this in code instead of a user causing the animation to begin.
#implementation WhiteRefreshControl : UIRefreshControl
- (void)beginRefreshing
{
[super beginRefreshing];
NSArray *subviews = [[[self subviews] lastObject] subviews];
//Range check on subviews
if (subviews.count > 1)
{
id spinner = [subviews objectAtIndex:1];
//Class check on activity indicator
if ([spinner isKindOfClass:[UIActivityIndicatorView class]])
{
UIActivityIndicatorView *spinnerActivity = (UIActivityIndicatorView*)spinner;
spinnerActivity.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
}
}
}
Well, you technically can do it, but it's not supported. You better follow Dave's suggestion, or read on if you insist.
If you investigate subviews of UIRefreshControl it turns out that it contains one subview, of class _UIRefreshControlDefaultContentView.
If you then check subviews of that content view in refreshing state, it contains the following:
UILabel
UIActivityIndicatorView
UIImageView
UIImageView
So technically in your callback to UIControlEventValueChanged event you can do something like this:
UIActivityIndicatorView *spinner = [[[[self.refreshControl subviews] lastObject] subviews] objectAtIndex:1];
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
And that would work. It also doesn't violate App Review Guidelines as it doesn't use private API (browsing subviews of a view and playing with them using public API is legal).
But keep in mind that the internal implementation of UIRefreshControl can change anytime and your code may not work or even crash in later versions of iOS.
No, that's not possible. You'll need to file an enhancement request to ask Apple to implement this.
Here's my code:
if([pantallas objectForKey:par]){
UIView *vista= [[UIView alloc] initWithFrame:self.Botones.frame];
vista.backgroundColor= [UIColor brownColor];
CGSize la= CGSizeMake(50,60);
int cuantos= [part2 count];
NSArray *arr= [COLGenerales tileN:cuantos RectsOfSize:la intoSpaceOf:vista.frame withMaxPerRow:5 spaceVertical:10 spaceHorizontal:10];
for(int j=0; j<cuantos; j++){
UIButton *bot= [[UIButton alloc] initWithFrame:[[arr objectAtIndex:j] CGRectValue]];
bot.tag=j;
bot.titleLabel.text=par;
bot.titleLabel.hidden=true;
bot.imageView.image = [UIImage imageNamed:[[part2 allKeys] objectAtIndex:j]];
[bot addTarget:self action:#selector(registrar:) forControlEvents:UIControlEventTouchDown];
[vista addSubview:bot];
}
[pantallas setObject:vista forKey:par];
self.Botones= vista;
}else{
self.Botones= [pantallas objectForKey:par];
}
Botones is a simple view embedded into the view this class controls (first initiated by the Nib file), the class method of COLGenerales returns an array of CGRects coded as NSValues, and registrar: is a local method.
Everything gets properly set (I've thoroughly checked this with the debugger). The view gets successfully created, set, and added to the dictionary.
However, I absolutely never get the actual screen to change. I even included the background color change just to check if it isn't some kind of problem with the buttons. Nothing. Any suggested solution to this?
A property that is an IBOutlet does not have an intrinsic connection to the view hierarchy—it only makes it possible to populate that property from a xib. When you set self.Botones, you'll need to do something like the following:
[self.Botones removeFromSuperview];
self.Botones = newValue;
[self.BotonesSuperview addSubview:self.Botones];
If you update self.Botones in many places, and you always want the change reflected on-screen, you could add this into a setter implementation:
-(void)setBotones:(UIView*)newValue {
if (newValue != _Botones) {
[_Botones removeFromSuperview];
_Botones = newValue;
[self.BotonesSuperview addSubview:_Botones];
}
}
I recommend using a UINavigation controller that houses these two views.
You can reference this link Swapping between UIViews in one UIViewController
Basically, you create one view, removeSubview for the first and then add the second one with addSubview!
[view1 removeFromSuperview];
[self.view addSubview: view2];
Other reference sources:
An easy, clean way to switch/swap views?
How to animate View swap on simple View iPhone App?
Hopefully this helps!
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