首页 关于 微信公众号
欢迎关注我的微信公众号

iOS 入门(3):实现一个多页面 App

本文主要讲解基于 UITabBarController 和 UINavigationController 搭建一个多页面 App 项目。本文的内容只包含最基本的知识点。

教程

目录:

1、项目目录结构调整

这一节,我们会基于 UITabBarController、UINavigationController 做一个多页面 App 项目。我们这里说的「页面」一般对应的是 UIViewController。

做一个项目时,建立一个清晰的目录结构,对于提高自己和其他合作伙伴的效率都是大有好处的。对于具体的目录结构,每个人都有自己的偏好,这里列出一种供参考:

|—MyProject
    |—Utility // 自己实现的一些通用性较好的功能代码,这些代码有比较好的接口且与本项目不存在耦合,可直接复用于其他项目。
    |—Common // 本项目的一些全局性代码,这些代码通常与本项目的业务逻辑存在一些耦合,所以不放在 Utility 目录中。
    |—Feature // 本项目的功能模块目录,该目录下将项目的功能划分为多个模块,每个模块穿透 MVC,可以作为独立任务划分出去。当然,在模块下你不采用 MVC,采用 MVVM 或其他架构方式也没问题的。
        |—Base // 定义本项目中各种 Controller、View、Model 的基础类或基础接口。
            |—Controller
            |—View
            |—Model
        |—Main // 示例 Feature 1。
            |—Controller
            |—View
            |—Model
        |—User // 示例 Feature 2。
            |—Controller
            |—View
            |—Model
    |—Resource // 本项目的资源目录,放置图片、音频等资料。
        |—Image
        |—Sound
|—Pods // 采用 CocoaPods 管理的第三方库。

那么对应到我们现在讲的这个项目中,我们调整后的项目目录结构大致如下:

image

你可以看看 iOS 项目的目录结构来了解更多。

2、Tab 式 + Navigation 式的项目架构实现

我们在实现一个多页面的 App 项目时,苹果官方提供的常用页面容器有 UITabBarController 和 UINavigationController。

UITabBarController 是通过数组的形式管理容器内的所有 UIViewController,UINavigationController 则是通过的形式管理容器内所有 UIViewController。这两种容器是可以嵌套使用的,标准嵌套的方式是以 UITabBarController 为主容器,而 UINavigationController 为辅助容器。大致结构如图:

image

下面,我们来看看代码:

1)在项目中添加新页面的代码:

添加工具类代码:

大致结构如图:

image

2) 添加工具方法。

修改 STCommonUtil.h 代码如下:

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface STCommonUtil : NSObject

#pragma mark - Image Util
+ (UIImage *)imageWithColor:(UIColor *)color;
+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size;

@end

修改 STCommonUtil.m 代码如下:

#import "STCommonUtil.h"

@implementation STCommonUtil

#pragma mark - Image Util
+ (UIImage *)imageWithColor:(UIColor *)color {
    return [STCommonUtil imageWithColor:color size:CGSizeMake(1.0, 1.0)];
}

+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size {
    CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, rect);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}

@end

代码解释:添加了两个用代码生成图片的工具方法。

3) 在应用程序的入口构建项目的框架。

修改 STAppDelegate.m 代码如下:

#import "STAppDelegate.h"
#import "STMainViewController.h"
#import "STExploreViewController.h"
#import "STMeViewController.h"
#import "STCommonUtil.h"

@interface STAppDelegate ()

@end

@implementation STAppDelegate

