Bài viết
Sử dụng MVVM để tableView của bạn trở nên mượt mà hơn
Như các bạn đã biết thì hiển thị một tập hợp các dữ liệu là một trong những task phổ biến nhất trong quá trình xây dựng một ứng dụng. Apple SDK đã cung cấp cho chúng ta 2 công cụ để làm việc này, đó là UITableView và UICollectionView.

Table view và collection view đều được thiết kế để hỗ trợ việc hiển thị dữ liệu mà có thể cuộn được. Tuy nhiên khi khối lượng dữ liệu cần hiển thị là rất lớn thì chúng ta còn cần phải đảm bảo việc mượt mà trong các thao tác vuốt, cuộn nữa. Bài viết này dựa trên kinh nghiệm của riêng tôi với table view và collection view trong việc hiển thị dữ liệu lớn một cách mượt mà.
Đầu tiên chúng ta hãy cùng nhau nhìn lướt qua 2 component trên để nắm được những thành phần cơ bản của chúng. UITableView được tối ưu để hiển thị dữ liệu theo các dòng – row hay còn gọi là cell. Việc hiển thị dữ liệu được thực hiện qua các delegates.
UICollectionView thì linh hoạt hơn, chúng ta có thể tùy chỉnh bố cục, layout cho các thành phần được hiển thị trong đó. Tuy nhiên, để có được sự linh hoạt đó thì chúng ta cũng phải thực hiện nhiều tác vụ chi tiết mà vẫn đảm bảo được performance của ứng dụng.
 Ở trong bài viết này, tôi sẽ áp dụng phương pháp cho TableView, và tất nhiên các bạn cũng có thể áp dụng nó cho Collection View
Sự tương tác giữa UITableView và UITableViewCell được mô tả qua những event sau:
- TableView gọi tới cell sẽ được hiển thị trên view:
- (tableView(_:cellForRowAt:)).
- TableView chuẩn bị hiển thị cell:
- (tableView(_:willDisplay:forRowAt:)).
- Cell đã được gỡ ra khỏi TableView
- (tableView(_:didEndDisplaying:forRowAt:)).
Trong tất cả những event trên, Table View sẽ truyền vào một giá trị index (row) tương ứng với từng dòng dữ liệu. Dưới đây là mô tả một vòng đời của một đối tượng UITableViewCell:

