iCarousel and UITextView -- blank text view - objective-c

I'm using iCarousel to display editable question cards. The cards contain a UITextView for entering the question (or already contain text as you swipe through filled cards). However, when the carousel is presented and scrolled, sometimes text views appear empty.
This is due to a UITextView optimization of not drawing text offscreen. But text views in a UITableView will not suffer from this.
As many know, using setNeedsDisplay will NOT work due to the optimization, so it doesn't redraw the text.
I currently change the text view's frame by adding and then removing 1px. This forces a redraw. However, I can only do this when the item changes. iCarousel does not have a willDisplayCell delegate method. (Nick, can you add one easily? The code baffles me)
Because iCarousel is preloading many views for smoothness (which is necessary, setting iCarouselOptionVisibleItems doesn't fix anything) there doesn't seem to be anything else I can do but know when the view is about to come on screen. Suggestions?
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
{
MIQuestionView *questionView = (MIQuestionView *)view;
if (questionView == nil)
{
MIQuestionType type = [self.testSection.questionType integerValue];
questionView = [[MIQuestionView alloc] initWithFrame:CGRectNull questionType:type];
questionView.delegate = self;
}
questionView.question = [self.testSection.questions objectAtIndex:index];
return questionView;
}
The text view is embedded in the MIQuestionView. The text is set in the question setter. There's no way for me to know when it's coming onto the screen. To be clear, I don't want to resize. The text is not drawn offscreen and appear blank when coming on-screen.
Sorry, I didn't look for ambiguous code above:
- (id)initWithFrame:(CGRect)frame questionType:(MIQuestionType)type
{
if (CGRectEqualToRect(frame, CGRectNull))
frame = CGRectMake(0, 0, 650, 244);
self = [super initWithFrame:frame];
...

It's an odd bug. I'm not sure if it's an issue with iCarousel or just a quirk of iOS that you need to deal with when dynamically adding and removing UITextViews from the view hierarchy.
I have a solution that's maybe a bit cleaner than the ones you've found though; Just add this to your MICardView:
- (void)didMoveToSuperview
{
if (self.superview)
{
_textView.frame = self.bounds;
}
}
This basically forces the textView to re-layout every time the cardView is recycled, and it avoids you having to do anything special in your view controller to work around the issue.

Related

Discrete (row-by-row) scrolling in NSTextView

How can I implement row-by-row scrolling in an NSTextView object? It has to work regardless of how the user tries to scroll: either if they drag the scroll knob or use a two-finger scroll gesture on the trackpad. Furthermore, the scroll knob should always move in discrete steps. (This is exactly the way Apples terminal emulator works.)
What I tried to do first was to subclass NSTextView and override adjustScroll: (which it inherits from NSView), which is described here, that is:
- (NSRect)adjustScroll:(NSRect)newVisible {
NSRect modifiedVisible = newVisible;
NSFont *font = [self font];
CGFloat lineHeight = [[self layoutManager] defaultLineHeightForFont:font];
modifiedVisible.origin.x = (int)(modifiedVisible.origin.x/lineHeight) * lineHeight;
modifiedVisible.origin.y = (int)(modifiedVisible.origin.y/lineHeight) * lineHeight;
return modifiedVisible;
}
This however only gives row-by-row scrolling when I use the scroll knob. By accident I found out that subclassing NSScrollView would give me row-by-row scrolling also when scrolling with the trackpad, even though it seems I'm not really doing anything (so why this works is a mystery), the only overridden methods are these:
- (instancetype)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
}
The scroll knob, however, still moves continuously. Do I have to subclass NSScroller and override setDoubleValue (I tried doing that, but it didn't work very well so that solution feels difficult, though maybe possible) or can I override reflectScrolledClipView: in my subclass to NSScrollView (that didn't work at all), or are there better solutions?

UIDynamics and Device Rotation

If I add any kind of UIDynamicBehavior to my views, it completely breaks things when the device is rotated. Here's what it is in portrait (displaying correctly):
And here it is in landscape, all broke:
I don't believe it's an autolayout issue because if I remove the calls to add the UIDynamicBehavior it works fine with no autolayout problems. No autolayout errors are ever thrown either. Here's the code:
#interface SWViewController () {
UICollisionBehavior *coll;
UIDynamicAnimator *dynamicAnimator;
}
#implementation SWViewController
- (void)viewDidLoad {
[super viewDidLoad];
dynamicAnimator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self setupCollisions]; // commenting this out fixes the layout
}
- (void)setupCollisions {
NSArray *dynamicViews = #[greenView];
coll = [[UICollisionBehavior alloc] initWithItems:dynamicViews];
CGFloat topBound = CGRectGetMinY(greenView.frame);
[coll addBoundaryWithIdentifier:#"top"
fromPoint:CGPointMake(0, h1)
toPoint:CGPointMake(CGRectGetWidth(greenView.frame), h1)];
[dynamicAnimator addBehavior:coll];
}
If I override didRotateFromInterfaceOrientation I can see that the top boundary of greenView doesn't follow what autolayout says it should (again, removing the call to setupCollisions fixes this).
The autolayout boundaries on greenView are:
height = 200
trailing space to Superview = 0
leading space to Superview = 0
bottom space to Superview = 0
One solution I found was to override willRotateToInterfaceOrientation: and remove the UIDynamicItems and then re-add them in didRotateFromInterfaceOrientation:. This strikes me as rather hacky, and could potentially introduce bugs once I add more complex behaviors.
Dynamic animator is changing frames of involved views. As a result any animation would be disturbed by a call to -[UIView setNeedsLayout] (views would be put to constraint driven positions regardless on dynamic animation state.
My observation is that is you use auto-generated layout constraints the dynamic animator removes them from any view involved in the animation.
If you add your own layout constraints - they persist - but can disturb your animation when view is asked to recalculate layout.
Please double check your auto layout constraints.
I had a very similar problem:
In my case, I have a subview, MyView, which had a set of constraints: V:|-(0)-[MyView], V:[MyView]-(0)-|, H:[MyView:(==300)], and it works good with out UIDynamics. But after adding UIDynamics to MyView, the width changed during rotation, which is very similar to your problem.
I fixed it by adding one more constraint: H:|-(0)-[MyView]

Resize UICollectionView cells after their data has been set

My UICollectionView cells contain UILabels with multiline text. I don't know the height of the cells until the text has been set on the label.
-(CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
This was the initial method I looked at to size the cells. However, this is called BEFORE the cells are created out of the storyboard.
Is there a way to layout the collection and size the cells AFTER they have been rendered, and I know the actual size of the cell?
I think your are looking for the invalidateLayout method you can call on the .collectionViewLayout property of your UICollectionView. This method regenerates your layout, which in your case means also calling -collectionView: layout: sizeForItemAtIndexPath:, which is the right place to reflect your desired item size. Jirune points the right direction on how to calculate them.
An example for the usage of invalidateLayout can be found here. Also consult the UICollectionViewLayout documentation on that method:
Invalidates the current layout and triggers a layout update.
Discussion:
You can call this method at any time to update the layout information. This method invalidates the layout of the collection view itself and returns right away. Thus, you can call this method multiple times from the same block of code without triggering multiple layout updates. The actual layout update occurs during the next view layout update cycle.
Edit:
For storyboard collection view which contains auto layout constraints, you need to override viewDidLayoutSubviews method of UIViewController and call invalidateLayout collection view layout in this method.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[yourCollectionView.collectionViewLayout invalidateLayout];
}
subclass UICollectionViewCell and override layoutSubviews like this
hereby you will anchor cell leading and trailing edge to collectionView
override func layoutSubviews() {
super.layoutSubviews()
self.frame = CGRectMake(0, self.frame.origin.y, self.superview!.frame.size.width, self.frame.size.height)
}
Hey in the above delegate method itself, you can calculate the UILabel size using the below tricky way and return the UICollectionViewCell size based on that calculation.
// Calculate the expected size based on the font and
// linebreak mode of your label
CGSize maximumLabelSize = CGSizeMake(9999,9999);
CGSize expectedLabelSize =
[[self.dataSource objectAtIndex:indexPath.item]
sizeWithFont:[UIFont fontWithName:#"Arial" size:18.0f]
constrainedToSize:maximumLabelSize
lineBreakMode:NSLineBreakByWordWrapping];
- (void)viewDidLoad {
self.collectionView.prefetchingEnabled = NO;
}
In iOS 10, prefetchingEnabled is YES by default. When YES, the collection view requests cells in advance of when they will be displayed. It leads to crash in iOS 10

UITextField -- Adding UIView for leftView - Can't get Focus

The UITextFields in my app have placeholder text defined (in Interface Builder), and I cannot cause these fields to acquire focus (i.e. show the keyboard and allow editing) when I tap on the area occupied by the placeholder text. If I tap on the textfields in an area just outside the that of placeholder text (though still within the bounds of the textfiled itself), it acts as normal (i.e. the keyboard pops up and I can edit the content of the textfield). How can I fix this?
Thanks.
EDIT 1
Ok, I think I've got it. I'm also setting a blank view to the "leftView" property of these UITextFields. If I remove this, you can touch the UITextFields in the area of the placeholder text and it reacts as expected; I need this view for the leftView though. If you change the background color of this spacer view to red, you can see that it doesn't get in the way at all, so I don't know what's going wrong.
Why does this code cause this problem?
Thanks.
+(UIView*)getTextFieldLeftSpacerViewWithBackgroundColor:(UIColor*)backgroundColor andHeight:(CGFloat)height
{
UIView *leftWrapper = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 8.0f, height)];
leftWrapper.autoresizingMask = UIViewAutoresizingNone;
[leftWrapper setOpaque:YES];
if(backgroundColor){leftWrapper.backgroundColor = backgroundColor;}
else{leftWrapper.backgroundColor = [UIColor clearColor];}
return [leftWrapper autorelease];
}
+(void)setTextFieldLeftSpacerForTextFieled:(UITextField*)textField
{
if(textField)
{
UIView *spacer = [MYViewController getTextFieldLeftSpacerViewWithBackgroundColor:nil andHeight:textField.bounds.size.height];
textField.leftView = spacer;
textField.leftViewMode = UITextFieldViewModeAlways;
}
}
Just ran into the same problem and didn't want to subclass, just had to use :
leftWrapper.userInteractionEnabled = NO;
I abandoned this approach. Instead of using an invisible view to offset the text, I opted to subclass UITextField and provide offset CGRects for the bounds of the text within theUITextField. The following SO post was very helpful:
Indent the text in a UITextField

