本文仅记录日常iOS开发的一些技巧, 不定时更新, 最新的置顶, 欢迎关注打赏订阅
5.记录百度地图的一个bug:
百度地图刷新页面标注点的时候, 首先需要删除所有标注,如下
//删除所有标注
NSArray *arrayAnmation=[[NSArray alloc] initWithArray:_mapView.annotations];
[_mapView removeAnnotations:arrayAnmation];
然后添加新的标注:
[_mapView addAnnotation:point];
添加完标注之后, 就会响应下面的回调:
- (BMKAnnotationView *)mapView:(BMKMapView *)mapView viewForAnnotation:(id<BMKAnnotation>)annotation{
BMKAnnotationView* view = nil;
view = [mapview dequeueReusableAnnotationViewWithIdentifier:@"start_node"];
}
问题就出在上面的view, 调用复用之后, 心想已经删除所有标注了, 调用[mapview dequeueReusableAnnotationViewWithIdentifier:@"start_node"];后应该返回nil, 但在添加第一个标注点的时候, 发现会有返回值, 而不是空. 除了第一个标注点之外, 却不会有返回值, 于是加入下面的代码:
- (BMKAnnotationView *)mapView:(BMKMapView *)mapView viewForAnnotation:(id<BMKAnnotation>)annotation{
BMKAnnotationView* view = nil;
view = [mapview dequeueReusableAnnotationViewWithIdentifier:@"start_node"];
if (view == nil) {
view = [[BMKAnnotationView alloc]initWithAnnotation:routeAnnotation reuseIdentifier:@"start_node"];
}else{
view = nil;
view = [[BMKAnnotationView alloc]initWithAnnotation:routeAnnotation reuseIdentifier:@"start_node"];
}
return view;
}
总结: 删除标注点后, 内存还会有一份复用的view保存着, UITableView的复用机制应该也是这样的. 所以即使删除了所有复用视图, 还是要判空一下.
4.有些场景下, 我们使用Masonry进行自动布局时, 会获取不到frame或者bounds
例如当想设置图片只有两个圆角的时候, 或者设置控件需要取得frame的时候, 会发现frame或者bounds都为0, 即这样设置圆角时:
_thumbView = [[UIImageView alloc] init];
[self.contentView addSubview:_thumbView];
[_thumbView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_bubbleView.mas_left);
make.size.mas_equalTo(CGSizeMake(kGridViewHeight, kGridViewHeight ));
make.top.equalTo(_bubbleView.mas_top);
}];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_thumbView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii:CGSizeMake(6, 6)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _thumbView.bounds;
maskLayer.path = maskPath.CGPath;
_thumbView.layer.mask = maskLayer;
图片直接就不显示了, 打个断点在maskLayer.frame = _thumbView.bounds;的前面, 会发现_thumbView.bounds全部为0.于是搜寻各种方法, 了解到了下面这些方法:
setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用layoutSubviews。
layoutIfNeeded:告知页面布局立刻更新。所以一般都会和setNeedsLayout一起使用。如果希望立刻生成新的frame需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。
layoutSubviews:系统重写布局
setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始
updateConstraintsIfNeeded:告知立刻更新约束
updateConstraints:系统更新约束
而我们就可以在layoutIfNeeded里面获取到空间的frame, 直接贴代码:
- (void)layoutIfNeeded{
[super layoutIfNeeded];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_thumbView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii:CGSizeMake(6, 6)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = _thumbView.bounds;//这里就可以获取到我们想要的frame啦
maskLayer.path = maskPath.CGPath;
_thumbView.layer.mask = maskLayer;
}
效果如图:
END
3.实现UITableView头部弹弹的动画效果,
先上效果图
其实就是检测UIScrollView(当然了,UITableView也是UIScrollView)滚动到某个位置, 如果小于某个位置回弹, 大于某个位置展开, 当然也可以换作自己的动画逻辑, 然后让UIScrollView继续平滑滚动, 滚动到固定的位置, 使其定住. 注意这个平滑的要求, 有种方案就是设置UIScrollView的contentoff, 但这种方案是一下子滚动到指定位置, 而不是平滑. 于是我们搜寻一下UIScrollView的代理方法:
@protocol UIScrollViewDelegate<NSObject>
@optional
- (void)scrollViewDidScroll:(UIScrollView *)scrollView; // any offset changes
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes
// called on start of dragging (may require some time and or distance to move)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
// called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView; // called on finger up as we are moving
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; // called when scroll view grinds to a halt
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView; // called when setContentOffset/scrollRectVisible:animated: finishes. not called if not animating
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView; // return a view that will be scaled. if delegate returns nil, nothing happens
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView; // return a yes if you want to scroll to the top. if not defined, assumes YES
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView; // called when scrolling animation finished. may be called immediately if already at top
@end
里面有个方法我们比较感兴趣, 但平日里比较少用, 就是这个:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
注释里告诉我们, 滚动完之后, 可以继续移动. 而targetContentOffset就是我们想要的目标偏移量, 里面的(inout CGPoint *)回调也比较奇怪, 字面意思大概就是可以当作输入输出, 应该是当作一个指针当作回传, 告诉UIScrollView将要移动到哪, 我们可以在targetContentOffset设置输入量, 如下:
targetContentOffset->x = 0;
targetContentOffset->y = -104;
当然这种设置并不完美, 在一些奇葩的拖动会有鬼畜的效果, 于是我们对速度加入了约束:
//也就是速度为0 之后再做滚动
if (velocity.y == 0) {
if ((-newY) > 100 && (-(newY) + 64) < self.headerView.sectionHeaderViewOrinY) {
targetContentOffset->x = 0;
targetContentOffset->y = -104;
}
}
于是就可以作出开头显示的动画了.END
2.实现UITextField回删按钮的回调检测
UITextField的代理事件只有下面几种:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;
- (void)textFieldDidBeginEditing:(UITextField *)textField;
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;
- (void)textFieldDidEndEditing:(UITextField *)textField;
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string;
- (BOOL)textFieldShouldClear:(UITextField *)textField;
- (BOOL)textFieldShouldReturn:(UITextField *)textField;
想要检测回删按钮的事件时, 发现没有回调事件,看来需要我们自己来实现UITextField回删按钮的回调.
首先点进UITextField的实现
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITextField : UIControl <UITextInput, NSCoding, UIContentSizeCategoryAdjusting>
实现了UITextInput的协议, 再点进UITextInput的协议看看
protocol UITextInput <UIKeyInput>
@required
/* Methods for manipulating text. */
- (nullable NSString *)textInRange:(UITextRange *)range;
- (void)replaceRange:(UITextRange *)range withText:(NSString *)text;
里面的几个方法没有我们感兴趣的, 继续点进UIKeyInput,可以看到
@protocol UIKeyInput <UITextInputTraits>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL hasText;
#else
- (BOOL)hasText;
#endif
- (void)insertText:(NSString *)text;
- (void)deleteBackward;
@end
好了, 我们找到了- (void)deleteBackward;就是隐藏的删除回调方法, 只是没写在UITextField的回调方法里面而已. 因此我们可以写一个继承UITextField的自定义类, 如下:
#import <UIKit/UIKit.h>
@protocol KeyboardInputDelegate <NSObject>
@optional
//我们自己定义的一个回删按钮的回调方法, 顺便传回UITextField
- (void)deleteBackword:(UITextField *)field;
@end
@interface DeleteDetectionTextField : UITextField
@property (nonatomic, weak) id<KeyboardInputDelegate> KeyboardInputDelegate;
@end
然后重写一下- (void)deleteBackward;方法
//这个就是被隐藏的回删方法, 咱们覆写一下
- (void)deleteBackward{
[super deleteBackward];
if (self.KeyboardInputDelegate && [self.KeyboardInputDelegate respondsToSelector:@selector(deleteBackword:)]) {
//这句话会不会引发循环引用?
[self.KeyboardInputDelegate deleteBackword:self];
}
}
然后我们调用自定义的DeleteDetectionTextField使用时, 记得设置一下代理:
inputField.delegate = self;
inputField.KeyboardInputDelegate = self;
实现回调:
#pragma mark - 回删按钮的回调
- (void)deleteBackword:(UITextField *)field{
//这里就可以干羞羞的事情
}
完成
1.xib拖的控件不能用代码作动画,用代码作动画会无端出现很多坑. 因此xib比较适合静态视图的搭建,一些动画视图还是使用纯代码比较合适些