Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

iOS 开发问与答(28-38)

$
0
0

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。

作者:kmyhy 发表于2017/1/22 13:27:44 原文链接
阅读:52 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>