UIColor转UIImage

没啥好说的,直接看代码

⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇

.h头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface UIImage (Color)

/**
* 将UIColor转换为UIImage且大小为1x1
*
* @param color 图片颜色
*
* @return UIImage
*/
+ (nullable instancetype)imageWithColor:(UIColor * _Nonnull)color;

/**
* 将UIColor转换为UIImage并指定大小
*
* @param color 图片颜色
* @param size 图片大小
*
* @return UIImage
*/
+ (nullable instancetype)imageWithColor:(UIColor * _Nonnull)color size:(CGSize)size;

@end

.m文件

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
@implementation UIImage (Color)

/**
* 将UIColor转换为UIImage且大小为1x1
*
* @param color 图片颜色
*
* @return UIImage
*/
+ (instancetype)imageWithColor:(UIColor *)color {
return [self imageWithColor:color size:CGSizeMake(1.0, 1.0)];
}

/**
* 将UIColor转换为UIImage并指定大小
*
* @param color 图片颜色
* @param size 图片大小
*
* @return UIImage
*/
+ (instancetype)imageWithColor:(UIColor *)color size:(CGSize)size {
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, (CGRect){CGPointZero, size});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}


@end

UITextView禁用回车符

最近项目有个需求,需要在UITextView中禁止用户输入回车符,因为后台不会过滤回车符号,所以当AFNetworking请求数据时,如果返回的数据中包含了回车符,那么JSON解析将会失败。既然后台不处理,那没办法,只能在UITextView上禁用回车符了。

我们需要解决以下问题:

  1. 禁止通过键盘的return键输入回车符
  2. 粘贴文本时过滤掉文本中的回车符

对于问题1,我们可以通过重写- (void)insertText:(NSString *)text方法,判断text是否为\n即可;
对于问题2,当用户点击Paste按钮时,系统会调用UITextView的- (void)paste:(id)sender方法,我们可以从此处下手。

下面附上示例代码

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
@interface XPTextView : UITextView

/// 是否禁用回车符
@property (nonatomic, assign, getter=isDisableReturnCharacter) BOOL disableReturnCharacter;

@end


@implementation XPTextView

- (void)insertText:(NSString *)text {
if (_disableReturnCharacter && [text isEqualToString:@"\n"]) {
// 禁止输入回车符
[self resignFirstResponder];
return;
}
[super insertText:text];
}

- (void)paste:(id)sender {
if (_disableReturnCharacter) {
NSString *text = [[UIPasteboard generalPasteboard] string];
if ([text containsString:@"\n"]) {
// 需要从粘贴的字符串中把回车符过滤掉
text = [[text componentsSeparatedByString:@"\n"] componentsJoinedByString:@""];
[self replaceRange:self.selectedTextRange withText:text];
return;
}
}
[super paste:sender];
}

@end

下面安利一下我写的XPTextView,不仅可以禁用回车符,还可以设置占位字符:

👉源码戳这里👈

IB_DESIGNABLE引起Xcode编写代码时异常卡顿

最近升级了Xcode9之后,发现Xcode经常性卡顿,菊花常现,已经严重影响到代码的编写了,一开始还以为是Xcode9的问题,后来在网上搜索了下,原来老版本的Xcode就存在这个问题,而且是由于IB_DESIGNABLE这个系统宏导致的。

当我们在自定义视图的定义前面用了IB_DESIGNABLE这个宏来修饰,就表明了我们这个视图是可以在SB/XIB中实时预览的,本来这是一个很好用的功能,我们直接修改了视图代码就可以在SB上直接渲染出来,这真是所见即所得,简直棒到没朋友。但是这功能确实也坑了我们一把,当我们在编写代码时,只要修改了源码,哪怕只敲了一个字符,Xcode也会自动重新编译SB文件,这就很尴尬了,当我们不断敲代码的时候,Xcode就不断地重新编译SB文件,最后导致Xcode异常卡顿。

所以要解决这个卡顿问题,有两个方案:

  • 自定义视图去掉IB_DESIGNABLE宏修饰;
  • 告诉Xcode不自动编译SB文件,选中项目的SB文件,把Editor -> Automatically Refresh Views菜单项的勾去掉即可。

