iOS 玩轉(zhuǎn)微信——通訊錄

概述

  • 2019年初--至今枝哄,筆者為求生計(jì)肄梨,被迫轉(zhuǎn)學(xué)Vue開發(fā),老兵不死挠锥,只會(huì)逐漸凋零众羡,以致于漸漸冷落了iOS開發(fā),畢竟有舍便有得蓖租,不逼自己一把粱侣,也不知道自己有多優(yōu)秀羊壹。

  • 由于大家對(duì) WeChat 中運(yùn)用的MVVM + RAC + ViewModel-Based Navigation的模式比較感興趣,但是此前此項(xiàng)目主要是用于團(tuán)隊(duì)內(nèi)部交流使用齐婴,主要介紹了其中使用技巧和實(shí)用技術(shù)油猫,以及一些細(xì)節(jié)處理,實(shí)用為主柠偶,功能為輔情妖。

  • 盡管實(shí)現(xiàn)了微信的整體架構(gòu),以及朋友圈等功能诱担,但是其中還是充斥著不少測(cè)試代碼毡证,這讓整體項(xiàng)目看起來像個(gè)Demo,并且不夠優(yōu)美蔫仙,隨著微信 7.0.0+的出現(xiàn)料睛,整體UI也發(fā)生了翻天覆地的變化,所以摇邦,只好痛定思痛恤煞,重蹈覆轍,重拾iOS涎嚼,這里先以高仿微信通訊錄為例阱州,宣告筆者強(qiáng)勢(shì)復(fù)出,后期爭(zhēng)取盡自己最大努力法梯,98%還原真實(shí)微信開發(fā)苔货,不斷剖析其中的技術(shù)實(shí)現(xiàn)和細(xì)節(jié)處理。

  • 筆者希望通過學(xué)習(xí)和實(shí)踐這個(gè)項(xiàng)目立哑,也能夠打開學(xué)習(xí)ReactiveCocoa + MVVM的大門夜惭。當(dāng)然同時(shí)也是拋磚引玉,擺渡眾生铛绰、取長補(bǔ)短诈茧,希望能夠提供一點(diǎn)思路,少走一些彎路捂掰,填補(bǔ)一些細(xì)坑敢会,在幫助他人的過程中,收獲分享技術(shù)的樂趣这嚣。

  • 源碼地址:WeChat

預(yù)覽

索引 側(cè)滑
ios_contacts_page_0.png
ios_contacts_page_1.png

GIF

ios_contacts_page.gif

功能

通訊錄模塊鸥昏,盡管UI看起來極其簡(jiǎn)單,但是涵蓋不少知識(shí)點(diǎn)姐帚,也是通訊錄模塊的功能所在吏垮,本篇文章將詳述以下知識(shí)點(diǎn)以及實(shí)現(xiàn)的細(xì)節(jié):

  • 漢字轉(zhuǎn)拼音數(shù)據(jù)排序按字母分組
  • 底部上拉顯示白底
  • A-Z 索引Bar膳汪、索引聯(lián)動(dòng)唯蝶、懸停HeaderView漸變
  • Cell 側(cè)滑備注修改側(cè)滑樣式

分析

數(shù)據(jù)處理

首先遗嗽,主要是將聯(lián)系人姓名轉(zhuǎn)成拼音粘我,然后取聯(lián)系人拼音首字母;其次媳谁,利用字典(NSDictionary)的key的唯一性涂滴,將聯(lián)系人的首字母插入到字典當(dāng)中去;最后晴音,取出字典的allKeys進(jìn)行字母排序柔纵,然后遍歷數(shù)據(jù),進(jìn)行按字母分組锤躁。

這里的核心技術(shù)就是漢字轉(zhuǎn)拼音搁料,當(dāng)然大家可以使用iOS原生庫方法PinYin4Objc來實(shí)現(xiàn),這里筆者主要講講系羞,iOS原生提供的API:

