项目实战No7 标签按钮 父子控制器

本文介绍iOS应用中标签的创建与管理方法,包括标签的逆向传递、标签显示及布局调整等内容,并探讨控制器间的数据传递技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 传递数据

  • 传递标签数据回到上一个界面(逆传)

    • 方法1

      NSMutableArray *tags = [NSMutableArray array];
      for (XMGTagButton *tagButton in self.tagButtons) {
       [tags addObject:tagButton.currentTitle];
      }
      
    • 方法2:KVC

  // 将self.tagButtons中存放的所有对象的currentTitle属性值取出来,放到一个新的数组中,并返回
     NSArray *tags = [self.tagButtons valueForKeyPath:@"currentTitle"];
     !self.getTagsBlock ? : self.getTagsBlock(tags);

    // 2.关闭当前控制器
    [self dismissViewControllerAnimated:YES completion:nil];
  • 取出数据,用block传数据
/** 传递tag数据的block, block的参数是一个字符串数组 */
@property (nonatomic, copy) void (^getTagsBlock)(NSArray *);

   XMGAddTagViewController *addTag = [[XMGAddTagViewController alloc] init];
   addTag.getTagsBlock = ^(NSArray *tags) {
        [weakSelf createTagLabels:tags];
    };

控制器A向B发送数据,由A发出通知,控制器A需要保留一个block属性或者delegate

二 显示标签Label

  • 创建标签Label
  // 所有的标签label
    for (int i = 0; i < tags.count; i++) {
    // 创建label
    UILabel *newTagLabel = [[UILabel alloc] init];
    newTagLabel.text = tags[i];
    newTagLabel.font = [UIFont systemFontOfSize:14];
    newTagLabel.backgroundColor = XMGTagBgColor;
    newTagLabel.textColor = [UIColor whiteColor];
    newTagLabel.textAlignment = NSTextAlignmentCenter;
    [self.topView addSubview:newTagLabel];
    [self.tagLabels addObject:newTagLabel];
    // 尺寸
    [newTagLabel sizeToFit];
    newTagLabel.height = XMGTagH;
    newTagLabel.width += 2 * XMGCommonSmallMargin;
    // 位置
     if (i == 0) {
         newTagLabel.x = 0;
         newTagLabel.y = 0;
        } else {
          // 上一个标签
          UILabel *previousTagLabel = self.tagLabels[i - 1];
          CGFloat leftWidth = CGRectGetMaxX(previousTagLabel.frame) + XMGCommonSmallMargin;
          CGFloat rightWidth = self.topView.width - leftWidth;
            if (rightWidth >= newTagLabel.width) {
                newTagLabel.x = leftWidth;
                newTagLabel.y = previousTagLabel.y;
            } else {
                newTagLabel.x = 0;
                newTagLabel.y = CGRectGetMaxY(previousTagLabel.frame) + XMGCommonSmallMargin;
            }
        }
    }
  • 加号按钮位置
 UILabel *lastTagLabel = self.tagLabels.lastObject;
    if (lastTagLabel) {
        CGFloat leftWidth = CGRectGetMaxX(lastTagLabel.frame) + XMGCommonSmallMargin;
        CGFloat rightWidth = self.topView.width - leftWidth;
        if (rightWidth >= self.addButton.width) {
            self.addButton.x = leftWidth;
            self.addButton.y = lastTagLabel.y;
        } else {
            self.addButton.x = 0;
            self.addButton.y = CGRectGetMaxY(lastTagLabel.frame) + XMGCommonSmallMargin;
        }
    } else {
        self.addButton.x = 0;
        self.addButton.y = 0;
    }
  • 移除所有Label,因为Label是重写创建

    • 方法1: for-in

      // 移除所有的label
      for (UILabel *label in self.tagLabels) {
        [label removeFromSuperview];
      }
      
    • 方法2:removeFromSuperview方法

   // 让self.tagLabels数组中的所有对象执行removeFromSuperview方法
      [self.tagLabels makeObjectsPerformSelector:@selector(removeFromSuperview)];
      [self.tagLabels removeAllObjects];
  • 一点加号数据传输下一个view(顺传)
