iOS 玩转微信——下拉小程序

本文详细介绍了如何实现iOS应用中模仿微信下拉显示小程序的功能,包括下拉和上拉阶段的处理,涉及模块分析、状态判断、动画过渡及细节处理,如导航栏变化、小程序容器和指示器的交互逻辑。并提供了源码地址。
摘要由CSDN通过智能技术生成

⭐️ 概述

  • 本文笔者将手把手带领大家像素级还原微信下拉小程序的实现过程。尽量通过简单易懂的言语,以及配合关键代码,详细讲述该功能实现过程中所运用到的技术和实现细节,以及遇到问题后如何解决的心得体会。希望正有此功能需要的小伙伴们,能够通过阅读本文后,能快速将此功能真正运用到实际项目开发中去。

  • 当然,笔者的实现方案不一定是微信官方的实现,毕竟一千个观众眼中有一千个潘金莲,但是,不管黑猫白猫,能捉老鼠的就是好猫,若能够实现此功能,相信也是一个不错的方案。希望该篇文章能为大家提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。

  • 源码地址:WeChat

🌈 预览

ios_mainframe_pulldown_applet_page.gif

🔎 分析

📦 模块

  • 三个球指示模块: 微信主页下拉时,用于指示用户的下拉处于哪个阶段。(MHBouncyBallsView.h/m)

  • 小程序模块: 展示我的小程序最近使用的小程序,以及搜索小程序的功能。(MHPulldownAppletViewController.h/m)

  • 云层模块: 背景云层展示。(WHWeatherView.h/m)

  • 小程序容器模块: 承载小程序模块云层模块、蒙版,以及处理上拉滚动逻辑。(MHPulldownAppletWrapperViewController.h/m)

  • 微信首页模块: 承载小程序容器模块,展示首页内容,以及处理下拉滚动逻辑。(MHMainFrameViewController.h/m)

🚩 阶段

本功能主要涵盖两大阶段:下拉显示小程序阶段上拉隐藏小程序阶段;当然,用户手指上拉或下拉阶段都涉及到以下三种状态:

  • MHRefreshStateIdle: 普通闲置状态(默认)
  • MHRefreshStatePulling: 松开就可以进行刷新的状态
  • MHRefreshStateRefreshing: 正在刷新中的状态

这里简要讲讲微信上拉或下拉进入MHRefreshStatePulling状态的条件:

  • 下拉阶段: 下拉超过临界点,且保证必须是下拉状态,即: 当前下拉偏移量 > 上一次的下拉偏移量。
  • 上拉阶段: 保证必须是上拉状态,即: 当前上拉偏移量 > 上一次的上拉偏移量,或者 偏移量为零且下拉。

松手检测:

  • 下拉阶段: 可以利用scrollView.isDragging来检测即可。
  • 上拉阶段: scrollView.isDragging 这个属性不好使,后面会给出替代方案。

📌 方案

考虑到小程序容器模块小程序模块的UI页面复杂、业务逻辑繁琐,以及涉及到模块下钻等场景,这里采用父子控制器的方案来实现,主要用到以下API:

  • 添加子控制器
[parentController.view addSubview:childController.view]; 

[parentController addChildViewController:childController];

[childController didMoveToParentViewController:parentController];
  • 移除子控制器
[childController willMoveToParentViewController:nil];

[childController.view removeFromSuperview];

[childController removeFromParentViewController];

整体的功能布局如下:

  • 小程序容器模块微信首页模块的子控制器。
  • 三个球指示模块微信首页模块的子控件。

  • 小程序模块小程序容器模块的子控制器。

  • 云层模块小程序容器模块的子控件。

整体的层级结构如下:<从上到下>

三个球模块 --> 小程序模块 --> 上拉UIScollView --> 云层模块 --> 黑色蒙版 --> 小程序容器模块.view --> 微信首页内容UITableView

🚀 实现