/// string 要轉(zhuǎn)換的string郭计,比如要轉(zhuǎn)換的中文,同時(shí)它是mutable的椒振,因此也直接作為最終轉(zhuǎn)換后的字符串昭伸。
/// range是要轉(zhuǎn)換的范圍,同時(shí)輸出轉(zhuǎn)換后改變的范圍澎迎,如果為NULL庐杨,視為全部轉(zhuǎn)換。
/// transform可以指定要進(jìn)行什么樣的轉(zhuǎn)換夹供,這里可以指定多種語言的拼寫轉(zhuǎn)換灵份。
/// reverse指定該轉(zhuǎn)換是否必須是可逆向轉(zhuǎn)換的。
/// 如果轉(zhuǎn)換成功就返回true哮洽,否則返回false
Boolean CFStringTransform(CFMutableStringRef string, CFRange *range, CFStringRef transform, Boolean reverse);
CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("羋月"));
CFStringTransform(string, NULL, kCFStringTransformMandarinLatin, NO);
NSLog(@"%@",string);
/// 打印結(jié)果:mǐ yuè

/// 由于??正確的輸出了拼音填渠,而且還帶上了音標(biāo)。有時(shí)候我們不需要音標(biāo)怎么辦鸟辅?還好CFStringTransform同時(shí)提供了將音標(biāo)字母轉(zhuǎn)換為普通字母的方法kCFStringTransformStripDiacritics氛什。我們?cè)谏厦娴拇a基礎(chǔ)上再加上這個(gè):

CFStringTransform(string, NULL, kCFStringTransformStripDiacritics, NO);
NSLog(@"%@",string);
/// 打印結(jié)果:mi yue

由于后期考慮到,搜索模塊需要增加本地搜索聯(lián)系人的需求匪凉,所以本項(xiàng)目這里采用了內(nèi)部已經(jīng)封裝好 PinYin4ObjcHighlightedSearch屉更,它支持搜索關(guān)鍵字,高亮顯示洒缀,支持漢字、全拼、簡(jiǎn)拼搜索树绩,支持多音字搜索萨脑。

漢子轉(zhuǎn)拼音API如下:

/// WPFPinYinTools.h
/** 獲取傳入字符串的第一個(gè)拼音字母 */
+ (NSString *)firstCharactor:(NSString *)aString withFormat:(HanyuPinyinOutputFormat *)pinyinFormat;

數(shù)據(jù)處理整體代碼如下:

/// 聯(lián)系人數(shù)據(jù)處理
- (void)_handleContacts:(NSArray *)contacts {
    if (MHObjectIsNil(contacts) || contacts.count == 0) return;
    
    // 計(jì)算總?cè)藬?shù)
    self.total = [NSString stringWithFormat:@"%ld位聯(lián)系人",contacts.count];
    
    
    // 這里需要處理數(shù)據(jù)
    NSMutableDictionary *tempDict = [[NSMutableDictionary alloc] init];
    
    // 獲取首字母
    for(MHUser *contact in contacts){
        // 存到字典中去 <ps: 由于 contacts.json 的wechatId 都是拼音 so...>
        [tempDict setObject:contact forKey:[[contact.wechatId substringToIndex:1] uppercaseString]];
    }
    
    
    //排序,排序的根據(jù)是字母
    NSComparator comparator = ^(id obj1, id obj2) {
        if ([obj1 characterAtIndex:0] > [obj2 characterAtIndex:0]) {
            return (NSComparisonResult)NSOrderedDescending;
        }
        if ([obj1 characterAtIndex:0] < [obj2 characterAtIndex:0]) {
            return (NSComparisonResult)NSOrderedAscending;
        }
        return (NSComparisonResult)NSOrderedSame;
    };
    
    // 已經(jīng)排好序的數(shù)據(jù)
    NSMutableArray *letters = [tempDict.allKeys sortedArrayUsingComparator: comparator].mutableCopy;
    NSMutableArray *viewModels = [NSMutableArray array];
    /// 遍歷數(shù)據(jù)
    for (NSString *letter in letters) {
        // 存儲(chǔ)相同首字母 對(duì)象
        NSMutableArray *temps = [[NSMutableArray alloc] init];
        // 存到數(shù)組中去
        for (NSInteger i = 0; i<contacts.count; i++) {
            MHUser *contact = contacts[i];
            if ([letter isEqualToString:[[contact.wechatId substringToIndex:1] uppercaseString]]) {
                MHContactsItemViewModel *viewModel = [[MHContactsItemViewModel alloc] initWithContact:contact];
                [temps addObject:viewModel];
            }
        }
        [viewModels addObject:temps];
    }
    
    /// 需要配置 新的朋友饺饭、群聊渤早、標(biāo)簽、公眾號(hào)瘫俊、
    MHContactsItemViewModel *friends = [[MHContactsItemViewModel alloc] initWithIcon:@"plugins_FriendNotify_36x36" name:@"新的朋友"];
    MHContactsItemViewModel *groups = [[MHContactsItemViewModel alloc] initWithIcon:@"add_friend_icon_addgroup_36x36" name:@"群聊"];
    MHContactsItemViewModel *tags = [[MHContactsItemViewModel alloc] initWithIcon:@"Contact_icon_ContactTag_36x36" name:@"標(biāo)簽"];
    MHContactsItemViewModel *officals = [[MHContactsItemViewModel alloc] initWithIcon:@"add_friend_icon_offical_36x36" name:@"公眾號(hào)"];
    // 插入到第一個(gè)位置
    [viewModels insertObject:@[friends,groups,tags,officals] atIndex:0];
    
    // 插入一個(gè)
    [letters insertObject:UITableViewIndexSearch atIndex:0];
    
    self.dataSource = viewModels.copy;
    self.letters = letters.copy;
}

頁面展示

當(dāng)數(shù)據(jù)處理完鹊杖,構(gòu)建好cell,刷新tableView扛芽,理論上頁面展示和微信頁面應(yīng)該一模一樣??骂蓖。當(dāng)然我們滾動(dòng)到頁面的最底部,繼續(xù)上拉川尖,會(huì)露出tableView淺灰色(#ededed)的背景色登下,但是看看微信的上拉,露出的卻是白色的背景色叮喳,所以必須把這個(gè)細(xì)節(jié)加上去被芳。

實(shí)現(xiàn)邏輯非常簡(jiǎn)單,只需要設(shè)置tableViiew的背景色為透明色馍悟,然后添加一個(gè)白色背景的UIViewtableView的下面即可畔濒,默認(rèn)隱藏,等有數(shù)據(jù)時(shí)才去顯示锣咒。實(shí)現(xiàn)代碼如下:

/// 添加一個(gè)tempView 放在最底下 用于上拉顯示白底
UIView *tempView = [[UIView alloc] init];
self.tempView = tempView;
// 默認(rèn)隱藏
tempView.hidden = YES;
tempView.backgroundColor = [UIColor whiteColor];
[self.view insertSubview:tempView belowSubview:self.tableView];

[self.tempView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).with.offset(0);
    make.right.equalTo(self.view).with.offset(0);
    make.bottom.equalTo(self.view).with.offset(0);
    make.height.mas_equalTo(MH_SCREEN_HEIGHT * 0.5);
}];

Cell側(cè)滑備注 功能實(shí)現(xiàn)侵状,筆者這里采用iOS 11.0 提供的左滑刪除功能,只需實(shí)現(xiàn)UITableViewDelegate即可宠哄。

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
        return NO;
    }
    return YES;
}
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath  API_AVAILABLE(ios(11.0)){
    
    UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"備注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        completionHandler(YES);
    }];
    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[remarkAction]];
    config.performsFirstActionWithFullSwipe = NO;
    
    return config;
}