Đầu tiên, method tableView(_:cellForRowAt:) sẽ cần phải thực thi càng nhanh càng tốt. Method này được gọi mỗi lần một cell chuẩn bị được hiển thị, tốc độ thực thi càng nhanh thì tác vụ cuộn hay vuốt sẽ càng trở nên mượt mà.
Để làm được việc này thì chúng ta có thể làm một vài thao tác theo chỉ dẫn của tài liệu Apple như sau:
| 1 2 3 4 5 6 7 8 9 10 | override  func  tableView ( _   tableView :   UITableView ,   cellForRowAt  indexPath :   IndexPath )   ->   UITableViewCell   {      // Table view cells are reused and should be dequeued using a cell identifier.      let  cell   =   tableView . dequeueReusableCell ( withIdentifier :   "reuseIdentifier" ,   for :   indexPath )      // Configure the cell ...      return   cell } | 
Sau khi khai báo một cell instance có thể tái sử dụng được (dequeueReusableCell(withIdentifier:for:)), chúng ta cần phải config nó bằng việc gán những giá trị cần thiết cho các property của nó.
Khởi tạo View Model cho đối tượng Cell
Có một cách để tất cả những property mà chúng ta cần hiển thị trở nên dễ dàng truy cập và gán giá trị vào hơn đó là sử dụng mô hình MVVM. Giả dụ rằng khi chúng ta cần hiển thị một tập hợp những user trong table view thì có thể định nghĩa lớp Model cho user như sau:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | enum   Role :   String   {      case   Unknown   =   "Unknown"      case   User   =   "User"      case   Owner   =   "Owner"      case   Admin   =   "Admin"      static   func  get ( from :   String )   ->   Role   {          if   from   ==   User . rawValue   {              return   . User          }   else   if   from   ==   Owner . rawValue   {              return   . Owner          }   else   if   from   ==   Admin . rawValue   {              return   . Admin          }          return   . Unknown      } } struct   User   {      let  avatarUrl :   String      let  username :   String      let  role :   Role      init ( avatarUrl :   String ,   username :   String ,   role :   Role )   {          self . avatarUrl   =   avatarUrl          self . username   =   username          self . role   =   role      } } | 
Sau đó chúng ta định nghĩa View Model cho User như sau:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct   UserViewModel   {      let  avatarUrl :   String      let  username :   String      let  role :   Role      let  roleText :   String      init ( user :   User )   {          // Avatar          avatarUrl   =   user . avatarUrl          // Username          username   =   user . username          // Role          role   =   user . role          roleText   =   user . role . rawValue      } | 
Đổ dữ liệu theo cách bất đồng bộ và Cache View Models:
Sau khi chúng ta đã khai báo Model và View Model, hãy dung chúng để đổ dữ liệu cho user qua web service, và tất nhiên chúng ta muốn mang đến trải nghiệm tốt nhất có thể, do đó chúng ta phải lưu ý những thứ sau:
- Tránh block main thread trong khi đổ dữ liệu.
- Update table View ngay sau khi nhận dữ liệu về.
Điều này có nghĩa rằng chúng ta sẽ đổ dữ liệu một theo hướng bất đồng bộ. Task này sẽ được thực thi qua một controller riêng biệt để tách những logic của việc đổ dữ liệu ra khỏi Model và ViewModel :
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | class   UserViewModelController   {      fileprivate  var   viewModels :   [ UserViewModel ? ]   =   [ ]      func  retrieveUsers ( _   completionBlock :   @ escaping   ( _   success :   Bool ,   _   error :   NSError ? )   ->   ( ) )   {          let  urlString   =   . . .   // Users Web Service URL          let  session   =   URLSession . shared          guard  let  url   =   URL ( string :   urlString )   else   {              completionBlock ( false ,   nil )              return          }          let  task   =   session . dataTask ( with :   url )   {   [ weak  self ]   ( data ,   response ,   error )   in              guard  let  strongSelf   =   self   else   {   return   }              guard  let  data   =   data   else   {                  completionBlock ( false ,   error  as   NSError ? )                  return              }              let  error   =   . . .   // Define a NSError for failed parsing              if   let  jsonData   =   try ?   JSONSerialization . jsonObject ( with :   data ,   options :   . allowFragments )   as ?   [ [ String :   AnyObject ] ]   {                  guard  let  jsonData   =   jsonData   else   {                      completionBlock ( false ,    error )                      return                  }                  var   users   =   [ User ? ] ( )                  for   json   in   jsonData   {                      if   let  user   =   UserViewModelController . parse ( json )   {                          users . append ( user )                      }                  }                  strongSelf . viewModels   =   UserViewModelController . initViewModels ( users )                  completionBlock ( true ,   nil )              }   else   {                  completionBlock ( false ,   error )              }          }          task . resume ( )      }      var   viewModelsCount :   Int   {          return   viewModels . count      }      func  viewModel ( at  index :   Int )   ->   UserViewModel ?   {          guard  index   >=   0   &&   index   <   viewModelsCount   else   {   return   nil   }          return   viewModels [ index ]      } } private   extension   UserViewModelController   {      static   func  parse ( _   json :   [ String :   AnyObject ] )   ->   User ?   {          let  avatarUrl   =   json [ "avatar" ]   as ?   String   ? ?   ""          let  username   =   json [ "username" ]   as ?   String   ? ?   ""          let  role   =   json [ "role" ]   as ?   String   ? ?   ""          return   User ( avatarUrl :   avatarUrl ,   username :   username ,   role :   Role . get ( from :   role ) )      }      static   func  initViewModels ( _   users :   [ User ? ] )   ->   [ UserViewModel ? ]   {          return   users . map   {   user  in              if   let  user   =   user   {                  return   UserViewModel ( user :   user )              }   else   {                  return   nil              }          }      } } | 
Bây giờ chúng ta có thể nhận dữ liệu và update tableView theo hướng bất đồng bộ như sau:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class   MainViewController :   UITableViewController   {      fileprivate  let  userViewModelController   =   UserViewModelController ( )      override  func  viewDidLoad ( )   {          super . viewDidLoad ( )          userViewModelController . retrieveUsers   {   [ weak  self ]   ( success ,   error )   in              guard  let  strongSelf   =   self   else   {   return   }              if   ! success   {                  DispatchQueue . main . async   {                      let  title   =   "Error"                      if   let  error   =   error   {                          strongSelf . showError ( title ,   message :   error . localizedDescription )                      }   else   {                          strongSelf . showError ( title ,   message :   NSLocalizedString ( "Can't retrieve contacts." ,   comment :   "The message displayed when contacts can’t be retrieved." ) )                      }                  }              }   else   {                  DispatchQueue . main . async   {                      strongSelf . tableView . reloadData ( )                  }              }          }      }      [ . . . ] } | 
Chúng ta có thể dùng đoạn mã trên để truyền dữ liệu theo nhiều cách khác nhau:
- Đặt vào viewDidLoad(). khi chúng ta load table view một lần duy nhất.
- Đặt vào viewWillAppear(_:). khi tableView cần phải load nhiều lần.
- Còn lại tùy theo yêu cầu của người dùng, chúng ta có thể đặt khối lệnh này ở trong method thực thi yêu cầu đó. Ví dụ như kéo xuống để refesh..vv
Load ảnh bất đồng bộ và lưu lại cache
Việc load và hiển thị ảnh ở trong cell là rất phổ biến, và để cho ra một tác vụ mượt mà nhất có thể, chúng ta hiển nhiên không muốn block main thread để tải ảnh. Có một cách để load ảnh bất đồng bộ đó là tạo một lớp wrapper qua URLSession:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | extension   UIImageView   {      func  downloadImageFromUrl ( _   url :   String ,   defaultImage :   UIImage ?   =   UIImageView . defaultAvatarImage ( ) )   {          guard  let  url   =   URL ( string :   url )   else   {   return   }          URLSession . shared . dataTask ( with :   url ,   completionHandler :   {   [ weak  self ]   ( data ,   response ,   error )   ->   Void   in              guard  let  httpURLResponse   =   response  as ?   NSHTTPURLResponse  where  httpURLResponse . statusCode   ==   200 ,                  let  mimeType   =   response ? . mimeType ,   mimeType . hasPrefix ( "image" ) ,                  let  data   =   data  where  error   ==   nil ,                  let  image   =   UIImage ( data :   data )              else   {                  return              }          } ) . resume ( )      } } | 
Ở đay chúng ta sẽ load từng tấm ảnh tại background thread và chỉ update UI khi mà đã có đủ data cần có. Bên cạnh đó chúng ta cũng có thể sử dụng những thư viện như SDWebImage hay AlamofireImage.
Tùy chỉnh Cell
Để tận dụng hoàn toàn những lợi ích từ View Model, chúng ta có thể tùy chỉnh User cell bằng cách subclass nó ( từ UITableViewCell cho TableView và từ UICollectionViewCell cho collection view). Hướng tiếp cận ở đây đó là tạo một outlet cho từng property của Model mà cần được hiển thị và khởi tạo chúng từ View Model:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class   UserCell :   UITableViewCell   {      @ IBOutlet  weak  var   avatar :   UIImageView !      @ IBOutlet  weak  var   username :   UILabel !      @ IBOutlet  weak  var   role :   UILabel !      func  configure ( _   viewModel :   UserViewModel )   {          avatar . downloadImageFromUrl ( viewModel . avatarUrl )          username . text   =   viewModel . username          role . text   =   viewModel . roleText      } } | 
Sử dụng Opaque Layers và tránh dùng Gradients
Sử dụng layer trong suốt hay kiểu Gradients đòi hỏi một khối lượng tính toán lớn, do vậy cũng sẽ ảnh hưởng đến performance. Vì thế nếu có thể, chúng ta nên tránh sử dụng chúng, cũ thể chúng ta có thể dùng màu RGB, ví dụ như UIColor.clear:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class   UserCell :   UITableViewCell   {      @ IBOutlet  weak  var   avatar :   UIImageView !      @ IBOutlet  weak  var   username :   UILabel !      @ IBOutlet  weak  var   role :   UILabel !      func  configure ( _   viewModel :   UserViewModel )   {          setOpaqueBackground ( )          [ . . . ]      } } private   extension   UserCell   {      static   let  defaultBackgroundColor   =   UIColor . groupTableViewBackgroundColor      func  setOpaqueBackground ( )   {          alpha   =   1.0          backgroundColor   =   UserCell . defaultBackgroundColor          avatar . alpha   =   1.0          avatar . backgroundColor   =   UserCell . defaultBackgroundColor      } | 
Sau khi chỉnh sửa xong, chúng ta tổng hợp tất cả các thứ lại như sau:
| 1 2 3 4 5 6 7 8 9 10 11 | override  func  tableView ( _   tableView :   UITableView ,   cellForRowAt  indexPath :   IndexPath )   ->   UITableViewCell   {      let  cell   =   tableView . dequeueReusableCell ( withIdentifier :   "UserCell" ,   for :   indexPath )   as !   UserCell      if   let  viewModel   =   userViewModelController . viewModel ( at :   ( indexPath  as   NSIndexPath ) . row )   {          cell . configure ( viewModel )      }      return   cell } | 
Tổng kết
Như vậy là qua bài viết trên, chúng ta đã có thể tìm ra một trong những ccách làm tối ưu performance cho tableView, collectionView nói riêng và ứng dụng của chúng ta nói chung. Bạn có thể download ví dụ trên tại đây.
- Viết ứng dụng Smartphone và Tablet
- 5 lý do sở hữu một ứng dụng di động là cần thiết đối với doanh nghiệp vừa và nhỏ
- Các nền tảng công nghệ hỗ trợ cho khởi nghiệp tiết kiệm, hiệu quả,...
- Hệ thống điều hành, tìm gọi và quản lý xe sử dụng công nghệ mới
- Ứng dụng bán hàng trên smartphone, smart TV, mạng xã hội...
- Hướng dẫn cài ứng dụng, phần mềm cho Android trực tiếp bằng tập tin APK
- IoT là gì? ứng dụng của IoT trong cuộc sống hiện đại
- Khắc phục lỗi đăng nhập Windows 10, không thể login vào Windows 10
- Platform là gì?
- Cách đổi tên thiết bị Android
- Hệ thống order chuyên nghiệp cho quán ăn, cafe, nhà hàng...
- 100 Website đặt backlink miễn phí chất lượng
DVMS chuyên:
  - Tư vấn, xây dựng, chuyển giao công nghệ Blockchain, mạng xã hội,...
  - Tư vấn ứng dụng cho smartphone và máy tính bảng, tư vấn ứng dụng vận tải thông minh, thực tế ảo, game mobile,...
  - Tư vấn các hệ thống theo mô hình kinh tế chia sẻ như Uber, Grab, ứng dụng giúp việc,...
  - Xây dựng các giải pháp quản lý vận tải, quản lý xe công vụ, quản lý   xe doanh nghiệp, phần mềm và ứng dụng logistics, kho vận, vé xe điện   tử,...
  - Tư vấn và xây dựng mạng xã hội, tư vấn giải pháp CNTT cho doanh nghiệp, startup,...
Vì sao chọn DVMS?
  - DVMS nắm vững nhiều công nghệ   phần mềm, mạng và viễn thông. Như Payment gateway, SMS gateway, GIS,   VOIP, iOS, Android, Blackberry, Windows Phone, cloud computing,…
  - DVMS có kinh nghiệm triển khai các hệ thống trên các nền tảng điện toán đám mây nổi tiếng như Google, Amazon, Microsoft,…
  - DVMS có kinh nghiệm thực tế tư vấn, xây dựng, triển khai, chuyển giao,   gia công các giải pháp phần mềm cho khách hàng Việt Nam, USA, Singapore,   Germany, France, các tập đoàn của nước ngoài tại Việt Nam,…
  
  Quý khách xem Hồ sơ năng lực của DVMS tại đây >> 
  
  Quý khách gửi yêu cầu tư vấn và báo giá tại đây >>