通过上面的层级分析和模块划分,我们可以针对下拉阶段上拉阶段,得出各个模块内部在这两个阶段分别作了怎样的处理,以及具体的实现过程。这里特别提醒❗️:分析过程看似简单的一逼,实现起来还是得细节拉满…

⬇️ 下拉阶段

下拉阶段: 无非就是监听微信首页内容UITableView的滚动,首先,根据UITableView下拉拖拽过程中产生的偏移量(contentOffset.y),从而影响各个模块的UI变化;然后,根据用户手指下拉拖拽的距离,判断当前下拉过程中处于哪个状态;最后,当用户结束拖拽(松手)后,是进入普通闲置状态还是正在刷新中的状态,从而呈现不同的UI效果。这里先贴出下拉过程中的关键代码,然后根据代码来分析各个模块的具体实现:

/// tableView 以滚动就会调用
/// 这里的逻辑 完全可以参照 MJRefreshHeader
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    // 在刷新的refreshing状态 do nothing...
    if (self.state == MHRefreshStateRefreshing) {
        return;
    }else if(self.state == MHRefreshStatePulling && !scrollView.isDragging) {
        /// fixed bug: 这里设置最后一次的偏移量 以免回弹
        [scrollView setContentOffset:CGPointMake(0, self.lastOffsetY)];
    }
    
    
    // 当前的contentOffset
    CGFloat offsetY = scrollView.mh_offsetY;
    // 头部控件刚好出现的offsetY
    CGFloat happenOffsetY = -self.contentInset.top;
    
    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即将刷新 的临界点
    CGFloat normal2pullingOffsetY = - MHPulldownAppletCriticalPoint1 ;
    
    /// 计算偏移量 正数
    CGFloat delta = -(offsetY - happenOffsetY);
    
    // 如果正在拖拽
    if (scrollView.isDragging) {
        
        /// 更新 navBar 的 y
        [self.navBar mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.view).with.offset(delta);
        }];
        
        /// 更新 ballsView 的 h
        [self.ballsView mas_updateConstraints:^(MASConstraintMaker *make) {
            CGFloat height = delta;
            make.height.mas_equalTo(MAX(6.0f, height));
        }];
        
        /// 传递offset
        self.viewModel.ballsViewModel.offsetInfo = @{@"offset": @(delta), @"state": @(self.state), @"animate": @NO};;
        
        /// 微信方案:不仅要下拉超过临界点 而且 保证是下拉状态:当前偏移量 > 上一次偏移量
        if (self.state == MHRefreshStateIdle && -delta < normal2pullingOffsetY && offsetY < self.lastOffsetY) {
            // 转为即将刷新状态
            self.state = MHRefreshStatePulling;
        } else if (self.state == MHRefreshStatePulling && (-delta >= normal2pullingOffsetY || offsetY >= self.lastOffsetY)) {
            // 转为普通状态
            self.state = MHRefreshStateIdle;
        }
        
        /// 传递状态
        self.viewModel.appletWrapperViewModel.offsetInfo = @{@"offset": @(delta), @"state": @(self.state)};
        
        /// 记录偏移量
        self.lastOffsetY = offsetY;
        
    } else if (self.state == MHRefreshStatePulling) {
        
        self.lastOffsetY = .0f;
        
        self.state = MHRefreshStateRefreshing;
    } else {
        /// 更新 navBar y
        [self.navBar mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.view).with.offset(delta);
        }];
        
        /// 更新 ballsView 的 h
        [self.ballsView mas_updateConstraints:^(MASConstraintMaker *make) {
            CGFloat height = delta;
            make.height.mas_equalTo(MAX(6.0f, height));
        }];
        
        /// 传递offset
        self.viewModel.ballsViewModel.offsetInfo = @{@"offset": @(delta), @"state": @(self.state), @"animate": @NO};
        
        /// 传递状态
        self.viewModel.appletWrapperViewModel.offsetInfo = @{@"offset": @(delta), @"state": @(self.state)};
        
        /// 记录偏移量
        self.lastOffsetY = offsetY;
    }
}