由于最新微信側(cè)滑備注淺黑色(#4c4c4c)壹将,而系統(tǒng)默認(rèn)的則是淺灰色的,所以我們需要修改系統(tǒng)的樣式毛嫉,由于每次側(cè)滑诽俯,都有調(diào)用- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath這個(gè)API,而且我們利用Debug view Hierarchy 工具查看層級(jí)承粤,發(fā)現(xiàn)側(cè)滑是加在tableView上暴区,而不是cell上,所以解決思路如下:一旦調(diào)用此API辛臊,立即遍歷tableViewsubView,然后找到對(duì)應(yīng)的UISwipeActionPullView仙粱,修改其內(nèi)部的UISwipeActionStandardButton 背景色。

但是這里需要指出的是彻舰,由于存在兩種層級(jí)關(guān)系如下:

  • iOS 13.0+: UITableView --> _UITableViewCellSwipeContainerView --> UISwipeActionPullView --> UISwipeActionStandardButton
  • iOS 13.0-: UITableView --> UISwipeActionPullView --> UISwipeActionStandardButton

所以最終處理如下:

- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
    /// 注意低版本的Xcode中 不一定是 `_UITableViewCellSwipeContainerView+UISwipeActionPullView+UISwipeActionStandardButton` 而是 `UISwipeActionPullView+UISwipeActionStandardButton`
    
    for (UIView *subView in tableView.subviews) {
        if ([subView isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
            subView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
            for (UIButton *button in subView.subviews) {
                if ([button isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
                    // 修改背景色
                    button.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                }
            }
        } else if ([subView isKindOfClass:NSClassFromString(@"_UITableViewCellSwipeContainerView")]) {
            for (UIView *childView in subView.subviews) {
                if ([childView isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
                    childView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                    for (UIButton *button in childView.subviews) {
                        if ([button isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
                            // 修改背景色
                            button.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                        }
                    }
                }
            }
        }
    }
}

當(dāng)然點(diǎn)擊備注時(shí)伐割,也得修改其背景色候味,否則又會(huì)被重置為淺灰色,代碼如下:

- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath  API_AVAILABLE(ios(11.0)){
    
    UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"備注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        sourceView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        sourceView.superview.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        // Fixed Bug: 延遲一丟丟去設(shè)置 不然無效 點(diǎn)擊需要設(shè)置顏色 不然會(huì)被重置
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            sourceView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
            sourceView.superview.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        });
        
        completionHandler(YES);
    }];
    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[remarkAction]];
    config.performsFirstActionWithFullSwipe = NO;
    
    return config;
}

當(dāng)然有興趣的同學(xué)也可以借助: MGSwipeTableCell 來實(shí)現(xiàn)側(cè)滑備注隔心。


關(guān)于白群,索引條A-Z的實(shí)現(xiàn),筆者這里借助的是:SCIndexView 來實(shí)現(xiàn)的硬霍,關(guān)于其具體的實(shí)現(xiàn)帜慢,筆者這里就不再一一贅述了,有興趣的同學(xué)可以自行學(xué)習(xí)唯卖。

項(xiàng)目中的配置代碼如下粱玲,輕松實(shí)現(xiàn)微信索引Bar:

/// 監(jiān)聽數(shù)據(jù)
@weakify(self);
[[RACObserve(self.viewModel, letters) distinctUntilChanged] subscribeNext:^(NSArray * letters) {
    @strongify(self);
    if (letters.count > 1) {
        self.tempView.hidden = NO;
    }
    self.tableView.sc_indexViewDataSource = letters;
    self.tableView.sc_startSection = 1;
}];

