SearchBar trên Navigation Bar

Mình tạo SearchBar cho Navigation bị lỗi như này có ai biết fix như nào không ạ?

Phần đầu. Không nên dùng UISearchBar ( + các thứ liên quan, UISearchController ).

Lý do là UISearchBar anh Apple code kiểu frame-based coordinate từ hồi iOS 7, lúc đó chưa có AutoLayout. Thiết bị mà iOS cài chỉ có 2 kích thước màn hình iPhone 5 và iPhone 4, nên việc tạo layout để xử lý UI đa màn hình không cần thiết. Khi Apple ra iPhone 6 và 6 plus, iPhone có tổng cộng 4 kích thước màn hình khác nhau (iPhone 4, iPhone 5, iPhone 6, iPhone 6+) nên Apple giới thiệu AutoLayout để sử dụng 1 bản UI design duy nhất có thể hoạt động tốt trên 4 iPhone khác nhau. Ban đầu AutoLayout code chưa tối ưu, còn bugs, nên gây ra độ trể (delay) trong quá trình render các views. Đến iOS 8, AutoLayout chạy tốt trên iPhone 5 trở lên nhưng lại bị trể trên iPhone 4. Đến iOS 9, Apple mới khắc phục được tất cả vấn đề đến iOS.

Cách thiết kế theo frame-based coordinate là cách tính toán vị trí (position), kích thước (width, height) từng UIView và UIControl dựa vào screen width và screen height. (các giá trị được tính toán từ các properties scale, bounds của UIScreen). Dev phải thiết lập công thức cho từng view property. Kết quả theo đơn vị pt. Auto layout hoạt động trên unit coordinate của parent view bounds, tính toán view property theo tỉ lệ từ 0 đến 1 (điểm (0,0) ở top-left và (1,1) ở bottom-right).

Giả sử UISearchBar nằm trong 1 view tên là parentView, UISearchBar tính theo frame dựa trên parentView.bounds. Khi bắt đầu render lần đầu, AutoLayout xác định vị trí của parentView, bounds của parentView được gán các giá trị pt tương ứng. Sau đó UISearchBar dựa trên bounds của parent view để tính toán các giá trị pt cho property của mình, parentView và UISearchBar đều ở vị trí mong muốn. Sau đó mọi chuyện tồi tệ hơn :relieved:, user kích hoạt event làm thay đổi AutoLayout của parentView, AutoLayout bị rơi vào invalid state. Lần gọi rendering lifecycle tiếp theo, app phát hiện parentView bị invalid state. App tính toán lại bounds mới cho parentView, parentView ở vị trí mong muốn, xong parentView cho tất cả child views bị invalid state. Lúc này UISearchBar bị gán invalid state, nhưng anh UISearchBar tính theo frame-based, ảnh không biết gì cả, ảnh bỏ qua cái thông báo của parentView. Trong rendering lifecycle, có 1 bộ rendering duyệt hết (như duyệt tree) từ parentView đến UISearchBar, do 2 anh không đồng nhất nên lúc ảnh hiện thị UISearchBar lúc dài ra, lúc ngắn lại.

Cách giải quyết cho vấn đề: thiết kế theo frame-based tất cả views, hoặc là autolayout tất cả views, không nên chơi nửa nạc nửa mở, vừa frame vừa autolayout. Kể cả dùng custom layout từ libs ngoài cũng thế. Không dùng layout để libs ngoài chung với AutoLayout luôn.


Phần hai, về NavigationBar.

Quay lại kể chuyện lịch sử tiếp, lúc AutoLayout được Apple giới thiệu. Một số vấn đề conflict xảy ra đối với các Container View Controller (UITabbarController, UINavigationController, …). Vì trên AppleStore có nhiều app sử dụng cái container view controller của anh Apple, nên anh Apple phải cố gắng làm đủ kiểu để nó chạy được trên các iOS mới. Apple đưa theo vào UIViewController các property mới topLayoutGuidebottomLayoutGuide (hiện đã deprecated) để cho UINavigationBar và UITabbar hoạt động được với main view controller dùng AutoLayout. Ý tưởng của Apple là dev không phần phải lo lắng về việc AutoLayout sẽ có conflict, gây bể UI, dev cứ thiết lập các layout constraints cho view controller có UINavigationBar hay UITabbar như đối với view controller không có bar. Nhưng khi build app, nhiều vấn đề rất khó chịu phát sinh do 2 cái guilde này gây nên: (sao cái ViewController ở trên mất nội dung ở top vậy? Sao cái view bị đẩy xuống dưới, push qua lại vài mình cái navigation bar nó ẩn luôn rồi, tại sao?) chưa kể còn dính vàì bug trên Interface Builder (tạo navigation controller mà không thấy navigation bar ở đâu cả, sao không tạo được seque, blabla,…). Nên dev bỏ lơ luôn, không dùng mấy layoutGuide nữa, và anh Apple phải đưa nó vào deprecated, và quảng cáo safeAreaLayoutGuide, nhưng kết quả vẫn thế.

