Skip to content

Commit

Permalink
Replace "Getting Started" with a more complete "Introduction"
Browse files Browse the repository at this point in the history
  • Loading branch information
sharplet committed Aug 28, 2020
1 parent b3fa610 commit 1fc3a69
Showing 1 changed file with 80 additions and 31 deletions.
111 changes: 80 additions & 31 deletions README.md
Expand Up @@ -2,79 +2,128 @@

An implementation of the Model-View-ViewModel (MVVM) pattern using Combine.

- [Getting Started](#getting-started)
- [Introduction](#introduction)
- [Installation](#installation)
- [Bindings](#bindings)
- [Contributing](#contributing)
- [About](#about)

## Getting Started
## Introduction

### Step 1: A view model is class that conforms to `ObservableObject`
CombineViewModel’s primary goal is to make view updates as easy in UIKit and
AppKit as they are in SwiftUI.

In SwiftUI, you write model and view-model classes that conform to Combine’s
[`ObservableObject`][ObservableObject] protocol. SwiftUI:

<ol style="list-style-type: lower-alpha">
<li>
Observe’s the each model's `objectWillChange` publisher via the
[`@ObservedObject`][ObservedObject] property wrapper, and;
</li>
<li>
Automatically rerenders the appropriate portion of the view hierarchy.
</li>
</ol>

The problem with `objectWillChange` _outside_ of SwiftUI is that there's no
built-in way of achieving (b) — being notified that an object _will_ change is
not the same as knowing that it _did_ change and it’s time to update the view.

[ObservableObject]: https://developer.apple.com/documentation/combine/observableobject
[ObservedObject]: https://developer.apple.com/documentation/swiftui/observedobject

### `ObjectDidChangePublisher`

Consider the following sketch of a view model for displaying a user’s social
networking profile:

```swift
// Counter.swift
// ProfileViewModel.swift

import CombineViewModel
import Foundation
import UIKit

final class Counter: ObservableObject {
@Published private(set) var value = 0
class ProfileViewModel: ObservableObject {
@Published var profileImage: UIImage?
@Published var topPosts: [Post]

var formattedValue: String {
NumberFormatter.localizedString(
for: NSNumber(value: value),
number: .decimal
)
func refresh() {
// Request updated profile info from the server.
}
}
```

func increment() {
value += 1
}
With CombineViewModel, you can subscribe to _did change_ notifications using
the `observe(on:)` operator:

func decrement() {
value -= 1
}
```
let profile = ProfileViewModel()
profileSubscription = profile.observe(on: DispatchQueue.main).sink { profile in
// Called on the main queue when either (or both) of `profileImage`
// or `topPosts` have changed.
}
profile.refresh()
```

### Step 2: Observe a view model with the `@ViewModel` property wrapper
### Automatic view updates

Building on `ObjectDidChangePublisher` is the `ViewModelObserver` protocol and
`@ViewModel` property wrapper. Instead of manually managing the
`ObjectDidChangePublisher` subscription like above, we can have it managed
automatically:

```swift
// CounterViewController.swift
// ProfileViewController.swift

import CombineViewModel
import UIKit

// (1) Conform your view controller to the ViewModelObserver protocol.
final class CounterViewController: UIViewController, ViewModelObserver {
@IBOutlet private var valueLabel: UILabel!
class ProfileViewController: UITableViewController, ViewModelObserver {
enum Section: Int {
case topPosts
}

@IBOutlet private var profileImageView: UIImageView!
private var dataSource: UITableViewDiffableDataSource<Section, Post>!

// (2) Declare your view model using the `@ViewModel` property wrapper.
@ViewModel private var counter: Counter
@ViewModel private var profile: ProfileViewModel

// (3) Initialize your view model in init().
required init?(coder: NSCoder) {
required init?(profile: ProfileViewModel, coder: NSCoder) {
super.init(coder: coder)
self.counter = Counter()
self.profile = profile
}

// (4) The `updateView()` method is automatically called on the main queue
// when the view model changes. It is always called after `viewDidLoad()`.
func updateView() {
valueLabel.text = counter.formattedValue
}
profileImageView.image = profile.profileImage

@IBAction func increment() {
counter.increment()
var snapshot = NSDiffableDataSourceSnapshot<Section, Post>()
snapshot.appendSections([.topPosts])
snapshot.appendItems(profile.topPosts)
dataSource.apply(snapshot)
}

@IBAction func decrement() {
counter.decrement()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

profile.refresh()
}
}
```

### Further reading

In the [Example](/Example) directory you’ll find a complete iOS sample
application that demonstrates how to integrate CombineViewModel into your
application.

## Installation

CombineViewModel is distributed via Swift Package Manager. To add it to your
Expand Down

0 comments on commit 1fc3a69

Please sign in to comment.