Get started with Instagram/IGListKit

IGListKit is Instagram’s new UICollectionView framework, which uses data driven to create a faster and more flexible list control.

Project address: github.com/Instagram/I… Author’s speech: large-scale refactoring, rewrite them Feed experience Address: official document sets. Making. IO/IGListKit/official address: the original sets. Making. IO/IGListKit/m…

IGListKit tutorial – Get a better understanding of IGListKit by simulating simple REQUIREMENTS for IMPLEMENTING NASA

Translation level is limited, in does not affect the original meaning of the premise of appropriate modification, there is an error in the message area correct


Translator: MTMwMjAwOTkzNjY=


Preface:

The translator is currently a junior iOS developer. It is inevitable that there are many inappropriate translations or comments. Welcome to correct them. We learned about this framework during the refactoring phase of the Q4 project, and it was named one of the 33 iOS open Source libraries to know in 2017. At the beginning, I read the author’s speech recommended at the head of the article. He explained that the reason for rewriting the Feed was Technical Debt. For example, when people scroll through the Instagram timeline, besides posts, they will also see the recommendations of excellent creators and other information. Because this information is not included in the FeedItem model, it can be difficult to implement.

Diff algorithm is used to compare whether there is any change in the Model to achieve local update of the interface.

This paper is only a one-sided translation of the Demo document provided by the official, without in-depth study of various principles for the time being. At the head of the article is a Demo that simulates implementing NASA’s business requirements to help you better understand IGListKit’s strengths.


Demo – Implement INSTAGRAM post content

Create the Model and bind

This article will teach you how to use IGListKit through a practical example. In this article you will learn:

  • A design specification for a combination of a top-level Model and multiple ViewModels
  • useListBindingSectionControllerUpdate the Cell
  • Response events and agent processing between the Cell and Controller
  • Update the UI with locally changed data

start

We can do this with the following example. Download rnystROM /IGListKit-Binding-Guide. This project has passed the CocoaPods integration IGListKit, so we can directly open: ModelingAndBinding – Starter/ModelingAndBinding. Xcworkspace

The goal of this project is to implement an Instagram-like detail page, so we can think about its data model:

  • The top Cell displays the user name and publication time label
  • The middle Cell contains an image loaded through the URL
  • The number of likes will be displayed at the bottom of the picture, and we will also add interactive functions. When the user clicks this button, the number of likes will increase
  • At the bottom is the comment list, which displays an indefinite number of comments, mainly including user names and comment content

Remember, IGListKit implements a Model for a Section Controller. All of the above cells are associated with a top-level Model. We create a Post model that contains all the data required by the cell.

A common mistake is to create one Model and one Section Controller for each individual cell. In this example, we are going to create a very complex top-level Model that will contain data for users, images, comments, likes, and so on.


Create a Model

Create post.swift in the open project

import IGListKit

final class Post: ListDiffable {

  // 1
  let username: String
  let timestamp: String
  let imageURL: URL
  let likes: Int
  let comments: [Comment]

  // 2
  init(username: String, timestamp: String, imageURL: URL, likes: Int, comments: [Comment]) {
    self.username = username
    self.timestamp = timestamp
    self.imageURL = imageURL
    self.likes = likes
    self.comments = comments
  }

}
Copy the code
  • The best practice is to use all attributesletDeclare it so it can’t be modified again. The above code will be prompted “Comment model cannot be found”, which can be ignored here.

The code above already follows the ListDiffable protocol, so implement it in Post.

// MARK: ListDiffable

func diffIdentifier() -> NSObjectProtocol {
  // 1
  return (username + timestamp) as NSObjectProtocol
}

// 2
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
  return true
}
Copy the code

1. Derive a unique identifier for each article. Since it is impossible for a post to have the same user name and Posting time, we use these two attributes to generate identifiers; 2. Use ListBindingSectionController is a core requirement, if the two Model with the same diffIdentifier, so they must be the same, so that the Section Controller can compare the View Model

Translator’s note: For the origin of this algorithm, you can read “author’s Speech” at the head of the article, or refer to the article:
Sets/IGListKit practice

View Models

Create the comment.swift file and complete the code by referring to the following requirements:

  • Username is a String
  • String text
  • Implement ListDiffable

Reference code:

import IGListKit 

final class Comment: ListDiffable { 
    let username: String 
    let text: String 
    
    init(username: String, text: String) { 
        self.username = username 
        self.text = text 
    } 
    