但是我之前用Xcode8的时候就没发现卡顿现象啊,真是够坑的

UITableViewCell长按拷贝菜单选项

运营推广人员向我们反馈说需要一个能方便拷贝会员手机号码的功能,方便进行其他操作。由于会员信息是采用UITableView来进行展示的,而系统已经为我们准备好了UITableViewCell长按功能菜单的API,我们只需实现对应的代理方法即可。

要实现该功能,主要步骤为:

  • 遵守UITableViewDelegate协议;
  • 实现- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath方法并返回YES;
  • 实现- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender方法,根据action决定对应的菜单按钮是否需要显示;
  • 实现- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender方法,根据action判断用户做出了什么操作。

具体实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return (indexPath.section == 1 && indexPath.row == 0);
}

- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (indexPath.section == 1 && indexPath.row == 0 && action == @selector(copy:)) {
return YES;
}
return NO;
}

- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
[[UIPasteboard generalPasteboard] setString:@"需要拷贝的内容"];
}
}

最后来一张效果图:

效果图

iOS11保存图片应用闪退

今天产品经理向我反馈在iOS11上点击保存图片后App闪退了,通过调试发现控制台输出了如下错误信息:

1
[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSPhotoLibraryAddUsageDescription key with a string value explaining to the user how the app uses this data.

根据控制台输出的错误日志,已经很明确知道原因所在了,那就是Info.plist中缺少了一个NSPhotoLibraryAddUsageDescription的键值对。打开Info.plist,点击+添加一个键值对,在Key中输入Privacy - Photo Library Additions Usage Description,Value类型为String并输入相应的提示语即可。

你也可以直接以Source Code方式打开Info.plist文件,然后复制以下内容并粘贴到Info.plist中:

1
2
<key>NSPhotoLibraryAddUsageDescription</key>
<string>此App需要您的授权才能保存图片</string>

iOS11中新增了以下的Key:

  • NFCReaderUsageDescription
  • NSFaceIDUsageDescription
  • NSPhotoLibraryAddUsageDescription

如果你的App使用到了NFC、FaceID、保存图片的功能,记得添加相应的Key。

注意:

  • iOS10上图库的访问和写入权限依然依赖于NSPhotoLibraryUsageDescriptionKey,如果在iOS10上只添加了NSPhotoLibraryAddUsageDescription而没有添加NSPhotoLibraryUsageDescription,当访问图库或者保存图片时应用依然会发生Crash
  • iOS11上只要添加了NSPhotoLibraryAddUsageDescription即具备图库的访问和写入权限,无需增加NSPhotoLibraryUsageDescription
  • iOS11上即使不添加NSPhotoLibraryUsageDescriptionNSPhotoLibraryAddUsageDescription也可以通过UIImagePickerController来访问图库(其他API方式未测试,真机和模拟器均可访问到图库数据),也不知道是否是Bug,居然不需要申请权限就可以访问系统图片。

综上所述,如果App需要兼容iOS10,则添加NSPhotoLibraryUsageDescriptionNSPhotoLibraryAddUsageDescription,如果只兼容iOS11则只添加NSPhotoLibraryAddUsageDescription即可。

有关Key的说明请参考这里:
https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html

iOS添加文件共享

由于iOS的沙盒机制,导致应用不能访问沙盒目录以外的文件目录。所以如果我们需要将音视频文件拷贝到App上,只能拷贝到App的Documents目录
如果我们的App需要支持从PC将文件拷贝到App的Documents目录并展示出来,则需要在Info.plist中添加UIFileSharingEnabled(Application supports iTunes file sharing)键,并将键值设置为YES

添加了UIFileSharingEnabled键值后就可以通过iTunes来管理共享文件了,可以从PC拷贝到App,也可以从App拷贝到PC上,也可以通过iTunes删除App上的文件(选中需要删除的文件,按下键盘的delete键即可)。

App可以通过访问Documents目录来获取需要关心的文件,过滤掉无关文件。

1
2
3
4
5
6
7
8
if let document = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
if let files = try? FileManager.default.contentsOfDirectory(atPath: document) {
for file in files {
// 可以通过后缀名来获取你关心的文件类型
print(file)
}
}
}

