【iOS界面开发】视图开发技巧

UIWindow & UIWindowLevel1

一、UIWindow是一种特殊的UIView,通常在一个程序中只会有一个UIWindow,但可以手动创建多个UIWindow,同时加到程序里面。UIWindow在程序中主要起到三个作用:

  1、作为容器,包含app所要显示的所有视图

  2、传递触摸消息到程序中view和其他对象

  3、与UIViewController协同工作,方便完成设备方向旋转的支持

二、通常我们可以采取两种方法将view添加到UIWindow中:

  1、addSubview

  直接将view通过addSubview方式添加到window中,程序负责维护view的生命周期以及刷新,但是并不会为去理会view对应的ViewController,因此采用这种方法将view添加到window以后,我们还要保持view对应的ViewController的有效性,不能过早释放。

  2、rootViewController

  rootViewController时UIWindow的一个遍历方法,通过设置该属性为要添加view对应的ViewController,UIWindow将会自动将其view添加到当前window中,同时负责ViewController和view的生命周期的维护,防止其过早释放

三、WindowLevel

  UIWindow在显示的时候会根据UIWindowLevel进行排序的,即Level高的将排在所有Level比他低的层级的前面。下面我们来看UIWindowLevel的定义:

    const UIWindowLevel UIWindowLevelNormal;
    const UIWindowLevel UIWindowLevelAlert;
    const UIWindowLevel UIWindowLevelStatusBar;
    typedef CGFloat UIWindowLevel;

  IOS系统中定义了三个window层级,其中每一个层级又可以分好多子层级(从UIWindow的头文件中可以看到成员变量CGFloat _windowSublevel;),不过系统并没有把则个属性开出来。UIWindow的默认级别是UIWindowLevelNormal,我们打印输出这三个level的值分别如下:
2012-03-27 22:46:08.752 UIViewSample[395:f803] Normal window level: 0.000000
2012-03-27 22:46:08.754 UIViewSample[395:f803] Alert window level: 2000.000000
2012-03-27 22:46:08.755 UIViewSample[395:f803] Status window level: 1000.000000

  这样印证了他们级别的高低顺序从小到大为Normal < StatusBar < Alert。

  注意两点:

  1)如果在第一个默认的window之后生成另一个normalWindow,并调用makeKeyAndVisible,但仍然没有显示出来。这说明当Level层级相同的时候,只有第一个设置为KeyWindow的显示出来,后面同级的再设置KeyWindow也不会显示。

  2)statusLevelWindow在alertLevelWindow之后调用makeKeyAndVisible,仍然只是显示在alertLevelWindow的下方。这说明UIWindow在显示的时候是不管KeyWindow是谁,都是Level优先的,即Level最高的始终显示在最前面。

iOS触摸事件处理2

界面响应消息机制分两块,(1)首先在视图的层次结构里找到能响应消息的那个视图。(2)然后在找到的视图里处理消息。

寻找响应消息视图过程

寻找响应消息视图的过程可以借用M了个J的一张图来说明。

寻找响应消息视图的过程

处理原理如下:

• 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中

• UIApplication会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)

• 主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件

(hitTest:withEvent:其实是UIView的一个方法,UIWindow继承自UIView,因此主窗口UIWindow也是属于视图的一种)

• hitTest:withEvent:方法大致处理流程是这样的:

首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内:

▶ 若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil

▶ 若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕:

▷ 若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束

▷ 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)

• 最终,这个触摸事件交给主窗口的hitTest:withEvent:方法返回的视图对象去处理。

拿到这个UIView后,就调用该UIView的touches系列方法。

消息处理过程

在找到的那个视图里处理,处理完后根据需要,利用响应链nextResponder可将消息往下一个响应者传递。

UIAppliactionDelegate <- UIWindow <- UIViewController <- UIView <- UIView

