iOS development with Reactive Programming and Model-View-ViewModel (MVVM) - part 3

In this blog, we will explore a bit on the new apple frameworks: SwiftUI and Combine . SwiftUI is an very innovative and relatively simple way to build user interfaces across all apple devices with pure Swift. For developer, we have one more choices to build our UI apart from using UIKit and storyboards. SwiftUI can work perfectly alone, or mixed with UIKit. Therefore you don't need to rewrite the whole existing app in order to use SwiftUI. On the other hand, it is pure Swift code base. It allows you to set break points and debugs as business logic. Debugging and merge requests are no longer a night mare for the UI features. This declarative Swift syntax helps developers to read and write easily, and totally minimize the maintenance costs. 

 
goal_swift_combine.gif
 

Combine is a reactive programming framework, which basically provides similar functionalities as RxSwift: "Combine provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers" (Apple Documentation). We will implement the same github repositories as in the last blog post experiment of RMVVM with RxSwift and Rxcocoa iOS Series (Episode 2): Reactive MVVM experiment (UIKit + RxCocoa + RxSwift ), but this time we will use SwiftUI and Combine.

Prerequisites:

  1. Download XCode 11 beta from the developer portal

  2. If you want to use the preview for SwiftUI, you also have to install macOS 10.15 beta from the developer portal (* macOS 10.15 and XCode 11 stable version will be released in the fall of this year) 

View

In SwiftUI, the View is the struct where you define your UI. In the View struct, developers should only implement the UI elements. In our case, we need a Searchbar and a List. The GithubSearchViewModel will be created within the struct and the viewModel owns all the properties that should be passing to different views. 

struct GithubSearchView : View {
    @ObjectBinding var viewModel = GithubSearchViewModel()
     
     var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: viewModel[\.query])
                List(viewModel.repositories) { repo in
                    RepositoryRow( repository: repo)
                }
            }
            .navigationBarTitle(Text("Github Search"))
        }
    }
}

There is another two UI elements in the above code: SearchBar and RepositoryRow

struct SearchBar : View {
    @Binding var text: String
    var body: some View {
        ZStack {
            HStack {
                TextField($text)
               .padding([.leading, .trailing, .top, .bottom], 8)
                     .background(Color.white.opacity(0.2))
                    .cornerRadius(8)
            }
            .padding([.leading, .trailing], 16)
             
        }
        .frame(height: 64)
    }
}

For RepositoryRow, we will create RepositoryRowViewModel to deal with the logic of the image download later on.

struct RepositoryRow : View {
   @ObjectBinding var viewModel: RepositoryRowViewModel = RepositoryRowViewModel()
   @State var repository: Repository
   var body: some View {
        HStack {
           setImage()
                .resizable()
                .renderingMode(.original)
                .animation(.basic())
                .aspectRatio(contentMode: .fit)
                .frame(width: 50, height: 50)
  
            Text(repository.full_name)
                .font(Font.system(size: 18).bold())
            Spacer()
        }.tapAction({
            UIApplication.shared.open(self.repository.html_url, options:  [:], completionHandler: nil)
        }).frame(height: 60)
    }
 
    func setImage() -> Image {
        //just return placeholder for now
        return  Image(uiImage: UIImage(named: "placeholder", in: Bundle.main, compatibleWith: nil) ?? UIImage())
    }
}

ViewModel

In order to bind a View and ViewModel in SwiftUI, there is BindableObject that we can use ObjectBinding property wrapper in the view to create a dependency. We have to define a property of PassthroughSubject to notify the ObjectBinding property in the View.

final class GithubSearchViewModel: BindableObject {
    var didChange = PassthroughSubject<GithubSearchViewModel, Never>()
    var subscriber: AnyCancellable?
     var repositories = [Repository]() {
        didSet {
            DispatchQueue.main.async {
                self.didChange.send(self)
            }
        }
    }
    var query = "" {
        didSet {
            if(oldValue.isEmpty){
                self.repositories = []
            }else{
                self.search()
            }
        }
    }
 