Kế tiếp là chính NavigationBar, các API mà Apple hỗ trợ cho NavigationBar không được phong phú, không dễ customize. UINavigationBar hướng dẫn cách thiết kế navigation bar hiệu quả (1 text hoặc view giới hạn width <= navigation.width / 2, có thể 1 nút bên trái, có thể 1 nút bên phải, và … hết) hơn là dùng UINavigationBar trong mọi trường hợp. Nếu bạn có iPhone sẽ có nhiều app có navigation bar rất oái ăm, trong khi đó UINavigationBar bị hạn chế.

Cách giải quyết:

  • Không dùng UINavigationBar, UITabbar, tự code hoàn toàn từ UIView.
  • Chỉ nên kế thừa từ UIViewController, không kế thừa từ UITabbarViewController hay UINavigationController.
5 Likes

Em cảm ơn anh đã trả lời rất chi tiết ạ. Cho e hỏi vài cái ạ:

  • Phần đầu: Không dùng UISearchBar và UISearchController thì cái ô search đó mình tự làm bằng TextField ạ?
  • Phần sau: tự code Navigation bằng UIView vậy lúc push từ màn hình này sang màn hình kia bình thuường e dùng navigationController.pushViewController thì nếu không dùng navigation nữa thì làm như nào ạ?

Mình đọc lại thấy chỉ đưa ra gợi ý, chưa đưa ra solution, xin lỗi em nha. :grin:

Navigation thì em vẫn dùng NavigationController. Tạo 1 class mới kế thừa UINavigationController, và ẩn navigation bar trong viewDidLoad

override func viewDidLoad() {
    super.viewDidLoad()
    self.setNavigationBarHidden(true, animated: false)
}

Về SearchBar, cần tạo 2 class, 1 class kế thừa từ UIView và 1 class SearchBarDelegate để xử lý các event từ SearchBar, giống với thiết kế của UISearchBar.

Thiết kế gần như vầy:

Bên View Controller phần NewFeed:

  • SearchBar (UIView), trừ trái sang phải

  • Search icon (ImageView, bên trái)

  • Khung search (UIView, ở giữa) // Cái này là search bar giả, không phải hàng thật
    – UILabel (text = “Search”)

  • NavigationBar (UIView), từ trái sang phải

  • Camera Icon (UIButton + UIImageView)

  • Search Bar

  • Message Icon (UIButton+ UIImageView)

View Controller’s view, từ trên xuống dưới

  • Navigation Bar
  • UITableView

Phần UIButton + UIImageView dùng 2 UIControl thay vì 1 UIControl (UIImage) thì để dễ customize cho icon. UIButton không có nhiều method để customize trong trường hợp button chỉ là 1 image, thiết kế:

  • UIView đống vai trò là parent
  • UIImage (bounds = parent.bounds)
  • UIButton (bounds = parent.bounds, text = “”)
  • Button nằm phía trước UIImage

Tiếp, khi user tap vào SearchBar, thì View Controller gọi navigation controller, và navigation controller push sang View Controller khác, là nơi thao tác thật sự để search.

Trong SearchBar’s view, gán cho nó thêm UITapGesture để nhận event khi user tap vào. Khi tap vì sẽ thực hiện push. Còn cách push chi tiết, bạn tham khảo thêm tại View Controller Guide - Customizing the Transition Animation, bạn sẽ kiểm soát quá trình push để không có animation nào được thực hiện, hiển thị view controller ngay tức khắc (giống FB App).


Trong View Controller mới, thiết kế cũng tương tự:

View Controller’s view

  • Navigation Bar (UIView), từ trên xuống dưới
  • UITableView
  • Table cell: Recent searches
  • Table cell: Today’s popular searches

NavigationBar (UIView), từ trái sang phải

  • Search Bar (custom view)
  • Cancel Button

SearchBar (UIView), từ trái sang phải

  • Search Icon (UIImage)
  • Search Field (UITextField, placeholder = “Search”)
  • BarCode (UIImage + UIButton)

Phần tạo Delegate gì đó, bạn tự tạo thêm nha. Mình viết nãy giờ mệt quá :sob:

2 Likes

Okie a. Để e thử làm theo hướng của a nhé. Cám ơn a nhiều nha :smiley: :smiley:

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?