iOS 原生基础

iOS开发初体验

工程准备

使用Xcode 11以上版本创建项目时,生成的工程会增加 scene 支持,其生命周期会发生较大改动。

iOS 13中引入了scene的概念,每一个scene对应一个UIWindow,UIWindow由UISceneDelegate管理。iOS 13之前一个APP也是可以创建多个UIWindow的,但只能有一个keyWindow。引入scene之后,每一个scene都会有一个keyWindow。原有的UIApplicationDelegate UI生命周期的几个方法将拆分到UISceneDelegate中。

简单说,就是Window(窗口)的概念已被Scene(场景)的概念所代替。 一个应用程序可以具有不止一个场景。

详细参见官方文档: Scenes

Scene的概念,主要是用来支持multiple windows(多窗口)功能,但目前多窗口仅在iPadOS上获得支持,对于iPhone意义不大,并且还要进行版本适配,因此删除Scene相关代码,回归通常的生命周期逻辑:

  1. 打开工程的info.plist,删除 Application Scene Manifest 配置:

  2. 删除工程中的Scenedelegate.h/.m文件

  3. 删除AppDelegate.m文件的configurationForConnectingSceneSession 和 didDiscardSceneSessions 两个方法:

  4. AppDelegate.h中添加window属性:

    @property (strong, nonatomic) UIWindow * window;
    

现在我们可以打开Main.storyboard文件,并拖拽一个UILabel,填入Hello,World

iOS的UI开发

首先要了解IB(Interface Builder)工具,它是一个图形化的、可拖拽的UI编辑器。Xcode 4.0以前,IB是一个独立软件,Xcode 4.0开始,重新设计,并直接集成到Xcode中。

iOS有三种UI开发方式:

  • 使用nibxib开发。

    在Xcode 3.0前,IB工具生成的界面描述文件是nib展名(NeXT Interface Builder),是一种二进制格式,不利于版本控制。从Xcode 3.0开始,IB使用了一种新的文件格式xib(XML Interface Builder),它是一种xml文本格式。xib文件会在工程编译时再转换成nib格式。通常一个xib文件对应一个ViewController,不过也可以使用xib文件来自定义View。xib开发的原理就是将xml文件解析出来,找到相应的view,转换成代码,然后创建对象并显示。

  • 使用StoryBoard开发。

    iOS 5开始提供了一种新的方式叫StoryBoard。它是一组xib的集合,并且描述了页面之间的关系。简单说,StoryBoard能将分散的xib汇总统一管理,并描述各种场景之间的过渡,这种过渡被称作 segue。StoryBoard 把 viewController称为scene,通过拖拽实现跳转过度,减少很多代码编写。StoryBoard本质上还是xml文件。

  • 使用纯代码开发。即手写代码开发界面。

StoryBoard存在的一些问题

  • 不适合以多人合作,大型项目较难维护
  • 实例化Storyboard上的ViewController十分麻烦,需要设置Storyboard ID
  • Segue中的各种操作都非常不直观,并且十分怪异

使用 xib 创建页面

  1. 删除工程中生成的ViewController.hViewController.mMain.storyboard三个文件。

  2. Deployment Info下的Main Interface清空(去掉Main)

  3. 新建视图控制器。 File > New > File ,选择:Cocoa Touch Class

  4. 弹出界面中勾选create XIB file

  5. 打开生成的xib文件,同StoryBoard一样拖拽控件,生成UI

  6. 修改AppDelegate.m,加载xib文件和视图控制器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 1.创建窗口
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    // 2.创建并设置根控制器
    RootViewController *rootVC = [[RootViewController alloc]init];
    self.window.rootViewController = rootVC;
    // 3.显示窗口
    [self.window makeKeyAndVisible];
    return YES;
}

注意:如果删除了Main.storyboard文件,并改变了Main Interface的设置,就相当于删除项目的主 window,直接运行项目会黑屏。因此,我们必须手动创建一个UIWindow作为主window:

 self.window = [[UIWindow alloc]initWithFrame:[[UIScreen mainScreen]bounds]];

使用纯代码创建页面

总体与使用xib类似,只是不使用xib文件。

创建一个视图控制器,添加代码

- (void)viewDidLoad {
    [super viewDidLoad];
    UILabel *lab = [[UILabel alloc]init];
    lab.frame = CGRectMake(100, 100, 100, 50);
    lab.text = @"hello Wolrd";
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:lab];
}

设计思想