有关File Sharing请参考:https://support.apple.com/zh-cn/HT201301

UITableView动态修改tableHeaderView的高度

有时候我们会用表格的头部视图来做一些简单的UI展示,如果遇到高度固定的情况还好,并无任何问题。但是如果遇到高度需要动态调整的时候,那就有点蛋疼了,因为你不能通过直接修改tableHeaderView的frame属性来达到效果。直接修改frame会导致UI有时候正常有时候又不正常,完全把控不了。

那么我们该如何解决这个问题呢,主要有以下几步:

  1. 先取出UITableView的tableHeaderView

  2. 将tableHeaderView从父视图移除并将UITableView的tableHeaderView设置为nil

  3. 更新tableHeaderView的frame

  4. 重新给UITableView的tableHeaderView赋值

演示代码如下:

1
2
3
4
5
6
7
UIView *tableHeaderView = self.tableView.tableHeaderView;
CGRect frame = tableHeaderView.frame;
[tableHeaderView removeFromSuperview];
self.tableView.tableHeaderView = nil;
frame.size.height = 100.0; // 新高度
tableHeaderView.frame = frame;
self.tableView.tableHeaderView = tableHeaderView;

Objective-C/Swift退出多层循环

如果有多层嵌套的情况下,有时候我们需要在某处直接退出多层循环,在Objective-C下并没有比较好的方式实现;而在Swfit中可以通过标签语句来比较优雅地实现,首先需要使用标签标识不同的循环体,形式如下:

1
2
3
LabelName: while condition {
statements
}

Swift退出多层循环示例代码:

1
2
3
4
5
6
7
8
9
let arr = [[1,2], [3,4], [5,6]]
for1: for items in arr {
for2: for item in items {
print(item)
if item == 3 {
break for1
}
}
}

通过break for1即可退出最外层循环,控制台将依次打印1、2、3

说完了Swift,下面来说说Objective-C下如何实现这种操作。Objective-C既不支持标签语句,也没有类似PHP中break 2这种黑科技,但是Objective-C是C的超集,而C语言提供了强大的goto语句,虽然很多人都不建议使用goto,但是既然存在就必然有它存在的道理,是不?针对这种情况,goto就很适合。但是还是强烈建议你谨慎使用goto,除非你清楚你的代码在干什么。

Objective-C退出多层循环示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSArray<NSArray<NSString *> *> *items = @[@[@"1",@"2"], @[@"3",@"4"], @[@"5",@"6"]];
for (NSArray<NSString *> *array in items) {
for (NSString *item in array) {
NSLog(@"%@", item);
if ([item intValue] == 3) {
// 跳出外层循环
goto finally;
}
}
}

finally: {
NSLog(@"goto label.");
}

控制台还是会依次打印出1、2、3的值,并且当item的intValue等于3的时候跳出多层for循环,直接执行finally语句块内的代码。

使用goto时需要注意:goto语句只能在函数内部进行转移,不能够跨越函数;label代码块的定义位置。

goto语句使用的一般格式为:

1
2
3
4
5
goto label;
label: { statements }
或者
label: { statements }
goto label;

前一种定义方式参见上面的示例,后一种定义方式的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
int i = 0;
first: {
i++;
NSLog(@"%d", i);
}
// 代码执行到这里时i等于1,控制台输出1
if (i < 3) {
goto first;
}
NSLog(@"--end--");
// 控制台将输出1、2、3、--end--

iOS截屏分享

最近在把玩支付宝小米商城App时发现了个好玩的东西,在支付宝首页中截屏,则会在屏幕右侧弹出一个小窗口来展示首页截屏图片,点击图片就会进入求助反馈页面,再点击意见反馈,截屏的图片就会自动被选上作为反馈图片使用;而小米商城的做法则更好玩,在任意页面中进行截屏,则会在屏幕右下角中弹出一个分享截图给好友的提示框,点击则会弹出分享页面,在此可以看到我们刚才截取的屏幕图片,接下来你就可以把截屏图片分享给你的微信朋友、QQ好友、发到朋友圈、发微博等等,任君选择。

