MVVM is a familiar pattern, but few people have actually used it in their projects. After all, Cocoa Touch is based on “MVC” and you can’t do it without a Controller. “Buzz Word” is a word that has a lot of buzz, but people in different fields have different meanings of it. I’m not going to get into the specifics of what MVC is and what MVVM is, which I mentioned a little bit earlier in this article. If you’re really interested in their original definitions, check out this MVC paper, the Gang of Four’s Design Patterns, which covers MVC; We know that MVVM was introduced by Microsoft when WPF was launched. Check out the MSDN blog.

Without further ado, today I want to talk about my understanding of the ViewModel. Because we don’t have to copy a model exactly, it’s good to take the best of it and apply it on a project-specific basis. The ViewModel concept is kind of neat to me. We all know that the ViewModel is bound to the View, so how to bind? There are several options:

  • Do the UI layout in IB as much as possible and put the binding logic in the View
  • Put the binding logic into the Model
  • Define a separate ViewModel to process the Model and output the data suitable for display to the View

The above several schemes are mainly said to be data binding, and are one-way binding, in fact, ViewModel can also be two-way data binding with View, logical binding, etc., these first press not table, the following example respectively about the implementation of these three one-way data binding and advantages and disadvantages.




Profile.png

Above is a Profile page for the Github client THAT I’m working on. Its header is a single View that contains some UI elements:

class ProfileHeader: UIView {

    @IBOutlet var contentView: UIView!
    @IBOutlet weak var backgroundView: UIImageView!
    @IBOutlet weak var avatarView: UIImageView!
    @IBOutlet weak var followersButton: UIButton!
    @IBOutlet weak var repositoriesButton: UIButton!
    @IBOutlet weak var followingButton: UIButton!
    @IBOutlet weak var nicknameLabel: UILabel!
    @IBOutlet weak var bioLabel: UILabel!

    // ...
}Copy the code

How do you pass the data to these UI elements to display when you get it from the network? Of course we could put it in controllers, but controllers are going to be very bloated. So let’s try all three.

Scheme 1: View as ViewModel

protocol ViewModelType {
    associatedtype Model
    func bind(model: Model)
}

extension ProfileHeader: ViewModelType {
    func bind(model: Profile) {
        let avatarURL = NSURL(string: model.avatar)
        backgroundView.presentImageAnimated(avatarURL)
        avatarView.presentImageAnimated(avatarURL)

        nicknameLabel.text = model.login

        bioLabel.text = model.bio ?? "Not set yet"

        let attributedFollowers = (title: "\(model.followers) \nFollowers", attributingTitle: "Followers")
        let attributedRepositories = (title: "\(model.repos) \nRepositories", attributingTitle: "Repositories")
        let attributedFollowing = ("\(model.following) \nFollowing", attributingTitle: "Following")
        followersButton.setAttributedTitle(attributedFollowers)
        repositoriesButton.setAttributedTitle(attributedRepositories)
        followingButton.setAttributedTitle(attributedFollowing)
    }
}Copy the code

This is the one THAT I used to prefer, but the advantage is that it’s very simple, there’s not a lot of twisting and turning, and basically it’s putting the code that was written in the Controller into the View. The drawback is that the “binding” is so tied that it is written directly into the View that the View cannot be reused. Let’s say we have a Header in another page, and the UI displays the same data from the Profile Model, but in a different way, for example, nicknameLabel displays Nickname: XXX, other UI elements also need to add additional information and so on, which would be awkward. You can only create a new NewHeader, copy the xiB file, change the class to NewHeader, pull the labels and buttons over, and write a new bind method. If ProfileHeader has many other helper methods that NewHeader needs, then NewHeader will inherit the ProfileHeader and override the bind method… So it’s not very scientific… As you can see, the ViewModelType protocol, in this case, doesn’t really work. Using a protocol as a type often provides more flexibility and extensibility, but if the protocol is implemented by a View, since the View is already the end of the data flow, there is no possibility of substitution once the logic for processing the data is written there. This protocol is only used as a constraint or marker — once someone sees a View implement this protocol, they know that the View has logic inside it to process data. But in this way, basically every View needs to process data, so it needs to implement this protocol. So this protocol is really dispensable, and it doesn’t really do much good other than make the code look more pop-oriented.