#pragma mark - UIApplicationDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // Override point for customization after application launch.
    
    // Init.
    UIImage *tabImage = nil;
    UIImage *tabImageHighlight = nil;
    
    //// Main.
    STMainViewController *mainViewController = [[STMainViewController alloc] init];
    mainViewController.title = @"Main";
    tabImage = [[STCommonUtil imageWithColor:[UIColor redColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    tabImageHighlight = [[STCommonUtil imageWithColor:[UIColor grayColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    mainViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:mainViewController.title image:tabImage selectedImage:tabImageHighlight];
    UINavigationController *mainNavigationController = [[UINavigationController alloc] initWithRootViewController:mainViewController];
    
    //// Explore.
    STExploreViewController *exploreViewController = [[STExploreViewController alloc] init];
    exploreViewController.title = @"Explore";
    tabImage = [[STCommonUtil imageWithColor:[UIColor greenColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    tabImageHighlight = [[STCommonUtil imageWithColor:[UIColor grayColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    exploreViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:exploreViewController.title image:tabImage selectedImage:tabImageHighlight];
    UINavigationController *exploreNavigationController = [[UINavigationController alloc] initWithRootViewController:exploreViewController];
    
    //// Me.
    STMeViewController *meViewController = [[STMeViewController alloc] init];
    meViewController.title = @"Me";
    tabImage = [[STCommonUtil imageWithColor:[UIColor blueColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    tabImageHighlight = [[STCommonUtil imageWithColor:[UIColor grayColor] size:CGSizeMake(30, 30)] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    meViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:meViewController.title image:tabImage selectedImage:tabImageHighlight];
    UINavigationController *meNavigationController = [[UINavigationController alloc] initWithRootViewController:meViewController];
    
    //// Main tab bar controller.
    UITabBarController *mainTabBarController = [[UITabBarController alloc] init];
    mainTabBarController.viewControllers = @[mainNavigationController, exploreNavigationController, meNavigationController];
    
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = mainTabBarController;
    [self.window makeKeyAndVisible];
    
    return YES;
}

@end

代码解释:在上面的代码中,我们构建了这个 App 的基本页面框架。以 UITabBarController 为主容器,并为其添加了 3 个 UINavigationController 子容器,初始化了每个 UINavigationController 的初始的 UIViewController,每个子容器独立管理其栈内的页面流。

4)使用 UINavigationController 来管理页面跳转。

修改 STMainViewController.m 代码如下:

#import "STMainViewController.h"
#import <Masonry/Masonry.h>
#import <SVProgressHUD/SVProgressHUD.h>
#import "STDetailViewController.h"

@interface STMainViewController ()

@end

@implementation STMainViewController

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Do any additional setup after loading the view, typically from a nib.
    
    // Setup.
    [self setupUI];
}

#pragma mark - Setup
- (void)setupUI {
    // Use full screen layout.
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.automaticallyAdjustsScrollViewInsets = YES;
    self.extendedLayoutIncludesOpaqueBars = YES;
    
    // Navigation item.
    UIBarButtonItem *detailBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Detail" style:UIBarButtonItemStylePlain target:self action:@selector(goToDetailPage)];
    self.navigationItem.rightBarButtonItem = detailBarButton;
    
    // Hello button.
    UIButton *helloButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [helloButton setTitle:@"Hello" forState:UIControlStateNormal];
    [helloButton addTarget:self action:@selector(onHelloButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:helloButton];
    [helloButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.equalTo(@60.0);
        make.height.equalTo(@40.0);
        make.center.equalTo(self.view);
    }];
}

#pragma mark - Navigation
- (void)goToDetailPage {
    STDetailViewController *detailViewController = [[STDetailViewController alloc] init];
    detailViewController.hidesBottomBarWhenPushed = YES;
    [self.navigationController pushViewController:detailViewController animated:YES];
}

#pragma mark - Action
- (void)onHelloButtonClicked:(id)sender {
    NSLog(@"Hello, world!");
    
    [SVProgressHUD showSuccessWithStatus:@"Hello, world!" maskType:SVProgressHUDMaskTypeBlack];
}

@end

修改 STDetailViewController.m 代码如下:

#import "STDetailViewController.h"

@interface STDetailViewController ()

@end

@implementation STDetailViewController

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Setup.
    [self setupUI];
}

#pragma mark - Setup
- (void)setupUI {
    // Use full screen layout.
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.automaticallyAdjustsScrollViewInsets = YES;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.view.backgroundColor = [UIColor whiteColor];
    
    // Title.
    self.title = @"Detail";
}

@end

代码解释:在 STMainViewController 的 setupUI 方法中,我们添加了 detailBarButton,通过这个点击这个按钮,我们可以跳转到一个 STDetailViewController 页面,跳转的代码实现就在 goToDetailPage 方法中。

3、使用 UITableView

UITableView 是 iOS 开发中最常使用的数据展示控件之一,它体现了大多数 Cocoa UI 组件的设计思路,也是我们封装自定义组件时的重要参考模板。

我们在 Explore 页面中添加 UITableView 来做一些基础内容展示,修改 STExploreViewController.m 文件代码如下:

#import "STExploreViewController.h"
#import "STCommonUtil.h"
#import <Masonry/Masonry.h>

NSString * const STExploreCellIdentifier = @"STExploreCellIdentifier";

@interface STExploreViewController () <UITableViewDataSource, UITableViewDelegate>

@property (strong, nonatomic) UITableView *myTableView;

@end

@implementation STExploreViewController

#pragma mark - Property
- (UITableView *)myTableView {
    if (!_myTableView) {
        _myTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
        [_myTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:STExploreCellIdentifier];
        _myTableView.delegate = self;
        _myTableView.dataSource = self;
    }
    
    return _myTableView;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Setup.
    [self setupUI];
}

#pragma mark - Setup
- (void)setupUI {
    // Use full screen layout.
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.automaticallyAdjustsScrollViewInsets = YES;
    self.extendedLayoutIncludesOpaqueBars = YES;
    
    // myTableView.
    [self.view addSubview:self.myTableView];
    [self.myTableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}

#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 44;
}

#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return @"Header";
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:STExploreCellIdentifier forIndexPath:indexPath];
    cell.textLabel.text = @"Hi";
    cell.imageView.image = [STCommonUtil imageWithColor:[UIColor redColor] size:CGSizeMake(30, 30)];
    cell.layer.shouldRasterize = YES;
    cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
    
    return cell;
}

@end

代码解释:我们在 STExploreViewController 中添加了一个 UITableView 作为 subview 来展示数据。UITableView 作为一个封装良好的组件,是通过 Delegate 的方式把它的数据和交互接口暴露给外部。所以在代码中,我们可以看到:

代码:

@interface STExploreViewController () <UITableViewDataSource, UITableViewDelegate>

代码:

_myTableView.delegate = self;
_myTableView.dataSource = self;

代码:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{...}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{...}
...

运行项目你应该能看到下面的界面:

image

Demo

你可以接着前面继续下面的步骤来获取这一节对应的 Demo 代码:

如果你还没有下载 iOSStartDemo,请先执行下列命令下载:

$ git clone https://github.com/samirchen/iOSStartDemo.git
$ cd iOSStartDemo/iOSStartDemo

如果已经下载过了,则接着进入正确的目录并执行下列命令:

$ git fetch origin s3
$ git checkout s3
$ pod install
$ open iOSStartDemo.xcworkspace

Blog

Opinion

Project