28.LocalSubstitionCache 不生效
AMapLocation 和 LocalSubstitutionCache 有冲突,无法和后者同时使用。将 AMapLocation 换成苹果的 CoreLocation 即可。
29.如何将按钮图标置于按钮文本的右侧?
正常情况下,按钮图片(ImageView)位于按钮文本(titleLabel)的左侧。要将二者左右交换,有以下几种方法。其中,最简单的是第一个方法:
func flipButtonTitleImage(button:UIButton){// 将按钮图标置于文本右侧
button.transform = CGAffineTransformMakeScale(-1.0, 1.0);
button.titleLabel!.transform = CGAffineTransformMakeScale(-1.0, 1.0);
button.imageView?.transform = CGAffineTransformMakeScale(-1.0, 1.0);
}
或者修改二者的 Insets:
thebutton.titleEdgeInsets = UIEdgeInsetsMake(0, -thebutton.imageView.frame.size.width, 0, thebutton.imageView.frame.size.width);
thebutton.imageEdgeInsets = UIEdgeInsetsMake(0, thebutton.titleLabel.frame.size.width, 0, -thebutton.titleLabel.frame.size.width);
或者子类化 UIButton 并覆盖这两个方法:
@implementation UIButtonSubclass
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
CGRect frame = [super imageRectForContentRect:contentRect];
frame.origin.x = CGRectGetMaxX(contentRect) - CGRectGetWidth(frame) - self.imageEdgeInsets.right + self.imageEdgeInsets.left;
return frame;
}
- (CGRect)titleRectForContentRect:(CGRect)contentRect
{
CGRect frame = [super titleRectForContentRect:contentRect];
frame.origin.x = CGRectGetMinX(frame) - CGRectGetWidth([self imageRectForContentRect:contentRect]);
return frame;
}
@end
当然,最简单的还是第一个方法。
30.如何定制 JSQMessagesLoadEarlierHeaderView?
默认 JSQMessagesLoadEarlierHeaderView 显示一个按钮,标题为“Load Earlier Messages”。我们可以修改这个按钮,让它显示“加载更多历史消息”,如下图所示。
在 JSQMessagesViewController 子类中实现这个方法:
// 数据源方法:设置除了 cell 之外的 View,比如 HeaderView
- (UICollectionReusableView *)collectionView:(JSQMessagesCollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath
{
if (self.showLoadEarlierMessagesHeader && [kind isEqualToString:UICollectionElementKindSectionHeader]) {// 如果是 JSQMessagesLoadEarlierHeaderView
JSQMessagesLoadEarlierHeaderView *header = [collectionView dequeueLoadEarlierMessagesViewHeaderForIndexPath:indexPath];
// Customize header
[header.loadButton setTitle:@"加载更多历史消息" forState:UIControlStateNormal];
return header;
}
return [super collectionView:collectionView
viewForSupplementaryElementOfKind:kind
atIndexPath:indexPath];
}
31.如何定制 JSQMessagesToolbarContentView
在左边 voice 按钮前加入一个 add 按钮:
UIButton *btn=[UIButton buttonWithType:UIButtonTypeContactAdd];
JSQMessagesToolbarContentView *contentView= self.inputToolbar.contentView;
[contentView.leftBarButtonContainerView addSubview:btn];//add the new button
contentView.leftBarButtonItemWidth=66; //make containerView wider to hold more buttons
//remove unused leftBarButtonItem constraint
for (NSLayoutConstraint *constraint in contentView.leftBarButtonItem.superview.constraints) {
if(constraint.secondItem ==contentView.leftBarButtonItem &&constraint.firstAttribute ==NSLayoutAttributeLeading){
constraint.active=NO;
}
}
// update two button constraint
[contentView.leftBarButtonItem mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(@30);
}];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(btn.superview.mas_top);
make.bottom.equalTo(btn.superview.mas_bottom);
make.left.equalTo(btn.mas_left);
make.width.equalTo(@30);
}];
在右边 send 按钮前加入一个 add 按钮:
-(void)customInputToolbar{
UIButton *btn=[UIButton buttonWithType:UIButtonTypeContactAdd];
JSQMessagesToolbarContentView *contentView= self.inputToolbar.contentView;
[contentView.rightBarButtonContainerView addSubview:btn];//add the new button
contentView.rightBarButtonItemWidth=76; //make containerView wider to hold more buttons
//remove unused rightBarButtonItem constraint
for (NSLayoutConstraint *constraint in contentView.rightBarButtonItem.superview.constraints) {
if(constraint.secondItem ==contentView.rightBarButtonItem &&constraint.firstAttribute ==NSLayoutAttributeLeading){
constraint.active=NO;
}
}
// update two button constraint
[contentView.rightBarButtonItem mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(@40);
}];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(btn.superview.mas_top);
make.bottom.equalTo(btn.superview.mas_bottom);
make.right.equalTo(contentView.rightBarButtonItem.mas_left);
make.width.equalTo(@30);
}];
[btn addTarget:self action:@selector(addAction:) forControlEvents:UIControlEventTouchUpInside];
// 设置 voice 按钮和 send 按钮
[contentView.leftBarButtonItem setImage:[UIImage imageNamed:@"chat_bottom_voice_press"] forState:UIControlStateNormal];
[contentView.leftBarButtonItem setImage:[UIImage imageNamed:@"chat_bottom_voice_nor"] forState:UIControlStateHighlighted];
[contentView.rightBarButtonItem setTitle:@"发送" forState:UIControlStateNormal];
}
32.如何判断 TextView 中回车键按下?
如果是 UITextField,可以用 textFieldShouldReturn(_) 委托方法。但 UITextView 中却没有类似的委托方法,因此只能用 textView(_, shouldChangeTextInRange:, replacementText:) 方法替代:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
if !textView.text.isEmpty {
textView.resignFirstResponder()
... ... // 回车键按下,进行相应处理
}
return false;
}
return true;
}
O-C 代码:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if([text isEqualToString:@"\n"]) {
if( [textView.text length]>0){
[textView resignFirstResponder];
... ... // 回车键按下,进行相应处理
}
return NO;
}
return YES;
}
33. 如何检测 UITextView 的 contentSize 被改变?
contentSize 随着输入内容改变。如果你需要知道 UITextView 的 contentSize 何时变化时,使用如下方法。
首先,添加键值观察:
[self.inputToolbar.contentView.textView addObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
然后实现 observeValueForKeyPath 方法,检测 contentSize 变化:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.inputToolbar.contentView.textView
&& [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
CGSize oldContentSize = [[change objectForKey:NSKeyValueChangeOldKey] CGSizeValue];
CGSize newContentSize = [[change objectForKey:NSKeyValueChangeNewKey] CGSizeValue];
CGFloat dy = newContentSize.height - oldContentSize.height;
[self jsq_adjustInputToolbarForComposerTextViewContentSizeChange:dy];
[self jsq_updateCollectionViewInsets];
if (self.automaticallyScrollsToMostRecentMessage) {
[self scrollToBottomAnimated:NO];
}
}
}
但是,UIKit 中的 KVO 兼容并不是非常可靠,特别在 iOS 9 中。替代的方案是子类化 UITextView 并覆盖 setContentSize 方法。
34. 第三方库编译出现错误“Too many arguments to function call,expected 0,have 3”
例如,编译 nimbus 库时, NIButtonUtilities.m 中的 method(: : :)方法调用出现编译错误:Too many arguments to function call,expected 0,have 3
根据 2014 WWDC 视频中 Session 417 “What’s New in LLVM” 所述,编译器会强制要求对 objc_msgSend 使用强类型声明。例如:
typedef void (*send_type)(void*, SEL, void*);
send_type func = (send_type)objc_msgSend;
func(anItem.callback_object, NSSelectorFromString(anItem.selector), dict);
注意 sent_type 的使用。现在 objc_msgSend 变成了一个强类型。如果是调用实例方法,也是同样:
typedef void (*send_type)(void*, SEL, void*);
send_type methodInstance = (send_type)[SomeClass instanceMethodForSelector:someSelector];
methodInstance(self, someSelector, someArgument);
注意定义 send_type 类型时,返回值不一定是 void,你可以根据需要进行修改。
如果不想修改源代码,则可以修改编译选项,将编译器的这个特性关掉。在 targets 中找到 NimBus,进入 Build Settings,搜索 Enable strict checking of objc_msgsend calls,设置为 NO。
35. 在 lldb 中如何打印 indexPath.row?
当我们使用 lldb 命令 p indexPath.row 时,总是报错:error: property ‘row’ not found on object of type ‘NSIndexPath *’
其实,只需要换成如下命令即可:
(lldb) p (NSInteger)[indexPath section]
(NSInteger) $15 = 0
36. 如何用 CocoaHttpServer 实现一个 https 服务器?
继承 HTTPConnection 类:
@interface MyHTTPConnection : HTTPConnection
在 HTTPConnection
子类中,实现 - (BOOL)isSecureServer 方法并返回 YES,表示支持 https。
制作一个 SSL 自签名证书(见问题 ##37),安装到钥匙串,然后导出为 .p12 文件,记住导出密码。
制作证书时需要注意 /CN 参数必须指定主机名,比如 /CN 参数指定为 127.0.0.1,则访问时只能用 https://127.0.0.1,而不能用 https://localhost 替代。
实现 sslIdentityAndCertificates 方法:
- (NSArray *)sslIdentityAndCertificates
{
SecIdentityRef identityRef = NULL;
SecCertificateRef certificateRef = NULL;
SecTrustRef trustRef = NULL;
NSString *thePath = [[NSBundle mainBundle] pathForResource:@"127.0.0.1" ofType:@"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
CFStringRef password = CFSTR("你导出 .p12 的密码");
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = errSecSuccess;
securityError = SecPKCS12Import(inPKCS12Data, optionsDictionary, &items);
if (securityError == 0) {
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
identityRef = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
trustRef = (SecTrustRef)tempTrust;
} else {
NSLog(@"Failed with error code %d",(int)securityError);
return nil;
}
SecIdentityCopyCertificate(identityRef, &certificateRef);
NSArray *result = [[NSArray alloc] initWithObjects:(__bridge id)identityRef, (__bridge id)certificateRef, nil];
return result;
}
复制 HTTPConnetion.m 中的 startConnection 方法和 startReadingRequest 方法代码。
找到 startConnection 方法中如下语句:
[settings setObject:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(NSString *)kCFStreamSSLLevel]
替换为:
[settings setObject:[NSNumber numberWithInteger:2] forKey:GCDAsyncSocketSSLProtocolVersionMin];
[settings setObject:[NSNumber numberWithInteger:2] forKey:GCDAsyncSocketSSLProtocolVersionMax];
在启动 HTTPServer (调用 startServer )之前,使用 setConnectionClass 方法将 HTTPConnecdtion 替换为 MyHTTPConnection:
[httpServer setConnectionClass:[MyHTTPConnection class]];
37 如何制作 SSL 自签名证书?
1. CA 私钥
openssl genrsa -out myCA.key 2048
2. CA 证书
openssl req -x509 -new -key myCA.key -out myCA.cer -days 35600 -subj /CN="Yunnan Yuan Xin"
3. SSL 证书私钥
openssl genrsa -out myserver.key 2048
4. CSR
openssl req -new -out sslcert.req -key myserver.key -subj /CN=127.0.0.1
5. ssl 证书
openssl x509 -req -in sslcert.req -out sslcert.cer -CAkey myCA.key -CA myCA.cer -days 36500 -CAcreateserial -CAserial serial
6. 合并私钥及证书
openssl pkcs12 -export -out sslcert.pfx -inkey myserver.key -in sslcert.cer
38. 实现本地 HTTPs 服务器后,在 iOS 9 上无法访问
这是 iOS 9 上 App Transport Security 相关错误之一,iOS 9 以前没有这个问题。查看设备日志,发现错误 CFNetwork SSLHandshake failed (-9801)。
原因是服务器上的 TSL 版本低于 TSLv1.2。在 HTTPConnection 子类的 startConnection 方法中,将:
[settings setObject:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL
forKey:(NSString *)kCFStreamSSLLevel];
替换为:
[settings setObject:[NSNumber numberWithInteger:kTLSProtocol12] forKey:GCDAsyncSocketSSLProtocolVersionMin];
[settings setObject:[NSNumber numberWithInteger:kTLSProtocol12] forKey:GCDAsyncSocketSSLProtocolVersionMax];
或者在 Info.plist 中设置:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>localhost</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSTemporaryExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>服务器的TSL/SSL协议版本</string><!-- 或者 SSLv3.0、TLSv1.0、TLSv1.1 等-->
<key>NSRequiresCertificateTransparency</key>
<false/>
</dict>
</dict>
注意,这里“服务器的TSL/SSL协议版本”要替换成服务器真正的 TSL/SSL 版本,比如 SSLv3.0、TLSv1.0、TLSv1.1。