Scheme 2: Model as ViewModel

This is the exact opposite of plan 1. Instead of injecting the Model into the View, we inject the View into the Model, again using Profile as an example:

typealias AttributedTitle = (title: String, attributingTitle: String)

private let attributingFollowers = "Followers"
private let attributingRepos = "Repositories"
private let attributingFollowing = "Following"

protocol RenderContext {
    func renderText(texts: String...)
    func renderAttributedText(attributedTexts: AttributedTitle...)
    func renderImage(imageURLs: NSURL...)
}

protocol ViewModelType {
    func renderInContext(context: RenderContext)
}

extension ProfileHeader: RenderContext {
    func renderText(texts: String...) {
        nicknameLabel.text = texts.first
        bioLabel.text = texts.last
    }

    func renderAttributedText(attributedTexts: AttributedTitle...) {
        followersButton.setAttributedTitle(attributedTexts[0])
        repositoriesButton.setAttributedTitle(attributedTexts[1])
        followingButton.setAttributedTitle(attributedTexts[2])
    }

    func renderImage(imageURLs: NSURL...) {
        backgroundView.presentImageAnimated(imageURLs.first)
        avatarView.presentImageAnimated(imageURLs.first)
    }
}

struct Profile {
    let login: String
    let id: Int
    let avatar: String
    let name: String
    let bio: String?
    let company: String?
    let blog: String
    let location: String
    let email: String?
    let repos: Int
    let followers: Int
    let following: Int
}

extension Profile: ViewModelType {
    func renderInContext(context: RenderContext) {
        context.renderText(login, bio ?? "Not set yet")

        let attributedFollowers = (title: "\(followers) \n\(attributingFollowers)", attributingTitle: attributingFollowers)
        let attributedRepositories = (title: "\(repos) \n\(attributingRepos)", attributingTitle: attributingRepos)
        let attributedFollowing = (title: "\(following) \n\(attributingFollowing)", attributingTitle: attributingFollowing)
        context.renderAttributedText(attributedFollowers, attributedRepositories, attributedFollowing)

        let avatarURL = NSURL(string: avatar) ?? defaultImageURL
        context.renderImage(avatarURL)
    }
}Copy the code

This one, it looks a little bit fancier, and the reusability of the View is improved. However, its limitations are actually similar to plan 1. Plan 1 is that Model can be reused, but View is not reusable. Plan 2, on the contrary, improves the reusability of View, but the Model is not reusable… The data processing logic is written in the renderInContext method of the Model. Whenever a business scenario requires different data processing logic, a new Model or inherited Profile is created. As we all know, inheritance is not advocated in Swift. The Profile I declared here is a struct and cannot be inherited, so this scheme is not the most appropriate one.

Scheme 3: Define separate ViewModel processing Model, and output data suitable for display to View

There is an important principle in software design — to encapsulate changes. By analyzing the limitations of the previous two schemes, we can clearly know that “data processing” is a relatively easy point to change, while View and Model are relatively stable. Then we can separate the logic of “data processing” and separate the changing parts from the unchanging parts, so that the reusability of View and Model is improved.

protocol ViewModelType {
    var avatarURL: NSURL { get }
    var followers: AttributedTitle { get }
    var repositories: AttributedTitle { get }
    var following: AttributedTitle { get }
    var nickname: String { get }
    var bio: String { get }
}


struct ProfileHeaderViewModel: ViewModelType {

    // MARK: Output
    let avatarURL: NSURL
    let followers: AttributedTitle
    let repositories: AttributedTitle
    let following: AttributedTitle
    let nickname: String
    let bio: String

    init(input: Profile) {
        avatarURL = NSURL(string: input.avatar) ?? defaultImageURL

        nickname = input.login

        bio = input.bio ?? defailtProfileItem

        followers = (title: "\(input.followers) \n\(attributingFollowers)", attributingTitle: attributingFollowers)

        repositories = (title: "\(input.repos) \n\(attributingRepos)", attributingTitle: attributingRepos)

        following = (title: "\(input.following) \n\(attributingFollowing)", attributingTitle: attributingFollowing)
    }
}Copy the code