addTag.tags = [self.tagLabels valueForKeyPath:@"text"];

三 工具条

  • 计算工具条高度
 // 计算工具条的高度
    self.topViewHeight.constant = CGRectGetMaxY(self.addButton.frame);
    CGFloat oldHeight = self.height;
    self.height = self.topViewHeight.constant + self.bottomView.height + XMGCommonSmallMargin;
    self.y += oldHeight - self.height; // y增大,旧高度 - 新高度

高度调整最好不要约束和frame一起用,容易出问题
调整高度约束:拖线,定义高度为topViewHeight

  • 进来有2个默认标签:“吐槽”“糗事”
 - (void)awakeFromNib
  {
   [self createTagLabels:@[@"吐槽", @"糗事"]];
  }
  • 当Xib尺寸比较小时,标签布局会有问题:
    • 布局代码写到layoutSubviews
  • 标签创建完,删除旧标签,所有标签默认都在(0,0)位置
    • 没有调用layoutSubviews,标签都叠在第一位
    • 需要重新布局子控件: [self setNeedsLayout];
  • 如果标签没有文字,点击删除直接return
  // 设置点击删除键需要执行的操作
  textField.deleteBackwardOperation = ^{
    // 判断文本框是否有文字
    if (weakSelf.textField.hasText || weakSelf.tagButtons.count == 0) return;
    // 点击了最后一个标签按钮(删掉最后一个标签按钮)
    [weakSelf tagClick:weakSelf.tagButtons.lastObject];
    };
  • 删除完标签时,左侧出现间隙,增加判断如下:
  XMGTagButton *lastTagButton = self.tagButtons.lastObject;
  if (lastTagButton == nil) {
       self.textField.x = 0;
       self.textField.y = 0;
    }

四 通知

  • 2种做法实现“控制器内部”通知的注册和移除
  • 做法1:不管当前控制器是否显示在用户眼前,只要控制器对象还在内存中,就会处理各个地方发出的通知

    • 1>在viewDidLoad方法中注册通知的监听
    • 2>在dealloc方法中移除通知的监听
  • 做法2:只有当前控制器显示在用户眼前,才会处理通知(代码)

    • 1> 在viewWillAppear:方法中注册通知的监听
    • 2> 在viewWillDisappear:方法中移除通知的监听
- (void)viewWillAppear:(BOOL)animated   // 即将显示
{
    [super viewWillAppear:animated];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
}