【关键】:要理解的有三点:1、iOS判断哪个界面能接受消息是从View层级结构的父View向子View传递,即树状结构的根节点向叶子节点递归传递。2、hitTest和pointInside成对,且hitTest会调用pointInside。3、iOS的消息处理是,当消息被人处理后默认不再向父层传递。

视图转场

iOS提供了一些内置的转场类型。Navigation controllers用push和pop来有层次地导航信息,tab bar controllers用切换tabs来在各部分之间跳转,所有的视图控制器可以根据特定任务模态化地present和dismiss另一个视图控制器。

  • 对于push和pop,意味着调用UINavigationController的push-、pop-、或者set-方法来修改视图控制器的堆栈。

  • 对于切换tabs,意味着修改UITabBarController的selectedIndex或selectedViewController属性。

  • 对于modal,则意味着调用[UIViewController presentViewController: animated: completion: ]或[UIViewController dismissViewControllerAnimated: completion: ]。

调整视图布局的时机

当一个控件被初始化以及开始使用之后,它的 frame 仍然可能发生变化,我们也需要接受这些变化,因为你提供的是 UIView 的接口,UIView 有很多种初始化方式:initWithFrame、initWithCoder、init 和类方法 new,用户完全可以在初始化之后再设置 frame 属性,而且用户就算使用 initWithFrame 来初始化也避免不了 frame 的改变,比如在横竖屏切换的时候。为了确保当它的 Size 发生变化后其子视图也能同步更新,我们不能一开始就把布局写死(使用约束除外)。

    当从代码实例化 UIView 的时候,initWithFrame 会执行;
    当从xib/Storybord文件加载 UIView 的时候,initWithCoder 会执行。

基于 frame

如果你是直接基于 frame 来布局的,你应该确保在初始化的时候只添加视图,而不去设置它们的frame,把设置子视图 frame 的过程全部放到 layoutSubviews 方法里:

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];

    self.label.frame = CGRectInset(self.bounds, 20, 0);
}

- (void)commonInit {
    [self addSubview:self.label];
}

- (UILabel *)label {
    if (_label == nil) {
        _label = [UILabel new];
        _label.textColor = [UIColor grayColor];
    }
    return _label;
}

这么做就能保证 label 总是出现在正确的位置上。

使用 layoutSubviews 方法有几点需要注意:

    不要依赖前一次的计算结果,应该总是根据当前最新值来计算
    由于 layoutSubviews 方法是在自身的 bounds 发生改变的时候调用, 因此 UIScrollView 会在滚动时不停地调用,当你只关心 Size 有没有变化的时候,可以把前一次的 Size 保存起来,通过与最新的 Size 比较来判断是否需要更新,在大多数情况下都能改善性能

基于 Auto Layout 约束

如果你是基于 Auto Layout 约束来进行布局,那么可以在 commonInit 调用的时候就把约束添加上去,不要重写 layoutSubviews 方法,因为这种情况下它的默认实现就是根据约束来计算 frame。最重要的一点,把 translatesAutoresizingMaskIntoConstraints 属性设为 NO,以免产生 NSAutoresizingMaskLayoutConstraint 约束,如果你使用 Masonry 框架的话,则不用担心这个问题,mas_makeConstraints 方法会首先设置这个属性为 NO:

- (void)commonInit {
    ...
    [self setupConstraintsForSubviews];
}

- (void)setupConstraintsForSubviews {
    [self.label mas_makeConstraints:^(MASConstraintMaker *make) {
        ...
    }];
}

Masonry例子:

    [_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        // 1.
        make.top.equalTo(superView.mas_top);
        make.bottom.equalTo(superView.mas_bottom);
        make.left.equalTo(superView.mas_left);
        make.right.equalTo(superView.mas_right);

        // 2.
        make.top.and.bottom.and.left.and.right.equalTo(superView);

        // 3.
        make.edges.equalTo(superView);
    }];

AutoLayout的基本使用3