    // MARK: ListDiffable 
    func diffIdentifier() -> NSObjectProtocol { 
        return (username + text) as NSObjectProtocol 
    } 
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool { 
        return true 
    }
}
 
Copy the code

By definition, an object actually corresponds to an identifier. When checking whether two objects are equal, we can directly test the diffIdentifier

Comments are used in the above Post model. The number of comments on each Post is different. For each Comment, we use a Cell to display it.

However, there may be a new concept. Is even using the ListBindingSectionController, we still need to ImageCell, ActionCell, UserCell create Model.

Each bound Section Controller is like a little IGListKit. The Section Controller contains an array of View models and assembles them into the specified Cell. Now create the Model for each Cell

Create UserViewModel. Swift:

import IGListKit

final class UserViewModel: ListDiffable {

  let username: String
  let timestamp: String

  init(username: String, timestamp: String) {
    self.username = username
    self.timestamp = timestamp
  }

  // MARK: ListDiffable

  func diffIdentifier() -> NSObjectProtocol {
    // 1
    return "user" as NSObjectProtocol
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    // 2
    guard let object = object as? UserViewModel else  { return false }
    return username == object.username
    && timestamp == object.timestamp
  }

}
Copy the code

Since there is only one UserViewModel per post, we can hardcode an identifier, which emphasizes using only one such Model and Cell

  • It is important to write a good equality method for the ViewModel implementation. Any time a change occurs that forces the model to be unequal, the Cell is refreshed.

Refer to the UserViewModel and implement the other two viewModels

import IGListKit 
final class ImageViewModel: ListDiffable { 
    let url: URL 
    init(url: URL) { 
        self.url = url 
    } 
    
    // MARK: ListDiffable 
    func diffIdentifier() -> NSObjectProtocol { 
        return "image" as NSObjectProtocol 
    } 
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool { 
        guard let object = object as? ImageViewModel else { return false } 
        return url == object.url 
    } 
} 


final class ActionViewModel: ListDiffable { 
    let likes: Int 
    
    init(likes: Int) { 
        self.likes = likes 
    } 
    
    // MARK: ListDiffable 
    
    func diffIdentifier() -> NSObjectProtocol { 
        return "action" as NSObjectProtocol 
    } 
    
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool { 
        guard let object = object as? ActionViewModel else { return false } 
        return likes == object.likes 
    } 
} 
Copy the code

useListBindingSectionController

Now we have the following ViewModel, which can be found in a post:

  • UserViewModel
  • ImageViewModel
  • ActionViewModel
  • Comment

Next we using ListBindingSectionController implementation Model and Cell binding.

This Controller gets a top-level Model (Post), requests an array of ViewModels from the data source, and binds them to the Cell once it gets the ViewModels.

Create PostSectionController. Swift

final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource {

  override init() {
    super.init()
    dataSource = self
  }

}
Copy the code

Code above you can see we inherit ListBindingSectionController, suggesting PostSectionController receive Post model.

The next three methods implement the Data Source protocol:

  • Returns an array containing all viewModels used by the Post model
  • Return the ViewModel size
  • Given ViewModel returns a Cell

First, focus on the conversion of Post and ViewModels

// MARK: ListBindingSectionControllerDataSource

func sectionController(
  _ sectionController: ListBindingSectionController<ListDiffable>,
  viewModelsFor object: Any
  ) -> [ListDiffable] {
    // 1
    guard let object = object as? Post else { fatalError() }
    // 2
    let results: [ListDiffable] = [
      UserViewModel(username: object.username, timestamp: object.timestamp),
      ImageViewModel(url: object.imageURL),
      ActionViewModel(likes: object.likes)
    ]
    // 3
    return results + object.comments
}
Copy the code

Create the ViewModel array by splitting the Post model into smaller models; Add the required API to return the Size of each ViewModel