Animate UITableViewCell ContentView to fade upon entering Edit Mode

I've noticed this functionality in the iPhone Mail.app and SMS.app apps, but I'm unsure how to implement it myself.
In a standard UITableView, when a user taps the 'Edit' button and the deletion buttons move into position, as the content views are sliding to the right, they perform a quick fade transition where they are replaced by thinner versions of themselves (to account for the space the deletion button is taking up).
I initially thought this was done by calling the following method when editing mode is enabled:
[self.tableView reloadRowsAtIndexPaths: [tableView indexPathsForVisibleRows] withRowAnimation: UITableViewRowAnimationFade];
However this cannot be right as it also fade transitions the delete button itself as well as any accessory views the cell has (suffice it to say, it looks quite odd).
I was wondering, how would it be possible to force the content view to redraw itself, and to then fade the new drawn version over the old one when the table enters edit mode?
UPDATE:
Thanks to Caleb's answer, this is the final block of code that allowed me to get what I was after:
My final solution was to subclass UITableViewCell and then apply the following code to the setEditing accessor:
-(void) setEditing: (BOOL)editing animated: (BOOL)animated
{
[super setEditing: editing animated: animated];
CATransition *animation = [CATransition animation];
animation.duration = 0.2f;
animation.type = kCATransitionFade;
//redraw the subviews (and animate)
for( UIView *subview in self.contentView.subviews )
{
[subview.layer addAnimation: animation forKey: #"editingFade"];
[subview setNeedsDisplay];
}
}
I probably should just clarify as well. Since performance is of utmost importance, everything inside my cell's contentView is being rendered through CG (ie drawRect:), so I can't control any elements being drawn in there specifically.
That animation happens when you call -setEditing:animated: on the table. The table then calls -setEditing:animated: on each of the visible cells. It looks like the standard behavior for a table cell is to adjust the size of the content views, shift them right, and remove the right accessory. If you have your own cells and want a different look in editing mode, I think you can override -setEditing:animated: to adjust the cell content as you like.