MVC具体指的是:将图形化应用划分为Model、View和Controller共3个部分:

  • Model:负责数据动态管理和软件业务逻辑,接收来自controller的操作,并将结果及时传递给view

  • View:直接呈现在用户面前的界面,并处理交互

  • Controller:是两者的中介,协调ModelView相互协作

优点是3个部分各司其职,降低耦合度,一定程度上降低了软件复杂性,提高了应用的质量及开发效率。当我们在布局UI控件的时候,其实就已经在使用MVC的架构方式。UIViewController扮演的就是MVC中的Controller。

基本概念

窗口与视图控制器之间的关系:

iOS的UI框架结构:

视图控制器与视图之间的关系:

UIScreen

iOS设备有一个主屏幕和零个或多个附属屏幕。每个屏幕对象定义了相关显示器的边界矩形和其他有趣的属性,比如它的亮度。在iOS 8和更高版本中,屏幕的边界属性会考虑到屏幕的界面方向。这种行为意味着纵向的设备的边界可能与横向的设备的边界不一样。

// 返回带有状态栏的 Rect
CGRect screenBounds = UIScreen.mainScreen.bounds;

// 返回不含状态栏的 Rect
CGRect applicationBounds = UIScreen.mainScreen.applicationFrame;

UIWindow

它是所有UIView的根,管理和协调的应用程序的显示,它与视图控制器一起工作,以处理事件并执行许多其他任务。

iOS 程序启动后,创建的第一个视图控件就是 UIWindow(创建的第一个对象是 UIApplication),接着创建UIViewController(视图控制器)的 UIView,最后将视图控制器的 UIView添加到 UIWindow上,于是控制器管理的 UIView就显示在屏幕上了。一般情况下,应用程序只有一个 UIWindow对象,即使有多个,也只有一个 UIWindow可以接受到用户的触屏事件。

通常情况下,Xcode会提供应用程序的主窗口。新的iOS项目使用storyboard 来定义应用程序的视图。storyboard 要求应用程序代理对象上存在一个window 属性,Xcode模板会自动提供该属性。如果你的应用程序不使用故事板,则必须自己创建这个window。

注意,一个window 的所有可见内容均由其根视图控制器的view提供。

关于窗口与视图的详细指南,参见官方文档:About Windows and Views

UIViewController

视图控制器, MVC 设计模式中的控制器部分。

它是应用程序内部结构的基础。每个应用程序至少都有一个视图控制器,大多数应用程序都有多个。每个视图控制器管理用户界面的一部分以及该界面与基础数据之间的交互。视图控制器还有助于在用户界面的不同部分之间进行转换。iOS的视图控制器就相当于Android中的Activity

参考官方文档 视图控制器的作用

UIView

定义了屏幕上的一个矩形区域,同时处理该区域的绘制和触屏事件。可以在这个区域内绘制图形和文字,还可以接收用户的操作。一个UIView的实例可以包含和管理若干个子UIViewUIWindow本身继承自UIView,它是一个特殊的UIView

启动流程

UIApplicationAppDelegate管理UIWindowUIWindow通过UIViewController管理UIView

我们知道Objective-C的入口是main函数,它创建并维护了一个自动释放池,并调用了UIApplicationMain函数。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain会根据第三个参数创建一个UIApplication类或其子类的对象,并开始接受事件。默认情况传入nil,也就是使用UIApplication。然后根据第四个参数创建一个AppDelegate对象作为应用的代理,它被用来管理与应用声明周期相关的事件。

总的来说,main函数通过UIApplicationMain函数创建了UIApplication对象和AppDelegate代理,并建立了一个事件循环机制来捕获、处理用户的操作行为。

UIApplication

iOS应用启动后创建的一个对象就是UIApplication,它是一个单例,可以通过[UIApplication sharedApplication]方法获取。每个应用在运行期间必须有且只能有一个UIApplication对象,不同的App拥有不同的UIApplication对象。它主要负责该应用程序的控制和协调工作。

UIApplication的一个主要工作就是处理用户事件,它会创建一个事件队列,将所有事件放入其中,并负责将每个事件发送至最合适处理该事件的目标控件,例如用户的点击事件会被交由某个特定的Button去处理。

UIApplication中有声明了windows属性:

@property(nonatomic,readonly) NSArray<__kindof UIWindow *>  *windows;

该数组包括了与App关联的所有UIWindow实例,包括可见和不可见的,但不包括由系统创建和管理的窗口,例如状态栏窗口是与系统关联的。虽然数组中不包括状态栏窗口,但是可以通过系统提供的方法设置状态栏。