func sectionController( _ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int ) -> CGSize { // 1 guard let width = collectionContext? .containerSize.width else { fatalError() } // 2 let height: CGFloat switch viewModel { case is ImageViewModel: height = 250 case is Comment: height = 35 // 3 default: height = 55 } return CGSize(width: width, height: height) }Copy the code

Finally, the implementation API returns the corresponding Cell for each ViewModel. The various cells are available, check them out at main.storyboard or refer to the code below

func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell { let identifier: String switch viewModel { case is ImageViewModel: identifier = "image" case is Comment: identifier = "comment" case is UserViewModel: identifier = "user" default: identifier = "action" } guard let cell = collectionContext? .dequeueReusableCellFromStoryboard(withIdentifier: identifier, for: self, at: index) else { fatalError() } if let cell = cell as? ActionCell { cell.delegate = self } return cell }Copy the code

Bind model-cell

PostSectionController is now implemented to create different viewModels, sizes, and cells. The Cell through ListBindingSectionController receive the ViewModel.

Implement ListBindable and the Cell can receive the ViewModel.

Next, refine each Cell

Perfect ImageCell. Swift

import UIKit
import SDWebImage
// 1
import IGListKit

// 2
final class ImageCell: UICollectionViewCell, ListBindable {

  @IBOutlet weak var imageView: UIImageView!

  // MARK: ListBindable

  func bindViewModel(_ viewModel: Any) {
    // 3
    guard let viewModel = viewModel as? ImageViewModel else { return }
    // 4
    imageView.sd_setImage(with: viewModel.url)
  }

}
Copy the code

Bind the remaining cells


The Controller calls

Back to ViewController.swift, after ViewDidload() and before setting up the dataSource and collectionView, add the following test code.

data.append(Post( username: "@janedoe", timestamp: "15min", imageURL: URL(string: "https://placekitten.com/g/375/250")! , likes: 384, comments: [ Comment(username: "@ryan", text: "this is beautiful!"), Comment(username: "@jsq", text: ""), Comment(username: "@caitlin", text: "#blessed"), ] ))Copy the code

Finally, modify the following method to replace the Return value

func listAdapter(
  _ listAdapter: ListAdapter,
  sectionControllerFor object: Any
  ) -> ListSectionController {
  return PostSectionController()
}
Copy the code

Bind click events

The next step will beActionCellthe(Like button) Bind event. To do this, we need to handle UIButton clicks and then forward the event toPostSectionController

Open ActionCell. Swift and add the following code

protocol ActionCellDelegate: class {
  func didTapHeart(cell: ActionCell)
}
Copy the code

Add a delegate to the ActionCell

weak var delegate: ActionCellDelegate? = nil
Copy the code

Overwrite awakeFromNib() to add action

override func awakeFromNib() {
  super.awakeFromNib()
  likeButton.addTarget(self, action: #selector(ActionCell.onHeart), for: .touchUpInside)
}
Copy the code

Finally, add the implementation of the action

func onHeart() { delegate? .didTapHeart(cell: self) }Copy the code

Open PostSectionController. Swift, update cellForViewModel: method, add code between the guard and return the cell:

if let cell = cell as? ActionCell {
  cell.delegate = self
}
Copy the code

The compiler will report an error, and we’ll temporarily implement the protocol methods in PostSectionController.

final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource,
ActionCellDelegate {

//...

// MARK: ActionCellDelegate

func didTapHeart(cell: ActionCell) {
  print("like")
}
Copy the code

Run the code and now you can implement the click event.


A minor change

Every time a user clicks the like button, we need to update the number of likes on the post page. However, the properties of our Model are defined by lets because it is more secure. If everything is immutable, how do we change the number of likes on the detail page?

PostSectionController is the best place to change processing and storage, open PostSectionController. Swift, add the following variables

var localLikes: Int? = nil
Copy the code

Going back to didTapHeart(cell:), let’s change print() to something concrete.

func didTapHeart(cell: ActionCell) { // 1 localLikes = (localLikes ?? object? .likes ?? 0) + 1 // 2 update(animated: true) }Copy the code
  1. The localLIkes variable is +1 based on the previous value, uses the likes value in Model if localLike itself is nil, and uses 0 as the initial value if that value doesn’t exist either.
  2. callupdate(animated:,completion:)API, refresh the cell

To actually send the changes to the Model, we need to start with localLikes instead of the likes we got from the server and assign them to the ActionViewModel

Or in PostSectionController. Swift, back to cellForViewModel: method, the ActionViewModel initialization is amended as below

ActionViewModel(likes: object.likes) ActionViewModel(likes: localLikes?? object.likes)Copy the code

Compile code, OK, function implementation.


conclusion

ListBindingSectionController is one of the most powerful IGListKit because it further encourages you to design small, composable Models, Views and Controllers.

We can use Section Controllers to handle any interaction and various variations (such as likes change), just like normal controllers.