iOS定製UISearchBar導航欄 同步iOS11

HasjOH發表於2017-12-17

五花八門的searchBar

系統原生的UISearchBar在iOS 11經歷了一次變革,高度由原來的44變成了56 (使用預設高度的估計都被坑了),樣式也發生了些微的變化,比如在未輸入狀態下圓角變化,放大鏡圖示和文字的文字不再居中而是靠左了。具體看圖

iOS定製UISearchBar導航欄 同步iOS11

一些主流App也常見在導航欄嵌入searchBar,以網易雲音樂和知乎為例,左邊是主頁,右邊是搜尋頁面 (注意游標)。

iOS定製UISearchBar導航欄 同步iOS11
iOS定製UISearchBar導航欄 同步iOS11

實現思路與案例

核心思想是設定導航欄的titleView和左右的barButtonItem。主要有3種方式

  • 首頁導航欄的titleView使用button來實現,搜尋頁面使用searchBar。
  • 首頁和搜尋頁面導航欄的titleView都是用searchBar,searchBar的樣式針對兩個頁面做不同的修改。這種方式可以重用我們定製的searchBar,減少冗餘。
  • 首頁導航欄titleView使用button來實現,搜尋頁面的使用textField。這種方式更徹底,更靈活,相對也更復雜一些。

為什麼上面的titleView說是button不是其他的?其他的當然也可以實現。button自帶imageView和titleLabel,只需要設定偏移量更容易達到我們想要的,而且檢視層級更少,在流暢性方面更有保證些。

案例

iOS定製UISearchBar導航欄 同步iOS11
iOS定製UISearchBar導航欄 同步iOS11
網易雲音樂首頁和搜尋頁面的導航欄檢視層級,titleView都使用MCSearchBar來實現,並且設定了導航欄左右兩邊的按鈕 。這類似上文所說的第二種思路。

iOS定製UISearchBar導航欄 同步iOS11
iOS定製UISearchBar導航欄 同步iOS11
圖中可以清楚看到知乎首頁導航欄由2個button組成,搜尋頁面使用了textField,這類似上文提到的第三種思路。

實戰

通過自定義SearchBar實現一個如下樣式的導航欄

iOS定製UISearchBar導航欄 同步iOS11
先自定義一個UISearchBar的初始化方法,觀察一下首頁和搜尋頁的異同,像searchField的大小背景色是一致的,可以這部分可以直接給定,而placeholder是不一樣的,所以應該在呼叫的時候提供。以此類推,新建一個OHSearchBar類,一個初始化方法

- (instancetype)initWithFrame:(CGRect)frame placeholder:(NSString *)placeholder textFieldLeftView:(UIImageView *)leftView showCancelButton:(BOOL)showCancelButton tintColor:(UIColor *)tintColor { 
    if (self = [super initWithFrame:frame]) {
        self.frame = frame;
        self.tintColor = tintColor; //游標顏色
        self.barTintColor = [UIColor whiteColor];
        self.placeholder = placeholder;
        self.showsCancelButton = showCancelButton;
        self.leftView = leftView; // 用來代替左邊的放大鏡
        [self setImage:[UIImage imageNamed:@"clear"] forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal]; // 替換輸入過程中右側的clearIcon
    }
    return self;
}
複製程式碼

新建一個首頁OHHomeViewController,設定導航欄的titleView和rightBarButton

// navigation buttom
    UIButton *messageButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [messageButton setImage:[UIImage imageNamed:@"msg"] forState:UIControlStateNormal];
    messageButton.bounds = CGRectMake(0, 0, 30, 30);
    UIBarButtonItem *messageBarButton = [[UIBarButtonItem alloc] initWithCustomView:messageButton];
    self.navigationItem.rightBarButtonItem = messageBarButton;
    
    // search bar
    UIImageView *leftView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"scan"]];
    leftView.bounds = CGRectMake(0, 0, 24, 24);
    self.ohSearchBar = [[OHSearchBar alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 44)
                                              placeholder:@"點選我跳轉"
                                        textFieldLeftView:leftView
                                         showCancelButton:NO
                                                tintColor:[UIColor clearColor]];
    self.navigationItem.titleView = self.ohSearchBar; 
複製程式碼

讓我們來看下效果,左邊為iOS 9,右邊iOS 11