#pragma mark - 初始化
- (void)_setup{
    
    self.tableView.rowHeight = 56.0f;
    self.tableView.backgroundColor = [UIColor clearColor];
    
    // 配置索引模塊
    SCIndexViewConfiguration *configuration = [SCIndexViewConfiguration configuration];
    // 設(shè)置item 距離 右側(cè)屏幕的間距
    configuration.indexItemRightMargin = 8.0;
    // 設(shè)置item 文字顏色
    configuration.indexItemTextColor = MHColorFromHexString(@"#555555");
    // 設(shè)置item 選中時(shí)的背景色
    configuration.indexItemSelectedBackgroundColor = MHColorFromHexString(@"#57be6a");
    /// 設(shè)置索引之間的間距
    configuration.indexItemsSpace = 4.0;
    
    self.tableView.sc_indexViewConfiguration = configuration;
    self.tableView.sc_translucentForTableViewInNavigationBar = true;
}

當(dāng)然通訊錄模塊中,還有個(gè)細(xì)節(jié)處理拜轨,那就是滾動(dòng)過程中抽减,懸浮HeaderView漸變,主要涉及到背景色的漸變和文字顏色的漸變撩轰。當(dāng)然實(shí)現(xiàn)還是比較簡(jiǎn)單的胯甩,就是實(shí)現(xiàn)- (void)scrollViewDidScroll:(UIScrollView *)scrollView;方法,計(jì)算headerView.mh_y的臨界點(diǎn)堪嫂。實(shí)現(xiàn)如下:

// UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    /// 刷新headerColor
    [self _reloadHeaderViewColor];
}

/// 刷新header color
- (void)_reloadHeaderViewColor {
    NSArray<NSIndexPath *> *indexPaths = self.tableView.indexPathsForVisibleRows;
    for (NSIndexPath *indexPath in indexPaths) {
        // 過濾
        if (indexPath.section == 0) {
            continue;
        }
        MHContactsHeaderView *headerView = (MHContactsHeaderView *)[self.tableView headerViewForSection:indexPath.section];
        [self configColorWithHeaderView:headerView section:indexPath.section];
    }
}

/// 配置 header color
- (void)configColorWithHeaderView:(MHContactsHeaderView *)headerView section:(NSInteger)section{
    if (!headerView) {
        return;
    }
    CGFloat insertTop = UIApplication.sharedApplication.statusBarFrame.size.height + 44;
    CGFloat diff = fabs(headerView.frame.origin.y - self.tableView.contentOffset.y - insertTop);
    CGFloat headerHeight = 33.0f;
    double progress;
    if (diff >= headerHeight) {
        progress = 1;
    }else {
        progress = diff / headerHeight;
    }
    [headerView configColorWithProgress:progress];
}



/// MHContactsHeaderView.m
- (void)configColorWithProgress:(double)progress {
    static NSMutableArray<NSNumber *> *textColorDiffArray;
    static NSMutableArray<NSNumber *> *bgColorDiffArray;
    static NSArray<NSNumber *> *selectTextColorArray;
    static NSArray<NSNumber *> *selectBgColorArray;
    
    if (textColorDiffArray.count == 0) {
        UIColor *selectTextColor = MHColorAlpha(87, 190, 106, 1);
        UIColor *textColor = MHColorAlpha(59, 60, 60, 1);
        // 懸浮背景色
        UIColor *selectBgColor = [UIColor whiteColor];
        // 默認(rèn)背景色
        UIColor *bgColor = MHColorAlpha(237, 237, 237, 1);
        
        selectTextColorArray = [self getRGBArrayByColor:selectTextColor];
        NSArray<NSNumber *> *textColorArray = [self getRGBArrayByColor:textColor];
        selectBgColorArray = [self getRGBArrayByColor:selectBgColor];
        NSArray<NSNumber *> *bgColorArray = [self getRGBArrayByColor:bgColor];
        
        textColorDiffArray = @[].mutableCopy;
        bgColorDiffArray = @[].mutableCopy;
        for (int i = 0; i < 3; i++) {
            double textDiff = selectTextColorArray[i].doubleValue - textColorArray[i].doubleValue;
            [textColorDiffArray addObject:@(textDiff)];
            double bgDiff = selectBgColorArray[i].doubleValue - bgColorArray[i].doubleValue;
            [bgColorDiffArray addObject:@(bgDiff)];
        }
    }
    
    NSMutableArray<NSNumber *> *textColorNowArray = @[].mutableCopy;
    NSMutableArray<NSNumber *> *bgColorNowArray = @[].mutableCopy;
    for (int i = 0; i < 3; i++) {
        double textNow = selectTextColorArray[i].doubleValue - progress * textColorDiffArray[i].doubleValue;
        [textColorNowArray addObject:@(textNow)];
        
        double bgNow = selectBgColorArray[i].doubleValue - progress * bgColorDiffArray[i].doubleValue;
        [bgColorNowArray addObject:@(bgNow)];
    }
    
    UIColor *textColor = [self getColorWithRGBArray:textColorNowArray];
    self.letterLabel.textColor = textColor;
    UIColor *bgColor = [self getColorWithRGBArray:bgColorNowArray];
    self.contentView.backgroundColor = bgColor;
}