- (void)viewWillDisappear:(BOOL)animated   // 即将消失
{
    [super viewWillDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

五 父子控制器

  • 场景:控制器挂掉,控制器的view还在,会有坏内存访问
    • 子控制器属于局部变量,所以作用域结束,子控制器就挂掉
    • 控制器的view添加到self.view上,被强引用,view不会死掉

  • 存在的问题分析

    • 控制器已经死了,但是控制器的view还在

      • 1> 点击控制器view内部的按钮、开关,直接EXEC_BAD_ACCESS错误(野指针错误,坏内存访问)
      • 2> 拖拽tableView,tableView上面显示的数据突然消失了
    • 叠加了很多不必要的view

    • 控制器创建得比较频繁
  • 解决方案

    • 保证控制器只创建一次,而且不让它马上死(懒加载,强引用控制器)
    • 保证控制器和控制器的view同在
  • 点击控制器1,把正在显示的控制器移除掉,添加到即将显示的控制器的view,转为正在显示控制器,其他控制器view移除掉
    • 重构代码:
/**
 * 切换控制器
 * @param index 控制器的索引
 */
- (void)switchToVc:(int)index
{
    // 移除当前正在显示的控制器的view
    [self.showingVc.view removeFromSuperview];

    // 添加即将显示的控制器的view
    UIViewController *willShowVc = self.vcs[index];
    willShowVc.view.frame = CGRectMake(0, 55, self.view.frame.size.width, self.view.frame.size.height - 55);
    [self.view addSubview:willShowVc.view];

    // 即将显示 -> 正在显示
    self.showingVc = willShowVc;
}
  • 重构方法2(严重依赖按钮顺序)
//  对应n个Button连线
- (IBAction)testClick:(UIButton *)button {
    // 计算按钮在父控件中的位置
    int index = (int)[button.superview.subviews indexOfObject:button];
    // 切换控制器
    [self switchToVc:index];
}
- (void)switchToVc:(int)index
{
    // 移除当前正在显示的控制器的view
    [self.showingVc.view removeFromSuperview];

    // 添加即将显示的控制器的view
    UIViewController *willShowVc = self.vcs[index];
    willShowVc.view.frame = CGRectMake(0, 55, self.view.frame.size.width, self.view.frame.size.height - 55);
    [self.view addSubview:willShowVc.view];

    // 即将显示 -> 正在显示
    self.showingVc = willShowVc;
}
  • 屏幕旋转
    • 监听方法
// 即将旋转
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
// 已经旋转
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
  1. 屏幕的旋转事件,首先会传递给窗口的根控制器(window.rootViewController);
  2. 窗口根控制器又会将屏幕的旋转事件,传递给它的子控制器;
  3. 子控制器又会将屏幕的旋转事件,传递给它自己的子控制器;
  4. 以此类推,所有的子控制器都能监听到屏幕的旋转事件。
  • 目前只有main控制器能监听旋转事件,下列控制器监听不到旋转事件?
    • 因为下面控制器只是装在数组里,并不是子控制器
/** 存放所有的控制器 */
@property (nonatomic, strong) NSArray *vcs;


- (NSArray *)vcs
{
    if (!_vcs) {
        _vcs = @[
                 [[XMGTest0ViewController alloc] init],
                 [[XMGTest1ViewController alloc] init],
                 [[XMGTest2ViewController alloc] init]
                 ];
    }
    return _vcs;
}
  • 所以要想成为子控制器?
 // 通过addChildViewController:添加的控制器,就会成为子控制器,会被自动添加到childViewControllers数组中
[self addChildViewController:[[XMGTest0ViewController alloc] init]];
  • 屏幕旋转另一方法
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{ 
// 重写这个事件,必须调用super,才能传递给子控制器
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}

父子控制器的开发准则:
1. 如果2个控制器的view是父子关系,那么这2个控制器也应该是父子关系
2. 代码表示形式:(包括间接加上去view和控制器)
[vc0.view addSubview:vc1.view];
[vc0 addChildViewController:vc1];

  • 父子控制器的重要性-push
    • 在Main控制器上添加一个Red控制器, 点击Main控制器跳转到Green控制器
      • Red控制器不是子控制器,不能跳转
      • Red控制器是子控制器,会顺着父控制器望上级一层层查有没有UINavigationController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   GreenViewController *green = [[GreenViewController alloc] init];
   green.title = @"点击Red过来的";
   [self.navigationController pushViewController:green animated:YES];

// 等价写法:  
//  [((UINavigationController *)self.parentViewController.parentViewController) pushViewController:green animated:YES];

//   逐级查找  
// self.parentViewController == XMGMainViewController
// self.parentViewController.parentViewController == XMGTestViewController
//self.parentViewController.parentViewController.parentViewController == UINavigationController; 
}
  • 父子控制器的重要性-modal
    • 点击White控制器modal出Red控制器,点击Red控制器退出
    • 创建Green控制器,添加到Red控制器,需要点击Green控制器退出Red控制器
      • 点击Red控制器,dismiss正确的
      • 点击Green控制器,如果不是Red控制器,就不能dismiss
      • Green控制器检查自己是不是modal出来的,如果不是检查父控制器是不是moadl,以此类推…
- (void)viewDidLoad {
    [super viewDidLoad];

    XMGGreenViewController *green = [[XMGGreenViewController alloc] init];
    green.view.frame = CGRectMake(0, 100, 200, 200);
    green.view.autoresizingMask = UIViewAutoresizingNone;
    [self.view addSubview:green.view];
    [self addChildViewController:green];
}

注意默认情况下,控制器的view.autoresizingMask的值是:
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
父控件宽高变化会拉伸子控件宽高

六 精华板块

  • 加一个Scrollview和标签栏,顶部底部可以穿透导航栏
    • 上下滚动是tableview,水平滚动是Scrollview
  • 设置Scrollview
 - (void)setupScrollView
 {
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.frame = self.view.bounds;
    scrollView.backgroundColor = XMGCommonBgColor;
    [self.view addSubview:scrollView];
    self.scrollView = scrollView;
 }
  • 设置标签栏
UIView *titlesView = [[UIView alloc] init];
    titlesView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.5];
