I am re-using a UIView subclass in a few places, but need the layout to be slightly different on occasion - sometimes the subviews are laid out horizontally and sometimes vertically.
I would like to somehow replicate the 'initWithStyle' approach that UITableView and other UIViews employ. It should be possible to 'initWithStyle' but there should be a default also. It should also be possible to change the style using setStyle.
What are the steps to achieve this? I tried defining an enum and a new initializer, but I am out of my depth here.
Sounds like you're on the right track, so I'm not sure how helpful I can be. Create your public initializers and public setStyle methods. Sorry for any typos. I'm doing this without an editor.
.h
-(id)initWithFrame:(CGRect)frame;
-(id)initWithFrame:(CGRect)frame style:(MyStyleEnum)myStyleEnum;
-(void)setStyle:(MyStyleEnum)myStyleEnum;
.m
//here's your default method which passes your default style to your custom init method
-(id)initWithFrame:(CGRect)frame {
return [self initWithFrame:frame style:myStyleEnumDefault];
}
//your custom init method that takes a style
-(id)initWithFrame:(CGRect)frame style:(MyStyleEnum)myStyleEnum {
self = [super initWithFrame:frame];
if(self) {
[self setStyle:myStyleEnum];
}
return self;
}
//this can be called after initialization
-(void)setStyle:(MyStyleEnum)myStyleEnum {
//make sure the view is clear before you layout the subviews
//[self.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];//not needed adjusting constraints and not repopulating the view
//change the view constraints to match the style passed in
//the acutally changing, should probably be done in separate methods (personal preference)
if(myStyleEnum == myStyleEnumDefault) {
//change constraints to be the default constraints
} else if(myStyleEnum == myStyleEnumA) {
//change constraints to be the A constraints
} else if(myStyleEnum == myStyleEnumB) {
//change constraints to be the B constraints
}
}
Related
So, I have a tableview and inside the viewforheaderinsection, I create a view, create some controls such as buttons and segmented controls programmatically. I add those controls as subview of the view and then return the view. The problem is when Accessibility reads the controls, it appends "heading" at the end. It says "button" pauses and then says "heading". I know I can convert the headerview to cells to suppress the "heading" callout but that is not an option. The project is pretty big and it requires a lot of time to change headerviews to cells. Is there a way to suppress the "heading" callout without changing headerview to cell?
You need to implement
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section;
for example
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
view.accessibilityTraits = UIAccessibilityTraitNone;
}
inside that function, set the accessibility trait for the header to UIAccessibilityTraitNone, or simply remove UIAccessibilityTraitHeader.
Doing it inside
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
will not do any good because the iOS will add the header trait later on. You need to modify the trait right before the header is being displayed.
The answer by #cocoaaficionado did not work for me on iOS 10.3.
What I had to do was the following:
For your header view, you need to froce by subclassing, the accessibilityElements property of the view, and use UIAccessibilityElements instead of UIView/UILabel/UIButton directly.
#interface NotAHeadingView : UIView
#end
#implementation NotAHeadingView
- (void)layoutSubviews
{
[super layoutSubviews];
NSMutableArray *accessibilityElements = [NSMutableArray arrayWithCapacity:self.subviews.count];
for (UIView *view in self.subviews) {
UIAccessibilityElement *element = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
// remove Header trait
element.accessibilityTraits = view.accessibilityTraits & ~UIAccessibilityTraitHeader;
element.accessibilityFrame = view.accessibilityFrame;
element.accessibilityLabel = view.accessibilityLabel;
element.accessibilityValue = view.accessibilityValue;
element.accessibilityHint = view.accessibilityHint;
element.accessibilityFrameInContainerSpace = view.frame;
[accessibilityElements addObject:element];
}
self.accessibilityElements = accessibilityElements;
}
#end
UIViews seem to automatically inherit the Header trait from the view hierarchy, even overriding traits for their direct superview isn't enough. This worked on an iOS 10.3 device for me.
This works for simple cases, but did not work for me when applied to a very complex section header view (multiple child viewControllers managing the subviews, deep view hierarchy including scrollViews) - instead it was much easier to resign how this view worked to avoid section headers for these elements. YMMV
Swift solution
Subclassing header elements and overriding their accessibilityTraits property was the easiest solution for me.
For example, I had the same situation for buttons in header, and here's the solution that works for me:
class HeaderButton: UIButton {
private var _accessibilityTraits: UIAccessibilityTraits = UIAccessibilityTraits.button
override var accessibilityTraits: UIAccessibilityTraits {
get {
return _accessibilityTraits
}
set {
_accessibilityTraits = newValue
_accessibilityTraits.remove(UIAccessibilityTraits.header)
}
}
}
#abalas solution works for me:
Try using.
private var _accessibilityTraits: UIAccessibilityTraits = UIAccessibilityTraits.none
override var accessibilityTraits: UIAccessibilityTraits {
get {
return _accessibilityTraits
}
set {
_accessibilityTraits = newValue
_accessibilityTraits.remove(UIAccessibilityTraits.header)
}
}
Rewrite setAccessibilityTraits of these controls such as buttons and segmented controls.
For example:
- (void)setAccessibilityTraits:(UIAccessibilityTraits)accessibilityTraits
{
accessibilityTraits = UIAccessibilityTraitButton;
[super setAccessibilityTraits:accessibilityTraits];
}
I have a UIStackView where the middle button is only visible in a different size class
storyboard view
After rotating the device the button will be visible because of its size class and is also in the right view hierarchy (image), but is has not enough constraints given by UIstackview to be positioned correctly, it is positioned on the upper left corner (label middle).
not enough constraints
The working buttons (not affected by size class changes) have much more constraints.
Is this a bug or am I missing something.
Does anybody know a workaround?
It's definitely a bug in iOS. When the middle button is installed, it's being added as a subview of the stack view, but not as an arranged subview.
Here's a workaround. Set the custom class of the middle button to StackViewBugFixButton. Then connect the (new) priorView outlet of the middle button to the next button to the left. Here's the definition of StackViewBugFixButton:
StackViewBugFixButton.h
#import <UIKit/UIKit.h>
#interface StackViewBugFixButton : UIButton
#property (nonatomic, weak) IBOutlet UIView *priorSibling;
#end
StackViewBugFixButton.m
#import "StackViewBugFixButton.h"
#implementation StackViewBugFixButton
- (void)didMoveToSuperview {
[super didMoveToSuperview];
[self putIntoArrangedSubviewsIfNeeded];
}
- (void)putIntoArrangedSubviewsIfNeeded {
if (![self.superview isKindOfClass:[UIStackView class]]) {
return;
}
if (self.priorSibling == nil) {
NSLog(#"%# %s You forgot to hook up my priorSibling outlet.", self, __func__);
return;
}
UIStackView *stackView = (UIStackView *)self.superview;
if ([stackView.arrangedSubviews indexOfObject:self] != NSNotFound) {
return;
}
NSUInteger priorSiblingIndex = [stackView.arrangedSubviews indexOfObject:self.priorSibling];
if (priorSiblingIndex == NSNotFound) {
NSLog(#"%# %s My priorSibling isn't in my superview's arrangedSubviews.", self, __func__);
return;
}
[stackView insertArrangedSubview:self atIndex:priorSiblingIndex + 1];
}
#end
You'll get one spurious warning of “You forgot to hook up my priorSibling outlet” because the view gets added as a subview of the stack view during loading, before its outlets have been connected.
Solutions offered by Rob and Uwe definitely work and address the issue of the conditionally-installed views not being added to arrangedSubviews on trait collection changes.
I preferred to address the issue without having to subclass every view that I wished to place inside a UIStackView though, so I subclassed UIStackView itself to override layoutSubviews() and add the orphaned subviews to arrangedSubviews:
class ManagedStackView: UIStackView {
override func layoutSubviews() {
super.layoutSubviews()
let unarrangedSubviews = subviews.filter({ !arrangedSubviews.contains($0) })
unarrangedSubviews.forEach({ insertArrangedSubview($0, atIndex: $0._arrangedSubviewIndex) })
}
}
extension UIView {
private var _arrangedSubviewIndex: Int {
get {
return tag
}
set {
tag = newValue
}
}
}
Thanks to Rob I created the following solution with swift, you have to set controlIndex in user defined runtime values however to the right index
(see image)
import UIKit
class UIButtonFixForStackView: UIButton {
var controlIndex:Int = 0
internal override func didMoveToSuperview() {
if (superview?.isKindOfClass(UIStackView) == false)
{
return
}
if let stackView = self.superview as? UIStackView
{
if stackView.arrangedSubviews.indexOf(self) != nil
{
return
}
stackView.insertArrangedSubview(self, atIndex: controlIndex)
}
}
}
While the accepted answers should work as a programmatic approach, it is worth noting that you can avoid this problem in Interface Builder by adding your size class variations for the Hidden property instead of the Installed property. iOS correctly handles this scenario because the arrangedSubviews never actually change, and UIStackView is built to adapt its layout when (arranged) subviews are hidden.
For example, if you want to hide a specific view for Compact widths, the subview's IB configuration would look like the following. Note that the view is Installed in all size classes:
When using auto layout, the view's size is unknown when it is initialised, this brings a problem to me.
When using UIImageView, I wrote a category that can load image from my own CDN by setting the image URL to UIImageView, my CDN stores one image with different sizes so that difference devices can load the size it really needs.
I want to make my UIImageView be able to load the URL for the resolution it needs, but when my UIImageView get the URL, the size of it is not yet determined by auto layout.
So is there a way for UIView to know that the layout process for it has finished for the first time?
there is a method for UIView.You could override it.
-(void)layoutSubviews {
CGRect bounds =self.bounds;
//build your imageView's frame here
self.imageView=imageViewFrame.
}
In Swift 5.X
override func layoutSubviews() {
super.layoutSubviews()
let myFrame = = self.bounds
}
If you have other complex items in the custom view, don't forget to call super if you override layoutSubviews()...
Sadly, there is no straightforward way that a UIView can be notified that its constraints have been set. You can try a bunch of different things though,
Implement layoutSubviews function of a UIView, this is called whenever UIView's layout is changed.
Implement viewDidLayoutSubviews of the UIViewController that has it inside it. This function is called when all the layouts have been set. At this point you can your category function.
here's a test, I only use autolayout and I only use custom subclassed subviews. I do all auto layout in the initializer of the subclass:
This is for a "login button" that has no frame but is then set with autolayout:
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewDidLayoutSubviews");
}
}
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewWillLayoutSubviews");
}
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"didAppear");
}
}
-(void)viewWillAppear:(BOOL)animated {
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewWillAppear");
}
}
-(void)viewDidLoad {
[super viewDidLoad];
if (self.contentView.loginButton.frame.size.height) {
NSLog(#"viewDidLoad");
}
}
Here's the output. So, this means that my view finally has a frame when the
2015-08-25 01:28:27.789 [67502:1183631] viewDidLayoutSubviews
2015-08-25 01:28:27.790 [67502:1183631] viewWillLayoutSubviews
2015-08-25 01:28:27.790 [67502:1183631] viewDidLayoutSubviews
2015-08-25 01:28:28.007 [67502:1183631] didAppear
This means that the first time the login button has a frame is in the viewDidLayoutSubviews, this will look weird becuase this is the first pass of main view's subviews. There's no frame in ViewWillAppear although the view of the viewcontroller itself is already set before the login button. The entire UIView subclass's main view is also set when the viewcontroler's view is set, this happens before the login button as well. So, the subviews of the view are set after the parent view is set.
The point is this: if you plop the imageview information pull in the viewDidLayoutSubviews then you have a frame to work with, unless you set this UIImageView frame to the view of the ViewController by type casting then you will have the UIImageView's frame set in the viewDidLoad. Good luck!
I am using Storyboard to create the UICollectionViewController - CollectionView - Cell(my own DasboardsViewCell) and my customer view (DashBoardView) inside cell. I wired up everything correctly and everything seems to work except when I scroll up and down. I will explain what my understanding is after I debug.
Also I have 2 views inside my DashBoardView custom view which i used "one" as main primary and other as FlipView (e.g. when user taps on cell). Since everything is wired from storyboard I dont have to registerClass for reusing.
1) In my DashboardCustomView (who has 2 other views in it as above) i do this
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
_isPrimary = YES;
//to init my 2 child views and insert to main custom view
[self initSubViewsWithInsert];
}
return self;
}
2) In my DashBoardViewCell class I do this
#synthesize dashBoardView = _dashBoardView;
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.backgroundColor = [UIColor colorWithWhite:0.75f alpha:0.6f];
self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 2.0f;
self.layer.cornerRadius = 7.0f;
}
return self;
}
// If I dont do this below I get overlapped images and data on cell when scrolling up and down
- (void)prepareForReuse
{
self.dashBoardView.mainContainerView = nil;
self.dashBoardView.flipView = nil;
}
3) Now after this I still see the custom view appears out of order and seems somewhat better than not doing "nil" on my views, but the problem is once I nil out my 2 subviews I have no defined way to reinitialize them when my collectionview controller requeus the cell and start adding my content.
i.e. I do like this cellForItemAtIndexPath
if ([cell isKindOfClass:[DashboardsViewCell class]]) {
DashboardsViewCell *dashCell = (DashboardsViewCell*) cell;
Dashboard* tmpDashboard = self.availableDashBoards[indexPath.item];
[dashCell.dashBoardView addDashboardImageViewAtPoint:tmpDashboard.identifier
usingIndexPath:indexPath];
Last line above is adding some image and text on dashboards main view which is self.dashBoardView.mainContainerView and it has already nil.
Can someone help me understand if there is defined way to do this. Also if you think I am looking at wrong problem
I'm not sure what the issue is exactly and with that code it's a bit hard to tell. If you could post the project somewhere it would be easier. Now that many things are storyboarded, it's harder to fix issues just by posting code :(
But are you using auto layout by any chance?
Thanks for your reply - No Im not using auto layout and I am unable to post my code here as it has grown too big now but here is what I did to solve the problem
1) I realized when cells are being refreshed (dequeue) my logic was such that I was actually maintaining state for certain cell (User taps dashboard-B from A,B,C,D etc) with my own internal state thinking that same cell will be applied to same indexPath. i.e.
a)If user taps on cell 2 - I switch to secondary view and maintains that state in my cell.
b)Now if user scrolls up and down then cells get refreshed and I releases my internal view in prepareForResuse
once i know what i was doing wrong, I have to change my approach and move the logic in controller.
I may sound obscure here but I found that I was wrong in my assumption about cell cellForItemAtIndexPath and what it supposed to do. Once I figured that out i was able to fix my design.
I have a custom class that's a subclass of UIView. In the storyboard I set a UIView's class to the custom class. The view in the storyboard has a height constraint so that I can change the height programmatically. (I know it's not the only way, but I think it's the easiest way.)
I want to perform some code in the custom class every time the view's height changes.
I tried the following:
- (void)setFrame:(CGRect)frame {
[super setFrame:frame};
NSLog(#"Frame did change");
}
But this method only runs on startup, not when it's (self) height was changed. How can I perform code anytime it's frame is changed?
Just override layoutSubviews method in your custom view class
- (void)layoutSubviews
{
[super layoutSubviews];
NSLog(#"Frame did change");
}