- (NSArray<NSNumber *> *)getRGBArrayByColor:(UIColor *)color
{
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char resultingPixel[4];
    CGContextRef context = CGBitmapContextCreate(&resultingPixel, 1, 1, 8, 4, rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipLast);
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, CGRectMake(0, 0, 1, 1));
    CGContextRelease(context);
    CGColorSpaceRelease(rgbColorSpace);
    
    double components[3];
    for (int component = 0; component < 3; component++) {
        components[component] = resultingPixel[component] / 255.0f;
    }
    double r = components[0];
    double g = components[1];
    double b = components[2];
    return @[@(r),@(g),@(b)];
}

- (UIColor *)getColorWithRGBArray:(NSArray<NSNumber *> *)array {
    return [UIColor colorWithRed:array[0].doubleValue green:array[1].doubleValue blue:array[2].doubleValue alpha:1];
}

細(xì)節(jié)處理:由于要后期需要彈出 搜索模塊和收回搜索模塊偎箫,所以要保證滾動(dòng)到最頂部時(shí),要確保搜索框完全顯示或者完全隱藏皆串,否則就會(huì)導(dǎo)致在彈出搜索模塊淹办,然后收回搜索模塊,會(huì)導(dǎo)致動(dòng)畫不流暢恶复,影響用戶體驗(yàn)怜森,微信想必也是考慮到如此場(chǎng)景,可以說是谤牡,細(xì)節(jié)滿滿副硅。

解決方案也比較簡(jiǎn)單:判斷列表停止?jié)L動(dòng)后scrollView.contentOffset.y 是否在(-scrollView.contentInset.top, -scrollView.contentInset.top + searchBarH) 范圍內(nèi),判斷當(dāng)前是上拉還是下拉翅萤,上拉隱藏恐疲,下拉顯示。 代碼如下:


/// 細(xì)節(jié)處理:
/// 由于要彈出 搜索模塊套么,所以要保證滾動(dòng)到最頂部時(shí)培己,要確保搜索框完全顯示或者完全隱藏,
/// 不然會(huì)導(dǎo)致彈出搜索模塊,然后收回搜索模塊胚泌,會(huì)導(dǎo)致動(dòng)畫不流暢省咨,影響體驗(yàn),微信做法也是如此
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    /// 注意:這個(gè)方法不一定調(diào)用 當(dāng)你緩慢拖動(dòng)的時(shí)候是不會(huì)調(diào)用的
    [self _handleSearchBarOffset:scrollView];
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // 記錄剛開始拖拽的值
    self.startDragOffsetY = scrollView.contentOffset.y;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    // 記錄剛開始拖拽的值
    self.endDragOffsetY = scrollView.contentOffset.y;
    // decelerate: YES 說明還有速度或者說慣性玷室,會(huì)繼續(xù)滾動(dòng) 停止時(shí)調(diào)用scrollViewDidEndDecelerating
    // decelerate: NO  說明是很慢的拖拽零蓉,沒有慣性笤受,不會(huì)調(diào)用 scrollViewDidEndDecelerating
    if (!decelerate) {
        [self _handleSearchBarOffset:scrollView];
    }
}