查了下资料,发现苹果在iOS7中新增了UIApplicationUserDidTakeScreenshotNotification通知,只要注册了这个通知,在用户截屏的时候App就会收到截屏的通知,接下来就可以做你想做的事情了。

1
2
// This notification is posted after the user takes a screenshot (for example by pressing both the home and lock screen buttons)
UIKIT_EXTERN NSNotificationName const UIApplicationUserDidTakeScreenshotNotification NS_AVAILABLE_IOS(7_0);

下面我们自己来撸一个自己的截屏分享功能。打开首页控制器,重写viewDidAppear和viewDidDisappear方法。因为我们只需要在首页中进行截屏分享,在其他页面截屏时不处理,所以我们在viewDidAppear方法中注册通知并且在viewDidDisappear方法中移除通知。

示例中通过[UIApplication sharedApplication].keyWindow来获取截屏图片,因为如果App中存在导航栏/TabBar的情况下,只有通过keyWindow生成的图片才能包含导航栏/TabBar,如果你仅仅想分享首页内容,不包含导航栏/TabBar的话,那你也可以直接用首页控制器的self.view来生成截屏图片,甚至如果你想分享一个长图片,如:首页是一个滚动视图,那么你完全可以通过滚动视图来生成一个长图片然后进行分享,这个完全可以根据你的需求来决定。

自己生成的截屏图片有一点很遗憾,就是生成的图片并不能包含顶部状态栏内容,支付宝和小米商城都有这个问题,这个应该是没辙了。或许只能去系统相册拿图片了,这个你就自己去测试吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDidTakeScreenshotNotification:) name:UIApplicationUserDidTakeScreenshotNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil];
}

- (void)userDidTakeScreenshotNotification:(NSNotification *)sender {
UIWindow *window = [UIApplication sharedApplication].keyWindow;
UIImage *screenshotImage = [UIImage snapshotImageWithView:window];
// TODO: 将screenshotImage进行分享,可以调用友盟SDK或自己集成第三方SDK实现,这里就不做演示了
}

下面附上从UIView获取图片的代码:

Objectie-C版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 通过视图获取视图快照
*
* @param view 需要生成快照的视图
*
* @return 快照图片
*/
+ (instancetype)snapshotImageWithView:(UIView * __nonnull)view {
if (nil == view) {
return nil;
}
UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[view.layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return image;
}

Swift版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension UIImage {

/// 获取视图快照图片
/// 必须在主线程上执行
///
/// - Parameter view: 需要生成快照的视图
/// - Returns: 快照图片
static func snapshot(_ view: UIView) -> UIImage? {
if !Thread.isMainThread {
assert(false, "UIImage.\(#function) must be called from main thread only.")
}
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, UIScreen.main.scale)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else { return nil }
view.layer.render(in: context)
return UIGraphicsGetImageFromCurrentImageContext()
}

}

UICollectionView监听reloadData完成状态

如果你只想知道代码的正确使用姿势,请直接看文章底部的示例代码。

有时候我们需要在UICollectionView执行了reloadData方法后,监听reloadData的完成状态以便执行某些回调操作。如果我们紧跟着reloadData代码,在其后面继续编写我们的回调操作,这个时候你会发现程序的运行结果与我们的预期有出入,并非是我们所期望的结果。

最近在撸一个图片轮播的功能,用UICollectionView来做,因为UICollectionView本身就支持横向滚动,并且有很好的重用机制,也不用我们自己计算上一页下一页然后将视图挪来挪去的。由于要做一个无限滚动的功能,所以做法就是在- (NSInteger)collectionView:numberOfItemsInSection:方法中返回图片个数x基数(如:100)的总数,然后将UICollectionView滚动至第图片个数x基数x0.5个Cell,这样便可以左右无限滑动了,问题的关键就在于如何在realoadData完成之后滚动到指定的IndexPath上。如果你直接像下面这样写代码,你会惊讶地发现,UICollectionView并没有滚动到指定的IndexPath,而是仍然显示第一个Cell。