iOS定製UISearchBar導航欄 同步iOS11

這時候可以看到幾處差異

  • searchBar的高度
  • searchBar的textField的放大鏡和文字位置
  • textField的圓角不一致
  • 更細心的還會發現,textField的位置不一致

解決方法: 第一和第二個問題,判斷裝置是否是iOS 11,若是則設定其高度,不是則讓其placeholder居左。關鍵程式碼如下

    if ([[UIDevice currentDevice] systemVersion].doubleValue >= 11.0) {
        [[self.heightAnchor constraintEqualToConstant:44.0] setActive:YES];
    } else {
        [self setLeftPlaceholder];
    }

- (void)setLeftPlaceholder {
    SEL centerSelector = NSSelectorFromString([NSString stringWithFormat:@"%@%@", @"setCenter", @"Placeholder:"]);
    if ([self respondsToSelector:centerSelector]) {
        BOOL centeredPlaceholder = NO;
        NSMethodSignature *signature = [[UISearchBar class] instanceMethodSignatureForSelector:centerSelector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        [invocation setTarget:self];
        [invocation setSelector:centerSelector];
        [invocation setArgument:&centeredPlaceholder atIndex:2];
        [invocation invoke];
    }
}

複製程式碼

對於第三和第四個問題,用KVC獲取textField,並對其進行定製。令textField位置、大小、圓角一致。

- (void)layoutSubviews{
    [super layoutSubviews];

    // search field
    UITextField *searchField = [self valueForKey:@"searchField"];
    searchField.backgroundColor = DARK_BLUE_COLOR;
    searchField.textColor = [UIColor whiteColor];
    searchField.font = [UIFont systemFontOfSize:16];
    searchField.leftView = self.leftView;
    searchField.frame = CGRectMake(0, 8, SCREEN_WIDTH, 28);
    searchField.layer.cornerRadius = 5;
    searchField.layer.masksToBounds = YES;
    [searchField setValue:[UIColor whiteColor] forKeyPath:@"placeholderLabel.textColor"];
    [self setValue:searchField forKey:@"searchField"];
    
    self.searchTextPositionAdjustment = (UIOffset){10, 0}; // 游標偏移量
}
複製程式碼

同樣的,先看下執行效果

iOS定製UISearchBar導航欄 同步iOS11

原本以為這下是沒什麼問題的,結果簡直是坑

iOS定製UISearchBar導航欄 同步iOS11

textFild的長度、位置、圓角都不一樣 解釋下這裡出現的問題

  • 觀察上方圖片上方的searchBar,會發現textField左邊是圓角,右邊是直角,說明是被擷取的。導航欄titleView的範圍就劃分到了那個部分,而下邊的searchBar連rightBarButton都不放過,直接搶佔了位置。推測這是由於iOS 11導航欄檢視層級變化產生的,可以這裡瞭解下www.jianshu.com/p/352f101d6…,或者自行科普,不詳細展開。所以對於searchBar的size設定要小心了,儘量控制在合適的範圍。

  • textField的圓角是不一致的,自定義圓角大小時,取消其本身的圓角樣式

    searchField.borderStyle = UITextBorderStyleNone;
    複製程式碼
  • 檢視檢視層級會發現,iOS 11以下,設定titleView,x的預設座標居然是12,而iOS 11是0。所以設定textField的x座標的話,在iOS 11下必須多出12才會是一致的位置。

    iOS定製UISearchBar導航欄 同步iOS11
    iOS定製UISearchBar導航欄 同步iOS11

修改程式碼上面的程式碼

- (void)layoutSubviews{
    [super layoutSubviews];

    // search field
    UITextField *searchField = [self valueForKey:@"searchField"];
    searchField.backgroundColor = DARK_BLUE_COLOR;
    searchField.textColor = [UIColor whiteColor];
    searchField.font = [UIFont systemFontOfSize:16];
    searchField.leftView = self.leftView;

    if (@available(iOS 11.0, *)) {
        // 檢視檢視層級,在iOS 11之前searchbar的x是12
        searchField.frame = CGRectMake(12, 8, SCREEN_WIDTH*0.8, 28);

    } else {
        searchField.frame = CGRectMake(0, 8, SCREEN_WIDTH*0.8, 28);
    }

    searchField.borderStyle = UITextBorderStyleNone;
    searchField.layer.cornerRadius = 5;

    searchField.layer.masksToBounds = YES;
    [searchField setValue:[UIColor whiteColor] forKeyPath:@"placeholderLabel.textColor"];
    [self setValue:searchField forKey:@"searchField"];
    
    self.searchTextPositionAdjustment = (UIOffset){10, 0}; // 游標偏移量
}
複製程式碼

這時候就是我們想要的結果了。

首頁暫時告一段落,接著開始我們的搜尋頁面。與首頁不同的是需要searchBar與searchController配合使用。新建一個OHSearchController類 新增一個屬性

@property (nonatomic, strong) OHSearchBar *ohSearchBar;
複製程式碼

初始化程式碼

- (instancetype)initWithSearchResultsController:(UIViewController *)searchResultsController searchBarFrame:(CGRect)searchBarFrame placeholder:(NSString *)placeholder textFieldLeftView:(UIImageView *)leftView showCancelButton:(BOOL)showCancelButton barTintColor:(UIColor *)barTintColor{
    if (self = [super initWithSearchResultsController:searchResultsController]) {
        self.ohSearchBar = [[OHSearchBar alloc] initWithFrame:searchBarFrame
                                                  placeholder:placeholder
                                            textFieldLeftView:leftView
                                             showCancelButton:YES
                                                    tintColor:barTintColor];
        
        UIButton *button = [self.ohSearchBar valueForKey:@"cancelButton"];
        button.tintColor = [UIColor whiteColor];
        [button setTitle:@"取消" forState:UIControlStateNormal];
        [self.ohSearchBar setValue:button forKey:@"cancelButton"];
    }
    return self;
}
複製程式碼

接著是我們的檢視控制器OHSearchViewController

    UIImageView *leftView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"search"]];
    leftView.bounds = CGRectMake(0, 0, 24, 24);
    self.ohSearchController = [[OHSearchController alloc] initWithSearchResultsController:self
                                                                           searchBarFrame:CGRectMake(0, 0, SCREEN_WIDTH, 44)
                                                                              placeholder:@"請輸入搜尋內容進行搜尋"
                                                                        textFieldLeftView:leftView
                                                                         showCancelButton:YES
                                                                             barTintColor:BASE_BLUE_COLOR];
    
    [self.ohSearchController.ohSearchBar becomeFirstResponder];
    self.ohSearchController.ohSearchBar.delegate = self;
    [self.ohSearchController.ohSearchBar setLeftPlaceholder];
    self.navigationItem.titleView = self.ohSearchController.ohSearchBar;
    self.navigationItem.hidesBackButton = YES;