#pragma mark - Setter & Getter
- (void)setState:(MHRefreshState)state {
    MHRefreshState oldState = self.state;
    if (state == oldState) return;
    _state = state;
    
    // 根据状态做事情
    if (state == MHRefreshStateIdle) {
        if (oldState != MHRefreshStateRefreshing) return;
        
        /// 动画过程中 禁止用户交互
        self.view.userInteractionEnabled = NO;
        
        /// 更新位置
        [self.navBar mas_updateConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.view).with.offset(0);
        }];
        
        /// 更新 ballsView 的 h
        [self.ballsView mas_updateConstraints:^(MASConstraintMaker *make) {
            CGFloat height = 0;
            make.height.mas_equalTo(MAX(6.0f, height));
        }];
        
        /// 传递offset
        self.viewModel.ballsViewModel.offsetInfo = @{@"offset": @(0), @"state": @(state), @"animate": @YES};
        
        // 先置位到最底下 后回到原始位置; 因为小程序 下钻到下一模块 tabBar会回到之前的位置
        self.tabBarController.tabBar.mh_y = MH_SCREEN_HEIGHT;
        self.tabBarController.tabBar.alpha = .0f;
        
        [UIView animateWithDuration:MHPulldownAppletRefreshingDuration animations:^{
            /// 导航栏相关 回到原来位置
//            self.tabBarController.tabBar.hidden = NO;
            self.tabBarController.tabBar.alpha = 1.0f;
            self.tabBarController.tabBar.mh_y = MH_SCREEN_HEIGHT - self.tabBarController.tabBar.mh_height;
            
            /// 设置tableView y
            self.tableView.mh_y = 0;
            
            [self.view layoutIfNeeded];
            self.navBar.backgroundView.backgroundColor = MH_MAIN_BACKGROUNDCOLOR;
        } completion:^(BOOL finished) {
            
            /// 完成后 传递数据给
            self.tableView.showsVerticalScrollIndicator = YES;
            /// 动画结束 允许用户交互
            self.view.userInteractionEnabled = YES;
        }];
    } else if (state == MHRefreshStateRefreshing) {
        dispatch_async(dispatch_get_main_queue(), ^{
            
            /// 隐藏滚动条
            self.tableView.showsVerticalScrollIndicator = NO;
            
            /// 传递offset 正向下拉
            self.viewModel.ballsViewModel.offsetInfo = @{@"offset": @(MH_SCREEN_HEIGHT - MH_APPLICATION_TOP_BAR_HEIGHT), @"state": @(self.state), @"animate": @NO};
            
            /// 传递状态
            self.viewModel.appletWrapperViewModel.offsetInfo = @{@"offset": @(MH_SCREEN_HEIGHT - MH_APPLICATION_TOP_BAR_HEIGHT), @"state": @(self.state)};
            
            /// 最终停留点的位置
            CGFloat top = MH_SCREEN_HEIGHT;
            /// 更新位置
            [self.navBar mas_updateConstraints:^(MASConstraintMaker *make) {
                make.top.equalTo(self.view).with.offset(top - MH_APPLICATION_TOP_BAR_HEIGHT);
            }];
            
            /// 动画过程中 禁止用户交互
            self.view.userInteractionEnabled = NO;
            
            /// 动画
            [UIView animateWithDuration:MHPulldownAppletRefreshingDuration animations:^{
                [self.view layoutIfNeeded];
                
                // 增加滚动区域top
                self.tableView.mh_insetT = top;
                
                // ⚠️ FBI Warning:
                // Xcode Version 11.4.1 设置animated: NO 也不好使 总之下面这两个方法都不好使
                // Xcode Version 10.2.1 设置animated: NO 却好使
                /// 妥协处理:这里统一用 animated: Yes 来处理 然后控制动画时间 与 scrollView 的 setContentOffset:animated: 相近即可
                // 设置滚动位置 animated:YES 然后
                [self.tableView setContentOffset:CGPointMake(0, -top) animated:YES];
                /// 按照这个方式 会没有动画 tableView 会直接掉下去
//                [self.tableView setContentOffset:CGPointMake(0, -top)];
                
                /// - [iphone – UIScrollview setContentOffset与非线性动画?](http://www.voidcn.com/article/p-glnejqrs-bsv.html)
                /// - [iphone – 更改setContentOffset的速度:animated:?](http://www.voidcn.com/article/p-bgupiewh-bsr.html)
                
                self.navBar.backgroundView.backgroundColor = [UIColor whiteColor];
                
                /// 这种方式没啥动画
//                self.tabBarController.tabBar.hidden = YES;
                /// 这种方式有动画
                self.tabBarController.tabBar.alpha = .0f;
                self.tabBarController.tabBar.mh_y = MH_SCREEN_HEIGHT;
                
            } completion:^(BOOL finished) {
                
                /// 小tips: 这里动画完成后 将tableView 的 y 设置到 MH_SCREEN_HEIGHT - finalTop ; 以及 将contentInset 和 contentOffset 回到原来的位置
                /// 目的:后期上拉的时候 只需要改变tableView 的 y就行了
                CGFloat finalTop = self.contentInset.top;
                self.tableView.mh_y = MH_SCREEN_HEIGHT - finalTop;
                // 增加滚动区域top
                self.tableView.mh_insetT = finalTop;
                // 设置滚动位置
                [self.tableView setContentOffset:CGPointMake(0, -finalTop) animated:NO];
                /// 动画结束 允许用户交互
                self.view.userInteractionEnabled = YES;
            }];
        });
    }
}
微信首页