持有窗口列表的意义在于,所有控件是基于窗口显示的,所以有了每一个UIWindow对象,就可以接触到任何一个UIView对象。

UIApplication可以在应用层与系统进行交互,例如设置桌面图标的角标数字、设置状态栏、设置联网状态等等。

UIApplication会将系统事件交给AppDelegate对象处理,AppDelegate是App生命周期的管理者。

App生命周期

App主要有三种状态:

  • Active:程序在前台运行,正在接受事件
  • Inactive:程序在前台运行,但没有接受事件,在前后台过渡的过程中会进入此状态
  • Background:后台。程序在后台且能执行代码,大多数程序进入这个状态后会在此状态上停留一会儿。时间到了就会进入挂起状态(Suspended)。有的程序经过特殊的请求后可以长期处于Backgroud状态

所谓App生命周期,也就是在这几个状态中来回切换:

  • Not running:程序没启动
  • Suspended:程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。

生命周期过程:

AppDelegate.m中生命周期相关的几个方法:

// 程序装载完成,即将运行
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions

// 程序将要进入前台,通常会在这个时候恢复数据
- (void)applicationWillEnterForeground:(UIApplication*)application

// 程序已经获取焦点 - 可以交互
- (void)applicationDidBecomeActive:(UIApplication*)application

// 程序将要失去焦点,即不能交互
- (void)applicationWillResignActive:(UIApplication*)application

// 程序已经进入后台,一般在这个时候保存数据
- (void)applicationDidEnterBackground:(UIApplication*)application

// 程序将要完全退出,最好不要做事,因很多时候都会是异常退出,不执行该方法
- (void)applicationWillTerminate:(UIApplication*)application

// 程序接收到内存警告
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application

UIViewController

视图控制器采用懒加载方式,即第一次访问它的view属性时才会真正加载或创建它。视图控制器有三种加载方式:

  1. 通过Storyboard加载。首先会生成UIStoryboard类型对象,然后调用该对象的instantiateViewControllerWithIdentifier:方法返回UIViewController对象
  2. 通过xib文件加载。调用UIViewControllerinitWithNibName:bundle:方法返回对象
  3. 通过loadview方法加载。即在loadView 方法中,手动创建自己的视图控制器和视图,并把根视图赋值给该可控制器的view属性

视图控制器的生命周期:

loadView

loadView方法是主要负责创建视图控制器的view。这里的View指的是UIViewController的根视图,平时为应用添加的View都是这个View的子View

UIWindow本身也是UIView,它的subview包含了UIView,各个UIView之间的关系:

viewDidLoad

loadView方法执行完之后,就会执行viewDidLoad方法。此时整个视图层次已经被放到内存中。无论是从xib文件加载,还是通过纯代码编写界面,viewDidLoad方法都会执行。在控制器的生命周期中,它只会被调用一次,因此非常适合做一些页面的初始化任务。可以重写这个方法,对xib文件加载的view做一些其他的初始化工作。比如可以移除一些视图,修改约束,加载数据等。

viewWillAppear

在视图加载完成,并即将显示在屏幕上时,会调用viewWillAppear方法,在这个方法里,可以改变当前屏幕方向或状态栏的风格等。

viewDidAppear

viewWillAppear方法执行完后,系统会执行viewDidAppear方法。表示视图已经在屏幕上可见。

ViewWillDisAppear 和 viewDidDisAppear

ViewWillDisAppear表示视图即将移除;viewDidDisAppear执行表示视图已经移除

ViewWillUnload 和ViewDidUnload

从 iOS 6 开始这两个方法都已经废弃。 iOS 6 之前,当内存过低时,需要释放一些不需要使用的视图时,在即将释放时调用viewWillUnload

现在,可以通过重写 - (void)didReceiveMemoryWarning-(void)dealloc 来进行清理工作。

小结

还有两个方法viewWillLayoutSubviewsviewDidLayoutSubviews会在viewWillAppear之后执行,表示即将开始子视图位置布局和完成子视图布局。

总的来说,一个UIView显示到屏幕上经历了三个过程:

  1. 创建并关联:initaddSubView
  2. 布局:layout
  3. 绘制:draw

注意,有Storyboard的项目中创建流程会有些区别,过程如下:

当用户点击应用程序启动图标时,先执行main函数,如前面所述流程,然后执行UIApplicationMain,创建UIApplication和代理对象,并给UIApplication设置代理对象,接着加载项目配置文件info.plist,获取其中配置的storyboard的name值,根据name找到对应的storyboard文件,然后开启一个事件循环,在调用didFinishLaunchingWithOptions:方法之前加载storyboard,在加载的时候会创建一个window,接下来会创建箭头所指向的控制器,把该控制器设置为UIWindow的根控制器,最后再将window显示出来,即看到了运行后显示的界面。

参考官方文档:The Role of View Controllers

多视图控制器

一个 app 可能有很多的 UIViewController 组成,这时需要一个容器来对这些 UIViewController 进行管理。大部分的容器也是一个 UIViewController,如 UINavigationControllerUITabBarController等。

总的来说,iOS中切换控制器的方式大致有五种:

  1. UINavigationController
  2. UITabBarController
  3. 模态视图控制器
  4. segue跳转
  5. 自定义容器控制器(父子控制器)

UINavigationController

导航控制器也是一个容器视图控制器,它在一个导航界面中管理一个或多个子视图控制器。在这种类型的界面中,每次只有一个子视图控制器是可见的。选择视图控制器中的一个项目,会使用动画将一个新的视图控制器推到屏幕上,从而隐藏之前的视图控制器。点击界面顶部导航栏中的后退按钮,会移除顶部的视图控制器,从而露出下面的视图控制器。

导航控制器对象使用一个有序的数组来管理它的子视图控制器,这被称为导航栈。数组中的第一个视图控制器是根视图控制器,代表栈的底部。数组中的最后一个视图控制器是堆栈中最顶端的项目,代表当前正在显示的视图控制器。

导航控制器管理着界面顶部的导航栏和界面底部的可选工具栏。导航栏始终存在,由导航控制器本身管理,它使用其子视图控制器提供的内容更新导航栏。当toolbarHidden属性为NO时,导航控制器同样用最上面的视图控制器提供的内容来更新工具条。

UINavigationController 继承自 UIViewController,通常新建的工程是直接将视图控制器添加到 window 上的,如果添加了 navigation 以后视图结构就多了一层。

UITabBarController

UITabBarController的功能是管理多个视图控制器的切换,通过点击底部对应按钮,选择当前需要展示的视图。

模态视图控制器

Presented View Controllers(简写为PVC)经常用于替换当前显示的视图控制器,即将PVC作为新显示的内容。当在视图控制器A中模态显示视图控制器B时,A就是presenting view controller(弹出VC),而B就是presented view controller(即被弹出VC)。注意,官方文档建议这两者之间通过delegate实现交互。

模态视图控制器只是一种显示方式,它不依赖于控制器容器。通常用于显示较独立的内容,在模态窗口显示时,其他视图的内容无法进行操作(遮盖住了)。另需注意,presented VC 与 presentation context 处于同一层级,而与presenting VC所在的层级无关。具体可参考此官方文档

// 显示
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^)(void))completion

// 退出
- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^)(void))completion;
  • presentViewController:animated:completion: 弹出新视图 可带动画效果,可实现完成后的回调,通常为nil
  • dismissViewControllerAnimated:completion:退出新视图 可带动画效果,可实现完成后的回调,通常为nil

这里有一个问题,谁来调用dismiss这个退出视图的方法?一般的,应该由 presenting VC来调用。此处系统做了优化,当在presented VC里面调用dismiss方法时,系统会自动的将这个消息传递到相应的presenting VC中。

切换动画在弹出一个新视图和退出顶层视图时都可以使用。设置presented VC的modalTransitionStyle属性,就可以设置切换动画的风格:

NewViewController *nvc = [[NewViewController alloc] init];
// 设置动画风格    
nvc.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
// 模态呈现新视图
[self presentViewController:nvc animated:YES completion:nil];

系统支持的四种动画:

typedef enum {
    UIModalTransitionStyleCoverVertical=0, //默认方式,从底部滑入
    UIModalTransitionStyleFlipHorizontal,  //水平翻转
    UIModalTransitionStyleCrossDissolve,   //隐出隐现
    UIModalTransitionStylePartialCurl,     //翻页效果
} UIModalTransitionStyle;

退到指定视图

从A控制器present到B控制器,再从B控制器present到C控制器,如何直接从C回到A呢?

思考一下,A是B的presenting VC,同时B又是C的presenting VC,因此:

UIViewController *rootVC = self.presentingViewController;
UIViewController *bottomVC;
while (rootVC) {
        bottomVC = rootVC;
        rootVC = rootVC.presentingViewController;
}

[bottomVC dismissViewControllerAnimated:YES completion:nil];

iOS13 适配

在 iOS13中,模态弹出的新视图控制器不是全屏的了,这是因为苹果将 UIViewControllermodalPresentationStyle 属性的默认值从UIModalPresentationFullScreen修改成了UIModalPresentationAutomatic,对于多数 VC,此值会映射成 UIModalPresentationPageSheet。想要恢复原来模态视图样式,只需要主动把modalPresentationStyle属性设置为UIModalPresentationFullScreen

NextViewController *nvc = [[NextViewController alloc] init];
nvc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nvc animated:YES completion:NULL];

如果是有UINavigationController的视图控制器,则设置UINavigationControllermodalPresentationStyle属性:

NextViewController *nvc = [[NextViewController alloc] init];
UINavigationController *nextNav = [[UINavigationController alloc] initWithRootViewController:nvc];
nextNav.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:nextNav animated:YES completion:NULL];

自定义容器视图控制器

负责一个或者多个View Controller的展示并对其视图生命周期进行管理的对象,称之为容器,大部分容器本身也是一个View Controller,这样的容器可以称之为Container View Controller。参考官方文档:Implementing a Container View Controller

要想实现自定义的视图控制器切换,容器视图需要依赖父子控制器的概念来实现。iOS5.0之后,视图控制器新添加了addChildViewController等方法。根据MVC的原则,每个视图控制器只需要管理一个view视图层次结构,并且此时的childViewController拥有与父视图控制器同步的生命周期。当某个子View没有显示时,将不会被load,减少了内存的使用。当内存紧张时,没有load的View将被首先释放,优化了程序的内存释放机制。

父子控制器相关的属性和方法:

// 子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

// 向父视图控制器中添加子父视图控制器
- (void)addChildViewController:(UIViewController *)childController;
// 将子视图控制器从父视图控制器中移除
- (void)removeFromParentViewController;
// 切换子视图控制器
- (void)transitionFromViewController::::::;
// 在添加或移除视图控制器之前被调用
- (void)willMoveToParentViewController:(UIViewController *)parent
// 子控视图制器已经被添加到某个父控制器上时调用
- (void)didMoveToParentViewController:(UIViewController *)parent

这里需要注意,当添加子视图控制器时,会自动调用willMoveToParentViewController方法,此时只需手动调用didMoveToParentViewController即可;当移除子视图控制器时,会自动调用didMoveToParentViewController,此时只需手动调用willMoveToParentViewController

// 添加子控制器
[self addChildViewController:childVC];
[self.view addSubview:childVC.view];
[childVC didMoveToParentViewController:self];

// 移除子控制器
[childVC willMoveToParentViewController:nil];
[childVC removeFromParentViewController];
[childVC.view removeFromSuperview];

示例:

// 添加子视图控制器
- (void) displayContentController: (UIViewController*) content {
    [self addChildViewController:content];
    content.view.frame = [self frameForContentController];
    [self.view addSubview:self.currentClientView];
    [content didMoveToParentViewController:self];
}

// 移除子视图控制器
- (void) hideContentController: (UIViewController*) content {
    [content willMoveToParentViewController:nil];
    [content.view removeFromSuperview];
    [content removeFromParentViewController];
}

//  子视图控制器之间的过渡动画
- (void)cycleFromViewController: (UIViewController*) oldVC toViewController: (UIViewController*) newVC {
    [oldVC willMoveToParentViewController:nil];
    [self addChildViewController:newVC];

    // 获取新视图控制器的起始帧和旧视图控制器的结束帧。两个矩形都在屏幕外
    newVC.view.frame = [self newViewStartFrame];
    CGRect endFrame = [self oldViewEndFrame];

    // 排队等待过渡动画
    [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
        // 为视图制作动画,使其处于最终位置
        newVC.view.frame = oldVC.view.frame;
        oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
        // 删除旧的视图控制器,并向新的视图控制器发送最终通知
        [oldVC removeFromParentViewController];
        [newVC didMoveToParentViewController:self];
        }];
}

关于子视图控制器之间的过渡,函数内部系统会自动添加新的视图和移除之前的视图。


公众号“编程之路从0到1”

20190301102949549

Copyright © Arcticfox 2021 all right reserved,powered by Gitbook文档修订于: 2022-05-01 12:20:20

results matching ""

    No results matching ""