複製程式碼

完成這一步後到了互動環節了,點選首頁的searchBar跳轉搜尋頁面,點選搜尋頁面的取消按鈕返回到首頁。 首頁設定searchbar的代理,並完成一下代理方法

- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
    OHSearchViewController *ohSearchViewController = [[OHSearchViewController alloc] init];
    [self.navigationController pushViewController:ohSearchViewController animated:NO];
    return YES;
}
複製程式碼

搜尋頁設定searchbar的代理,並完成一下代理方法

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    [self.navigationController popViewControllerAnimated:NO];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    [self.ohSearchController.ohSearchBar resignFirstResponder];
    // 讓取消按鈕一直處於啟用狀態
    UIButton *cancelBtn = [searchBar valueForKey:@"cancelButton"];
    cancelBtn.enabled = YES;
}
複製程式碼

這時候問題又出現了,點選搜尋頁面的取消按鈕,沒有跳回首頁而是還在這個頁面。但是可以看到螢幕的閃動。通過列印訊息發現,點了取消按鈕,執行了首頁的- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar方法。 仔細推敲之後想明白了原因是沒有取消第一響應者,加上導航欄的互動機制,pop到上個頁面的時候並不會進行頁面重新整理導致了這個問題。 解決辦法在首頁要push搜尋頁面的時候取消第一響應者

- (void)viewWillDisappear:(BOOL)animated {
    [self.ohSearchBar resignFirstResponder];
}
複製程式碼

到此,便大功告成了。可以看下原始碼加深理解。 專案原始碼傳送門: HasjOH/OHSearchBarInNaviBar

相關文章