In this blog, we will explore a bit on the new apple frameworks: SwiftUI and Combine. SwiftUI is a 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.
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:
-
Download XCode 11 beta from the developer portal
-
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.
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.
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