The ViewModel takes a Model as input and some data as output that can be used directly by the View. Then we inject it into the View, and it doesn’t matter how we inject it, whether it’s an initialization parameter, a property parameter, a method parameter, etc., as long as it’s something that can be injected externally and not generated by the View itself. For example, as an attribute:

var viewModel: ViewModelType! {
    didSet {
        nicknameLabel.text = viewModel.nickname

        configAvatar(viewModel.avatarURL)

        bioLabel.text = viewModel.bio

        configButtons(viewModel.followers, viewModel.repositories, viewModel.following)
    }
}Copy the code

So we can declare multiple ViewModels that all follow the ViewModelType, and when we want to display different data, we just assign different viewModels to the ProfileHeader.

Expand the thinking

A ProfileHeaderView, for example, can be used in a Repository module as well as in a Profile module (just to give an example, A Repository module would not use a Header like this. . The avatarView in the middle is not used to show the avatar, but to show the Logo of the project, backgroundView also shows the Logo, nicknameLabel shows the name of the project, bioLabel shows the description of the project, and so on. At this point, you will find that ProfileHeaderView names, including internal UI elements, are not appropriate and are too tightly coupled to the specific business. If the Header were to be used everywhere, it would be named CustomHeaderView, centerImageView, topLabel, or something business-neutral. In this case, the corresponding ViewModel only needs to ensure that the output is directly usable data. The input does not have to be a Profile, or a Repository, or even a Model. It can also be Dictionary, JSON, etc. But the more general it is, the less readable it tends to be, so obviously nicknameLabel is much better than topLabel; The more generic you are, the more middle layers you tend to have and the less intuitive the relationships between modules are. So when encapsulating change, make sure that the abstraction encapsulates a real possibility of change, otherwise it becomes overdesign. For example, ViewModel this thing, if your View is a highly customized View, almost no reuse of the possibility, then in the name, can be relevant to the business, data processing can also use scheme 1, because this is the easiest way to understand, but also the most convenient way to develop. There are few universal truths in this world, so consider and weigh each situation.

RxSwift + MVVM

Since Cocoa Touch itself does not have a unified data binding mechanism, MVVM came to iOS developers’ attention almost entirely with RAC, the FRP framework. Data binding and MVVM patterns can be implemented more elegantly with RAC. RxSwift is also an FPR framework, which implements ViewModel like this:

protocol ViewModelType { var avatarURL: Driver { get } var followers: Driver { get } var repositories: Driver { get } var following: Driver { get } var nickname: Driver { get } var bio: Driver { get } } struct ProfileHeaderViewModel: ViewModelType { // MARK: Output let avatarURL: Driver let followers: Driver let repositories: Driver let following: Driver let nickname: Driver let bio: Driver init(input: Profile) { let profileDriver = Driver.of(input) avatarURL = profileDriver .map { $0.avatar } .map { NSURL(string: $0)?? defaultImageURL } followers = profileDriver .map { (title: "\($0.followers) \n\(attributingFollowers)", attributingTitle: attributingFollowers) } repositories = profileDriver .map { (title: "\($0.repos) \n\(attributingRepos)", attributingTitle: attributingRepos) } following = profileDriver .map { (title: "\($0.following) \n\(attributingFollowing)", attributingTitle: attributingFollowing) } nickname = profileDriver .map { $0.login } bio = profileDriver .map { $0.bio ?? defailtProfileItem } } } class ProfileHeader: UIView { // ... private let bag = DisposeBag() var viewModel: ViewModelType! { didSet { viewModel.nickname .drive(nicknameLabel.rx_text) .addDisposableTo(bag) viewModel.avatarURL .drive(onNext: configAvatar) .addDisposableTo(bag) viewModel.bio .drive(bioLabel.rx_text) .addDisposableTo(bag) Drive.zip (ViewModel.followers, viewModel.repositories, viewmodel.following) {($0.0, $0.1, $0.2)}. Drive (onNext: configButtons) .addDisposableTo(bag) } } // ... }Copy the code

FRP is actually more suitable for complex business projects, the performance in my simple example is no better than the ordinary ViewModel in plan 3. But RxSwift is very fun, I recommend you even if the official project can not use, private can also play ~