•在创建约束之后,需要将其添加到作用的view上。在添加时要注意目标view需要遵循以下规则:
•1) 对于两个同层级view之间的约束关系,添加到他们的父view上
•2) 对于两个不同层级view之间的约束关系,添加到他们最近的共同父view上
•3) 对于有层次关系的两个view之间的约束关系,添加到层次较高的父view上

支持 sizeToFit

Two modes for asking view’s size:

  1. Auto layout mode using “-systemLayoutSizeFittingSize:”

  2. Frame layout mode using “-sizeThatFits:”

如果你的控件对尺寸有严格的限定,比如有一个统一的宽高比或者是固定尺寸,那么最好能实现系统给出的约定成俗的接口。

sizeToFit 用在基于 frame 布局的情况下,由你的控件去实现 sizeThatFits: 方法:

- (CGSize)sizeThatFits:(CGSize)size {
    CGSize fitSize = [super sizeThatFits:size];
    fitSize.height += self.label.frame.size.height;
    // 如果是固定尺寸,就像 UISwtich 那样返回一个固定 Size 就 OK 了
    return fitSize;
}

然后在外部调用该控件的 sizeToFit 方法,这个方法内部会自动调用 sizeThatFits 并更新自身的 Size:

[self.customView sizeToFit];

在 ViewController 里调整视图布局

当执行 viewDidLoad 方法时,不要依赖 self.view 的 Size。很多人会这样写:

- (void)viewDidLoad {
    ...
    self.label.width = self.view.width;
}

这样是不对的,哪怕看上去没问题也只是碰巧没问题而已。当 viewDidLoad 方法被调用的时候,self.view 才刚刚被初始化,此时它的容器还没有对它的 frame 进行设置,如果 view 是从 xib 加载的,那么它的 Size 就是 xib 中设置的值;如果它是从代码加载的,那么它的 Size 和屏幕大小有关系,除了 Size 以外,Origin 也不会准确。整个过程看起来像这样:

当访问 ViewController 的 view 的时候,ViewController 会先执行 loadViewIfRequired 方法,如果 view 还没有加载,则调用 loadView,然后是 viewDidLoad 这个钩子方法,最后是返回 view,容器拿到 view 后,根据自身的属性(如 edgesForExtendedLayout、判断是否存在 tabBar、判断 navigationBar 是否透明等)添加约束或者设置 frame。

你至少应该设置 autoresizingMask 属性:

- (void)viewDidLoad {
    ...
    self.label.width = self.view.width;
    self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
}

或者在 viewDidLayoutSubviews 里处理:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    self.label.width = self.view.width;
}

如果是基于 Auto Layout 来布局,则在 viewDidLoad 里添加约束即可。

Constants.h

ORIGIN

Constants.h

#define kFilterDate @"date"
#define kFilterRadius @"radius"
#define kFilterSort @"sort"

//Global Strings
#define kDividingString @" / "

//Strings
#define kTour @"Tour"
#define kToursKey @"tours"

REVISION

more safe about our constants

Create your constants in your Constants.h file using const variables

//Filters
FOUNDATION_EXPORT NSString *const kFilterDate;
FOUNDATION_EXPORT NSString *const kFilterRadius;
FOUNDATION_EXPORT NSString *const kFilterSort;

//Global Strings
FOUNDATION_EXPORT NSString *const kDividingString;

//Strings
FOUNDATION_EXPORT NSString *const kTour;
FOUNDATION_EXPORT NSString *const kToursKey;

And in Constants.m

//Filters
NSString *const kFilterDate = @"date";
NSString *const kFilterRadius = @"radius";
NSString *const kFilterSort = @"sort";

//Global Strings
NSString *const kDividingString = @" / ";

//Strings
NSString *const kTour = @"Tour";
NSString *const kToursKey = @"tours";

猜你喜欢

转载自blog.csdn.net/freeWayWalker/article/details/50877467