下拉拖拽过程:即scrollView.isDragging == YES,该过程主要是:1、修改自定义导航栏的Y值。 2、计算当前下过过程中处于什么状态(Pullingor Idle)。3、传递偏移量状态三个球指示模块下拉程序容器模块

下拉松手过程:即scrollView.isDragging ==NO,如果下拉拖拽过程中的状态时Pulling,那么松手的瞬间会进入到Refreshing;反之,则回弹到原始下拉过程中,即默认状态(Idle)。

刷新状态逻辑:手指释放:下拉状态由 Pulli

如果您下载了本程序,但是该程序存在问题无法运行,那么您可以选择退款或者寻求我们的帮助(如果找我们帮助的话,是需要追加额外费用的)。另外,您不会使用资源的话(这种情况不支持退款),也可以找我们帮助(需要追加额外费用) 微信小程序是腾讯公司基于微信平台推出的一种轻量级应用形态,它无需用户下载安装即可在微信内直接使用。自2017年正式上线以来,小程序凭借其便捷性、易获取性和出色的用户体验迅速获得市场认可,并成为连接线上线下服务的重要桥梁。 小程序的核心特点包括: 零安装:用户只需通过微信扫一扫或搜索功能,即可打开和使用小程序,大大降低了用户的使用门槛和手机存储空间压力。 速度快:加载速度相较于传统的HTML5网页更快,依托于微信强大的基础设施,能够实现近乎原生应用的流畅体验。 跨平台兼容:开发者一次开发,即可在多种终端设备上运行,免除了复杂的适配工作,大大提高了开发效率。 社交属性强:小程序可以无缝嵌入微信生态,支持分享至聊天窗口、朋友圈等社交场景,有利于用户间的传播和裂变增长。 丰富接口能力:提供丰富的API接口,可调用微信支付、位置服务、用户身份识别等多种功能,方便企业进行商业服务的集成与拓展。 目前,微信小程序已经覆盖了电商购物、生活服务、娱乐休闲、教育学习、工具助手等多个领域,为数以亿计的用户提供便捷的服务入口,也为众多商家和开发者提供了新的商业模式和创业机会。随着技术的不断升级和完善,小程序已成为现代移动互联网生态中不可或缺的一部分。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值