iOS学习笔记(三)

发布在 iOS开发

花了两天半的时间基本实现了APP中让用户选择头像的功能(仿照手机版QQ),整理和总结如下——

思路

- UIImagePickerController


原先想要采用UIImagePickerController来实现这个功能,UIImagePickerController是UINavigationController的子类,是系统提供的拍摄、选择照片和视频的UI。具体来说,在选择头像这个功能中,头像的来源有两种——拍照和从相册选择,同时完成头像的选择后我们还应该能够让用户选择头像的范围以及大小。

若头像的来源为拍照:

1
2
3
4
5
6
UIImagePickerController * imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.delegate = self;
imagePickerController.allowsEditing = YES;//允许编辑图片
imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;//图片的来源为相机
//...还可以在这里修改相机的前后镜头 是否开启闪光灯等
[self presentViewController:imagePickerController animated:YES completion:nil];

同时,实现UIImagePickerDelegate中的如下方法:

1
2
3
4
5
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage * originImage = info[UIImagePickerControllerOriginalImage];//这样获取的是原图像
UIImage * editedImage = info[UIImagePickerControllerEditedImage];//这样获取的是修改后的图像
//...在这里实现图像的保存
}

如果头像的来源为相册,则只需修改imagePickerController的sourceType属性即可:

`imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;`

这样做就可以实现我们的需求了,但是这个方法的缺点也很明显,在这个方法中,我们使用的是系统封装好的UI,参考手机版QQ的选择头像我们可以发现,我们不能自定义相册中图片的分类、排版。同样的,我们也不能自定义编辑头像的界面,你不能将正方形的头像选框改成圆形的。因此,我们需要自己实现一个“UIImagePickerController”。在这个UIImagePickerController中,除了图片来源为拍照的情况下我们采用系统原生的UIImagePickerController以外,编辑图片和从相册中选取图片的功能我们都自己实现。

- PhotoKit和UICollectionView


参考博客:iOS 开发之照片框架详解

对于从相册选择图片的功能,我们可以采用PhotoKit来获取到图片的信息,并通过UICollectionView加以展示,这里主要给出PhotoKit的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Photos/Photos.h>

PHFetchOptions *options = [[PHFetchOptions alloc]init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult * results = [PHAsset fetchAssetsWithOptions:options];
//...这样就取得了照片库中所有照片并将它们通过创建时间排序 当然这只是最简单的做法 还可以创建多个不同类别的相册

//...然后我们要通过如下的代码取得照片
PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc]init];
requestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;//这里指定了照片的清晰度
[[PHImageManager defaultManager]requestImageForAsset:results[number]
targetSize:CGSizeMake(length, length)//指定了照片的大小
contentMode:PHImageContentModeDefault
options:requestOptions resultHandler:^(UIImage * result, NSDictionary * info) {
//...在这里展示照片
}];

- 自定义编辑头像的界面


主要有两个方面

  • 展示图片 我采用的方法是向一个UIScrollView中添加一个UIImageView
  • 圆形选框 采用了两个CAShapeLayer,一个是选框的Layer,另一个是遮罩的Layer

实现展示图片的功能首先需要了解UIScrollView中frame,contentSize和contentOffset的区别。同时,为了将图片限制在我们所画的边框内,关键是设置合理的minimumZoomScale(最小缩放比例)以及实时更新ContentSize,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (void)imageConfiguration {
self.imageView.frame = CGRectMake(WIDTH/2-LENGTH/2, HEIGHT/2-LENGTH/2-32, _imageSize.width, _imageSize.height);
//设置图片的位置
CGFloat zoomScale =[self zoomScaleWithSize:_imageSize];
//计算初始的zoomScale
CGFloat minimumZoomScale = [self minimumZoomScaleWithSize:_imageSize];
//计算最小的zoomScale
CGFloat maximumZoomScale = (minimumZoomScale <= 1.2) ? 1.2 : minimumZoomScale;
//这里的1.2可以替换成其他值
self.scrollView.minimumZoomScale = minimumZoomScale;
self.scrollView.maximumZoomScale = maximumZoomScale;
self.scrollView.zoomScale = zoomScale;
self.scrollView.contentSize = CGSizeMake(_imageSize.width*zoomScale+WIDTH-LENGTH, _imageSize.height*zoomScale+HEIGHT-LENGTH-64);
//为了保证将图片限制在边框内初始的ContentSize
self.scrollView.contentOffset = CGPointMake(WIDTH/2-LENGTH/2, HEIGHT/2-LENGTH/2-32);
//WIDTH是屏幕宽度 HEIGHT是屏幕高度 LENGTH是边框变长
}