/// 處理搜索框顯示偏移
- (void)_handleSearchBarOffset:(UIScrollView *)scrollView {
    // 獲取當(dāng)前偏移量
    CGFloat offsetY = scrollView.contentOffset.y;
    CGFloat searchBarH = 56.0f;
    /// 在這個(gè)范圍內(nèi)
    if (offsetY > -scrollView.contentInset.top && offsetY < (-scrollView.contentInset.top + searchBarH)) {
        // 判斷上下拉
        if (self.endDragOffsetY > self.startDragOffsetY) {
            // 上拉 隱藏
            CGPoint offset = CGPointMake(0, -scrollView.contentInset.top + searchBarH);
            [self.tableView setContentOffset:offset animated:YES];
        } else {
            // 下拉 顯示
            CGPoint offset = CGPointMake(0, -scrollView.contentInset.top);
            [self.tableView setContentOffset:offset animated:YES];
        }
    }
}


以上就是微信通訊錄模塊所涉及到的全部知識(shí)點(diǎn),且難度一般壁公。當(dāng)然感论,通訊錄模塊還有個(gè)重要功能--搜索。??盡管筆者已經(jīng)在 WeChat 項(xiàng)目中實(shí)現(xiàn)了紊册,且效果跟微信如出一撤??。 但是考慮到其邏輯的復(fù)雜性快耿,以及UI的搭建等問題囊陡,后期筆者會(huì)單獨(dú)寫一篇文章,來詳細(xì)描述搜索模塊的技術(shù)實(shí)現(xiàn)和細(xì)節(jié)處理掀亥。敬請(qǐng)期待...

期待

  1. 文章若對(duì)您有些許幫助撞反,請(qǐng)給個(gè)喜歡??,畢竟碼字不易搪花;若對(duì)您沒啥幫助遏片,請(qǐng)給點(diǎn)建議??,切記學(xué)無止境撮竿。
  2. 針對(duì)文章所述內(nèi)容吮便,閱讀期間任何疑問;請(qǐng)?jiān)谖恼碌撞吭u(píng)論指出幢踏,我會(huì)火速解決和修正問題髓需。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:WeChat

主頁

GitHub 掘金 CSDN 知乎
點(diǎn)擊進(jìn)入 點(diǎn)擊進(jìn)入 點(diǎn)擊進(jìn)入 點(diǎn)擊進(jìn)入

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市房蝉,隨后出現(xiàn)的幾起案子僚匆,更是在濱河造成了極大的恐慌,老刑警劉巖搭幻,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咧擂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡檀蹋,警方通過查閱死者的電腦和手機(jī)松申,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來续扔,“玉大人攻臀,你說我怎么就攤上這事∩疵粒” “怎么了刨啸?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長识脆。 經(jīng)常有香客問我设联,道長善已,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任离例,我火速辦了婚禮换团,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宫蛆。我一直安慰自己艘包,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布耀盗。 她就那樣靜靜地躺著想虎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叛拷。 梳的紋絲不亂的頭發(fā)上舌厨,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音忿薇,去河邊找鬼裙椭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛署浩,可吹牛的內(nèi)容都是我干的揉燃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瑰抵,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼你雌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起二汛,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤婿崭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后肴颊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氓栈,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年婿着,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了授瘦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竟宋,死狀恐怖提完,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丘侠,我是刑警寧澤徒欣,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站蜗字,受9級(jí)特大地震影響打肝,放射性物質(zhì)發(fā)生泄漏脂新。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一粗梭、第九天 我趴在偏房一處隱蔽的房頂上張望争便。 院中可真熱鬧,春花似錦断医、人聲如沸滞乙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酷宵。三九已至,卻和暖如春躬窜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背炕置。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工荣挨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人朴摊。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓默垄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親甚纲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子口锭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354