zoukankan      html  css  js  c++  java
  • xib的动态桥接

    本文原文发表自我的【自建博客】,cnblogs同步发表,格式未经调整,内容以原博客为准

    我是前言

    个人很主张使用Interface Builder(以下都简称IB)来构建程序UI,包括storyboardxib,相比代码更可视和易于修改,尤其在使用AutoLayout的时候,一目了然。
    但用了这么久IB之后发现一个很大的槽点,就是IB间很难嵌套混用,比如一个xib中的view是另一个xib的子view,或者一个storyboard中两个vc都用到了一个xib构建的view等。解决方法一般是代码手动拼接,这就造成了比较混乱的情况。
    本文将尝试解决这个问题,实现xib的动态桥接,并提供一个支持cocoapods的开源工具类供方便使用。

    一张图顶十句话:

    实现效果:


    黑魔法方法

    实现这个功能的关键在于:在ib加载的某个时刻将placeholder的view动态替换成从xib加载的view,下面的方法就可以做到:

    1
    
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder NS_REPLACES_RECEIVER;
    

    这个方法很少用到,在NSObject (NSCoderMethods)中定义,由NSCoder在decode过程中调用(于-initWithCoder:之后),所以说就算从文件里decode出来的对象也会走这个方法。
    方法后面有NS_REPLACES_RECEIVER这个宏:

    1
    
    #define NS_REPLACES_RECEIVER __attribute__((ns_consumes_self)) NS_RETURNS_RETAINED
    


    在clang的文档中可以找到对这个编译器属性的介绍

    > One use of this attribute is declare your own init-like methods that do not follow the standard Cocoa naming conventions.

    所以这个宏主要为了给编译器标识出这个方法可以像self = [super init]一样使用,并作出合理的内存管理。
    So,这个方法提供了一个机会,可以将decode出来的对象替换成另一个对象


    动态桥接流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
    {
        self = [super awakeAfterUsingCoder:aDecoder];
        
        // 0. 判断是否要进行替换
        // 1. 根据self.class从xib创建真正的view
        // 2. 将placeholder的属性、autolayout等替换到真正的view上
        // 3. return 真正的view
    }
    

    流程不难理解,就是有2个小难点:

    • 步骤1从xib创建真正的view时也会调用这个方法,会造成递归,如何判断
    • 迁移AutoLayoutConstrains

    解决递归问题

    这个topic全网可能就《这篇文章》有写,本文也是从它发起的,但是发现它的方法并不能解决所有问题(尤其是用storyboard加载xib时),所以换了个思路,采取了设置标志位的方式避免递归调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
    {
        self = [super awakeAfterUsingCoder:aDecoder];
        if (这个类的Loading标志位 -> NO)
        {
            Loading标志位 -> YES
            从xib加载真实的View (这里会递归调用这个函数)
            return 真实View
        }
        Loading标志位 -> NO
        return self
    }
    

    方法有点土,但是有效了,源代码文章后面会给地址。

    迁移AutoLayoutConstrains

    由于IB在加载AutoLayoutConstrains时的顺序是先加载子View内部的约束,后加载父View上的约束,而我们替换placeholder的时机是:

    1. placehodler view被创建(只带width,height的自身约束)
    2. 真正的view被从xib动态加载(带其子view的所有约束)
    3. placeholder被替换成真的view
    4. placeholder view在其父View(一直到父父父…View)的约束被创建

    所以说,迁移AutoLayout时,只需要把placeholder view的自身约束copy到真实View上就好了(停顿10s感受下)
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    - (void)replaceAutolayoutConstrainsFromView:(UIView *)placeholderView toView:(UIView *)realView
    {
        for (NSLayoutConstraint *constraint in placeholderView.constraints)
        {
            NSLayoutConstraint* newConstraint  = [NSLayoutConstraint constraintWithItem:realView
                                                         attribute:constraint.firstAttribute
                                                         relatedBy:constraint.relation
                                                            toItem:nil // Only first item
                                                         attribute:constraint.secondAttribute
                                                        multiplier:constraint.multiplier
                                                          constant:constraint.constant];
            newConstraint.shouldBeArchived = constraint.shouldBeArchived;
            newConstraint.priority = constraint.priority;
            [realView addConstraint:newConstraint];
        }
    }
    


    One more thing,保证AutoLayout生效还要加上下面这句话:

    1
    
    realView.translatesAutoresizingMaskIntoConstraints = NO;
    

    开源项目XXNibBridge

    光说方案不给源码还是不地道的,demo放到了我的github上面的XXNibBridge项目,回顾一下上面的关系图:

    不得不提到IB命名约定的最佳实践方案:

    将类名作为Cell或者VC的Reusable Identifier
    ReuseIdentifier一直比较蛋疼,我一般将Cell的类名作为ReuseIdentifier(当然,大多数情况我们都会子类化Cell的),写法如:

    1
    
    [self.tableView registerClass:[XXSarkCell class] forCellReuseIdentifier:NSStringFromClass([XXSarkCell class])];
    


    dequeueCell的时候同理,这样的好处在于省去了起名的恶心、通过ReuseId可以直接找到Cell类、同时重构Cell类名时ReuseId也不用去再改。

    View的xib与View的类名同名 同理

    实现了桥接Xib的功能的同时,也简单实现了这个命名约定:

    1
    2
    3
    4
    5
    
    // XXNibBridge.h
    + (NSString *)xx_nibID; // 类名
    + (UINib *)xx_nib; // 返回类名对应nib
    + (id)xx_loadFromNib; // 对应nib的类对象
    + (id/*UIViewController*/)xx_loadFromStoryboardNamed:(NSString *)name; // 返回类名对应的vc
    


    所以之后的代码可以这么写:

    1
    
    [tableView registerNib:[XXSarkView xx_nib] forCellReuseIdentifier:[XXSarkView xx_nibID]];
    

    XXNibBridge的使用

    Cocoapods安装

    1
    
    pod 'XXNibBridge', :git => 'https://github.com/sunnyxx/XXNibBridge.git'
    

    对于要支持Bridge的类,重载下面的方法:

    1
    2
    3
    4
    5
    6
    7
    
    #import "XXNibBridge.h"
    @implementation XXDogeView
    + (BOOL)xx_shouldApplyNibBridging
    {
        return YES;
    }
    @end
    

    在父的Xib或Storyboard中拖个UIView进来作为Placeholder,设置为真实Nib的类

    保证真实Nib的类名和Nib名相同,记得在Nib中设好Class

    Done.


    References

    http://blog.yangmeyer.de/blog/2012/07/09/an-update-on-nested-nib-loading
    http://stackoverflow.com/questions/19816703/replacing-nsview-while-keeping-autolayout-constraints
    http://clang-analyzer.llvm.org/annotations.html#attr_ns_consumes_self


    原创文章,转载请注明源地址,blog.sunnyxx.com

  • 相关阅读:
    最新iOS发布App Store详细图文教程~
    介绍一个轻量级iOS安全框架:SSKeyChain
    今天科普一下 苹果开发者账号中:个人、公司、企业账号的区别
    iOS开发:Framework的创建
    JMS 在 SpringBoot 中的使用
    iOS 引入外部字体 otf/ttf/ttc
    公司企业苹果开发者账号中个人、公司企业账号的不同
    Git版本管理
    有关苹果手机下载应用后提示不受信任的企业开发者解决方案:
    尝试一下markdown
  • 原文地址:https://www.cnblogs.com/sunnyxx/p/3824968.html
Copyright © 2011-2022 走看看