- (CGFloat)zoomScaleWithSize:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
CGFloat scale = (CGFloat)width/height;
CGFloat screenScale = (CGFloat)WIDTH/HEIGHT;
if (scale > screenScale) {
if ((CGFloat)WIDTH/width*height>LENGTH) {
return ((CGFloat)WIDTH / width);
} else {
return (LENGTH/height);
}
} else {
if ((CGFloat)HEIGHT/height*width>LENGTH) {
return ((CGFloat)HEIGHT/height);
} else {
return (LENGTH/width);
}
}
}
//思路是通过比较宽高比确定让图片正好容纳在屏幕内的比例,同时还保证图片能够包括边框

- (CGFloat)minimumZoomScaleWithSize:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
if ((LENGTH / height)>(LENGTH / width)) {
return (LENGTH / height);
} else {
return (LENGTH / width);
}
}
//只保证图片能够包括边框即可

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
self.scrollView.contentSize = CGSizeMake(_imageSize.width*self.scrollView.zoomScale+WIDTH-LENGTH, _imageSize.height*self.scrollView.zoomScale+HEIGHT-LENGTH-64);
}
//UIScrollView代理 在缩放时能够正确的更新contentSize

圆形选框则比较简单,用一个CAShapeLayer即可实现,实现阴影遮罩(效果可以参考手机版QQ)则需要另一个CAShapeLayer,这里有一个问题,如果直接把遮罩设在self.view上,将会在页面切换的时候造成动画的不自然,我的解决方法是在self.view上添加一个UIView的实例contentView作为容器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
self.contentView.layer.mask = self.maskLayer;
[self.contentView.layer insertSublayer:self.borderLayer above:self.imageView.layer];

- (CAShapeLayer *)borderLayer {
if (!_borderLayer) {
//...在此设置边框
}
return _borderLayer;
}

- (CAShapeLayer *)maskLayer {
if (!_maskLayer) {
_maskLayer = [CAShapeLayer layer];
[_maskLayer setFrame:CGRectMake(0, 0, WIDTH, HEIGHT)];
[_maskLayer setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:0.5].CGColor];
//确定阴影的明暗
//...setPath确定无阴影的范围
}
return _maskLayer;
}

问题

- 修改导航栏的高度


1
2
3
4
5
6
7
8
9
10
11
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
CGRect navRect = self.navigationController.navigationBar.frame;
self.navigationController.navigationBar.frame = CGRectMake(navRect.origin.x, navRect.origin.y, navRect.size.width, 64);
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
CGRect navRect = self.navigationController.navigationBar.frame;
self.navigationController.navigationBar.frame = CGRectMake(navRect.origin.x, navRect.origin.y, navRect.size.width, 44);
}

- 调整导航栏中Title和Button的垂直位置


Title:

1
2
3
4
5
6
7
8
9
10
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController.navigationBar setTitleVerticalPositionAdjustment:-10.0 forBarMetrics:UIBarMetricsDefault];
//-10.0可以根据需要调节
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController.navigationBar setTitleVerticalPositionAdjustment:0.0 forBarMetrics:UIBarMetricsDefault];
}

Button:

1
2
[self.barButtonItem setBackgroundVerticalPositionAdjustment:-10.0 forBarMetrics:UIBarMetricsDefault];
//-10.0可以根据需要调节