   func search() {
        //implement search request
    }
}

RepositoryRowViewModel is a ViewModel for the RepositoryRow. In this class, we put the lazy loading logic for the image.

final class RepositoryRowViewModel: BindableObject{
 
    var didChange = PassthroughSubject<RepositoryRowViewModel, Never>()
     
    private(set) var image = UIImage(named: "placeholder") {
        didSet { didChange.send(self) }
    }
     
    func lazyLoadImage(url : URL){
        URLSession.shared.dataTask( with: url, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                if let data = data, let img = UIImage(data: data) {
                    self.image = img
                }
            }
        }
        ).resume()
    }
}

Model

Basically, we can reuse the same code as previous project in RxSwift. 

struct SearchRepositoryResponse: Decodable {
    var items: [Repository]
}
 
struct Repository: Decodable, Hashable, Identifiable {
    var id: Int64
    var full_name: String
    var description: String?
    var stargazers_count: Int = 0
    var language: String?
    var owner: User
    var html_url: URL
}
 
struct User: Decodable, Hashable, Identifiable {
    var id: Int64
    var login: String
    var avatar_url: URL
}

Binding Implementation:

First of all, we need to implement the network logic for the request of searching the repository. In this case, I would like to create a separate class to handle the http request and response. Let's call it GithubServicesClient. In this class, we have only one function to pass a parameter of query and then make the http request to github. Combine provides a lot of operators for handing the response. Also, in the new iOS SDK, it provides DataTaskPublisher for the developer using subscriber to observe the response. And this function will return AnyPublisher object, which is the counterpart of RxSwift’s Observable.  

class GithubServicesClient {
 
    func search(query: String) -> AnyPublisher<[Repository], Error> {
        guard let url = url(query)
            else { preconditionFailure("Can't create url for query: \(query)") }
        let decoder = JSONDecoder()
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: SearchRepositoryResponse.self, decoder: decoder)
            .map { $0.items }
            .eraseToAnyPublisher()
    }
}

Now, we have to implement the search() function in GithubSearchViewModel. In order to observe the response on the search, we need to subscribe the publisher in this function. It means whenever client gets the responses from github, it will assign the result to the property repositories. And when the property repositories is updated, it will pass the message back to the View, so that it will reload the view.

let client = GithubServicesClient()
 
 var repositories = [Repository]() {
      didSet {
          DispatchQueue.main.async {
              self.didChange.send(self)
          }
      }
}
func search() {
      client
          .search(query: query)
          .catch { _ in Just([]) }
          .assign(to: \.repositories, on: self)
}

Since we already implemented the RepositoryRowViewModel, we just need to update the setImage function code in RepositoryRow, so that it has the right time to trigger the image download. The basic principle is same as the GithubSearchViewModel. In every RepositoryRowViewModel owns a property of one single repository. @State is telling the SwiftUI, this view is depending on this state property.

@State var repository: Repository
 
func setImage() -> Image {
    viewModel.lazyLoadImage(url: repository.owner.avatar_url)
    return Image(uiImage:viewModel.image ?? UIImage())
}

OK, now, you are ready to run the code. So basically, you’re done with the task!

SwiftUI Previews

One of the SwiftUI cool features is, you can preview your View implementation as in the following image. However, one downside of it is, you have to write the code to preview it.

Screenshot 2019-07-14 at 00.59.01.png

Dark mode setting

With iOS 13, we finally got the dark mode. Therefore, every app should be able to match the dark mode UI design. In this case, it is relatively easy to do so. You just need to add a property in info.plist. Then your app will be automatically change to dark mode except the UI elements that have explicitly set a color. 

Screenshot 2019-07-14 at 01.02.39.png

Conclusion

SwiftUI + Combine provide a great experience on implementation a reactive MVVM app. It is much less code to write and much easier to read. Although SwiftUI and Combine are still in beta, (Apple still actively updates the framework, so many breaking change lately!), I do believe that it really helps people to develop reactive apps much easier. 

The full project can be downloaded here: https://github.com/chauchinyiu/SwiftUI-Combine-Demo