// 设置透明度方法2: titlesView.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.5];
// 设置透明度方法3: titlesView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5];
    titlesView.frame = CGRectMake(0, XMGNavBarMaxY, self.view.width, 35);
    [self.view addSubview:titlesView];
    self.titlesView = titlesView;
  • 标签内部内容实现
// 标签栏内部的标签按钮
    NSArray *titles = @[@"全部", @"视频", @"声音", @"图片", @"段子"];
    NSUInteger count = titles.count;
    CGFloat titleButtonH = titlesView.height;
    CGFloat titleButtonW = titlesView.width / count;
    for (int i = 0; i < count; i++) {
        // 创建
        XMGTitleButton *titleButton = [XMGTitleButton buttonWithType:UIButtonTypeCustom];
        [titleButton addTarget:self action:@selector(titleClick:) forControlEvents:UIControlEventTouchUpInside];
        [titlesView addSubview:titleButton];
        [self.titleButtons addObject:titleButton];

        // 文字
        [titleButton setTitle:titles[i] forState:UIControlStateNormal];

        // frame
        titleButton.y = 0;
        titleButton.height = titleButtonH;
        titleButton.width = titleButtonW;
        titleButton.x = i * titleButton.width;
    }
  • 监听点击,颜色可以切换
    • 选中状态还可以点选,重复刷新
  // 控制按钮状态
  self.selectedTitleButton.selected = NO;
  titleButton.selected = YES;
  self.selectedTitleButton = titleButton;
  • 以下方法,可以实现选中过一次不能在点选
 self.selectedTitleButton.enabled = YES;
 titleButton.enabled = NO;
 self.selectedTitleButton = titleButton;
  • 取消高亮状态,重写方法
- (void)setHighlighted:(BOOL)highlighted { }
  • 标签底部添加高度为1的红色view
 // 标签栏底部的指示器控件
    UIView *titleBottomView = [[UIView alloc] init];
    titleBottomView.backgroundColor = [self.titleButtons.lastObject titleColorForState:UIControlStateSelected];
    titleBottomView.height = 1;
    titleBottomView.y = titlesView.height - titleBottomView.height;
    [titlesView addSubview:titleBottomView];
    self.titleBottomView = titleBottomView;
  • 默认点击最前面的按钮
 [self titleClick:self.titleButtons.firstObject];

注意:Button和Label的宽度,红色view的宽度就是Label的宽度

// 默认点击最前面的按钮
   XMGTitleButton *firstTitleButton = self.titleButtons.firstObject;
   [firstTitleButton.titleLabel sizeToFit];
   titleBottomView.width = firstTitleButton.titleLabel.width;
   titleBottomView.centerX = firstTitleButton.centerX;
   [self titleClick:firstTitleButton];

注意:当不显示view时候,检查代码顺序,先添加子控制器,才能调用方法
scrollView滚动完毕后,才添加对应子控制器的view到scrollView中
scrollView设置contentsize,才能实现滚动

  • 自动调整scrollViewV的滚动条方法
 self.automaticallyAdjustsScrollViewInsets = NO;  // 不自动调
  • 点A标签滚动到对应位置:
  CGPoint offset = self.scrollView.contentOffset;
  offset.x = self.view.width * [self.titleButtons indexOfObject:titleButton];
  [self.scrollView setContentOffset:offset animated:YES];
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值