- 隐藏状态栏


1
2
3
- (BOOL)prefersStatusBarHidden {
return YES;
}

- UIScrollView不能缩放


需要实现代理方法- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView指定需要缩放的子控件

- 截取缩放过的UIImageView中的特定位置和大小的图片


1
2
3
4
5
6
7
8
9
10
- (UIImage *)imageWithZoomScale:(CGFloat)zoomScale {
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, zoomScale);
[self.imageView drawViewHierarchyInRect:self.imageView.bounds afterScreenUpdates:YES];
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGRect imageRect = //...在这里指定位置和大小
CGImageRef sourceImageRef = [image CGImage];
CGImageRef targetImageRef = CGImageCreateWithImageInRect(sourceImageRef, imageRect);
return [UIImage imageWithCGImage:targetImageRef];
}

评论和共享

iOS学习笔记(二)

发布在 iOS开发

因为一些原因,用了整整一周的时间来重构项目,因为期末考试的原因,决定暑假继续这个项目,在复习之前最后总结一下这一段开发过程中遇到的一些问题。

定位问题


参考博客:后台定位上传的代码实践|里脊串的开发随笔

需求是每隔一定时间向服务器上传一次地理位置,而不管用户或系统是否杀死了APP。这里参考了里脊串博客里的写法,使用BackgroundMode中的Location updates即可实现。但是与这篇博客中的APP的需求不同,并不需要考虑速度距离等因素,只要考虑时间即可,因此可以通过判断两次location中的时间戳间隔即可。

1
2
3
4
5
#define kUpdateTimeInteval 60.0 //上传间隔
@interface LocationManager()
@property (nonatomic, strong) CLLocation * lastLocation; //最新一次的地理位置
@property (nonatomic) double timeInteval; //上一次上传地理位置时的时间戳
@end

然后在locationManager的委托方法中加入如下代码:

1
2
3
4
5
6
7
8
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
_lastLocation = locations[0];
if (_lastLocation.timestamp.timeIntervalSince1970 - _timeInteval > kUpdateTimeInteval) {
NSLog(@"update location");
[self updateLocation]; //上传地理位置
_timeInteval = _lastLocation.timestamp.timeIntervalSince1970;
}
}

需要注意的是,在进入后台模式或者是APP被杀的情况下,如果设备的位置没有发生变化,那么locationManager的 didUpdateLocations方法将不会被调用,因此尽管间隔的时间已经超过了设定的时间,地理位置也不会被上传。

不显示动画


由于APP中存在用户注册和登录的需求,因此需要加入验证码认证,两次验证码之间应有一分钟的间隔,此时,UI应该每隔1s刷新一次剩余时间以提醒用户,因此我使用了一个每隔1s执行一次的NSTimer去刷新UI,却发现每次刷新的时候按钮上的Title都会有渐入渐出的动画效果。后来发现这是UIButtonTypeRoundedRect的自带动画效果,为了不让这个渐变动画干扰了UI的刷新,加入如下代码:

1
2
3
4
[UIView performWithoutAnimation:^{
//在这里设置UIButton的属性如Text等
[_button.layer layoutIfNeeded];
}];

自定义UIBarButtonItem


参考了StackOverFlow

需求是自定义一个同时带图片和文字的UIBarButtonItem,查阅文档和搜索后发现UIBarButtonItem有以下的方法:

- (instancetype)initWithCustomView:(UIView *)customView;

因此我们可以先实例化一个UIButton,给这个UIButton添加Image并且设置Title,再用这个UIButton去实例化一个UIBarButtonItem即可,代码如下:

1
2
3
4
5
6
7
8
UIButton * button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"返回" forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:17.0];
[button setImage:[UIImage imageNamed:@"back"] forState:UIControlStateNormal];
button.imageEdgeInsets = UIEdgeInsetsMake(0, -3, 0, 0); //使用这个属性控制UIImageView和UILabel之间的距离
[button sizeToFit]; //设定button的frame
[button addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem * leftButton = [[UIBarButtonItem alloc]initWithCustomView:button];

但是这样又发现一个新的问题,我们无法控制UIBarButtonItem的边距。官方文档UINavigationItem中对于leftBarButtonItems和rightBarButtonItems这两个属性中有如下描述:

leftBarButtonItems are placed in the navigation bar left to right with the first item in the list at the left outside edge and left aligned.

rightBarButtonItems are placed right to left with the first item in the list at the right outside edge and right aligned.

因此我们可以再实例化一个UIBarButtonItem并设置它的width,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
UIBarButtonItem * negativeSpacer = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
negativeSpacer.width = -16.0; //可根据实际需要调整
UIButton * button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"返回" forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:17.0];
[button setImage:[UIImage imageNamed:@"back"] forState:UIControlStateNormal];
button.imageEdgeInsets = UIEdgeInsetsMake(0, -3, 0, 0);
[button sizeToFit];
[button addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem * leftButton = [[UIBarButtonItem alloc]initWithCustomView:button];
self.navigationItem.leftBarButtonItems = @[negativeSpacer, leftbutton];

去除UINavigationBar的阴影


我采用的是如下的方法:

[nav.navigationBar setShadowImage:[[UIImage alloc]init]];

评论和共享

iOS学习笔记(一)

发布在 iOS开发

最近开始了自己的第一个项目,到目前为止已经写完了基本的界面,将开发过程中遇到的一些问题列举如下:

自定义cell分割线

首先设置 tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

然后在自定义的cell的实现文件内实现如下的方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)drawSeparator{
_separatorlineLayer = [CAShapeLayer layer];
CGMutablePathRef separatorShapePath = CGPathCreateMutable();
[_separatorLayer setFillColor:[UIColor clearColor].CGColor];
[_separatorLayer setStrokeColor:[UIColor headlineColor].CGColor];
_separatorLayer.lineWidth = 0.5f;
CGPathMoveToPoint(separatorShapePath, NULL, 0.0f, 0.0f);
CGPathAddLineToPoint(separatorShapePath, NULL, WIDTH, 0.0f);
[_headlineLayer setPath:separatorShapePath];
CGPathRelease(separatorShapePath);
[self.contentView.layer addSublayer:_separatorLineLayer];
}

其中,_separatorLayerCAShapeLayer的实例,setStrokeColor方法设定了线的颜色,CGPathMoveToPointCGPathAddLineToPoint设定了线的起始点。

同理,可以类似地实现画点或者更为复杂形状的效果。

iOS8之后cell的动态高度计算

首先使用autolayout布局,且确保cell的contentView至少top和bottom都和cell内部的View建立了约束。之后添加如下代码

1
2
tableView.estimatedRowHeight = 60.0f;
tableView.rowHeight = UITableViewAutomaticDimension;

需要注意的是,必须要为tebleView的estimiatedRowHeight属性设定值。

返回键盘的高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)viewDidLoad{
[super viewDidLoad];
//注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}

- (void)dealloc {
//移除通知
[[NSNotificationCenter defaultCenter]removeObserver:self];
}

- (void)keyboardWillShow:(NSNotification *)notification{
//键盘弹出时调用
NSDictionary * userInfo = [notification userInfo];
NSValue * frameValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];
CGRect keyboardRect = [frameValue CGRectValue];
NSInteger height = keyboardRect.size.height;
}

- (void)keyboardWillHide:(NSNotification *)notification{
//键盘隐藏时调用
}

height中储存的即为当前键盘的高度

小心block导致的retain cycle

如果在block中需要访问本类的实例变量,需要使用

__weak UIViewController * weakSelf = self;

self持有了block,而block通过捕获self来访问实例变量,导致保留换的产生,通过__weak打破保留环。

评论和共享

  • 第 1 页 共 1 页
作者的图片

码龙黑曜

iOS开发者/计算机科学/兽人控


华中科技大学 本科在读


Wuhan, China