1
2
3
4
[collectionView reloadData];
// 在这里继续编写你的特定代码
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:100 inSection:0];
[collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionNone animated:NO];

UICollectionView有一个方法-performBatchUpdates:completion:,其定义如下:

1
- (void)performBatchUpdates:(void (^ __nullable)(void))updates completion:(void (^ __nullable)(BOOL finished))completion; // allows multiple insert/delete/reload/move calls to be animated simultaneously. Nestable.

根据方法名和注释很容易明白这个接口是用来批量操作更新UICollectionView的,把你需要插入/删除/重新加载/移动UICollectionViewCell的代码统统放到updates这个block中,并且在当这些更新操作完成时系统会回调completion这个block。

看到这里,你是不是很激动,是不是已经想到该如何写代码了,是不是像下面这样:

1
2
3
4
5
6
// 错误的使用姿势
[collectionView performBatchUpdates:^{
[collectionView reloadData];
} completion:^(BOOL finished) {
NSLog(@"*************reloadData执行完毕*********");
}];

如果你是这样子写的代码,很好,和我想法一致,虽然程序有可能运行很正常,为什么说有可能呢,因为当[collectionView reloadData]之后,如果展示的数据并没有发生改变,数据未增加也未减少,那么此时程序就能够正常运行;但是如果执行realodData后,数据源返回的数据有所改变,需要展示的数据条目增多或减少了,那么这个时候程序就会Crash了,并且控制台会打印如下的错误信息:

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
2017-07-14 11:22:57.054 Demo[9368:620226] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections.  The number of sections contained in the collection view after the update (0) must be equal to the number of sections contained in the collection view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'
*** First throw call stack:
(
0 CoreFoundation 0x0000000109a9fb0b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010c770141 objc_exception_throw + 48
2 CoreFoundation 0x0000000109aa3cf2 +[NSException raise:format:arguments:] + 98
3 Foundation 0x000000010a9f13b6 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 193
4 UIKit 0x000000010dec753d -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:] + 15364
5 UIKit 0x000000010ded0259 -[UICollectionView _endUpdatesWithInvalidationContext:tentativelyForReordering:animator:] + 71
6 UIKit 0x000000010ded05a0 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:] + 437
7 UIKit 0x000000010ded03c8 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:] + 91
8 UIKit 0x000000010ded034a -[UICollectionView _performBatchUpdates:completion:invalidationContext:] + 74
9 UIKit 0x000000010ded029f -[UICollectionView performBatchUpdates:completion:] + 53
10 VBemall 0x00000001082da642 __31-[VBHomeController viewDidLoad]_block_invoke + 194
11 libdispatch.dylib 0x000000010f6aa05c _dispatch_client_callout + 8
12 libdispatch.dylib 0x000000010f686c6e _dispatch_continuation_pop + 1020
13 libdispatch.dylib 0x000000010f69b9fc _dispatch_source_latch_and_call + 230
14 libdispatch.dylib 0x000000010f6944f1 _dispatch_source_invoke + 1167
15 libdispatch.dylib 0x000000010f68b69a _dispatch_main_queue_callback_4CF + 1066
16 CoreFoundation 0x0000000109a64909 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
17 CoreFoundation 0x0000000109a2aae4 __CFRunLoopRun + 2164
18 CoreFoundation 0x0000000109a2a016 CFRunLoopRunSpecific + 406
19 GraphicsServices 0x0000000111ae5a24 GSEventRunModal + 62
20 UIKit 0x000000010d57c0d4 UIApplicationMain + 159
21 VBemall 0x00000001082da1ef main + 111
22 libdyld.dylib 0x000000010f6f665d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

看来是不能这样子用了,不能直接在updates这个block中执行reloadData方法。

然后我尝试了将reloadData放到block外,发现能够程序能够正常按照我们的预期运行了,代码如下:

1
2
3
4
5
6
// 代码的正确使用姿势
[collectionView reloadData];
[collectionView performBatchUpdates:nil completion:^(BOOL finished) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:100 inSection:0];
[collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}];

ps: UITableView在iOS11也引入了-performBatchUpdates:completion:这个接口,使用上应该也是和UICollectionView一样一样的。