How to bind image to UIImageView with rxswift? - ios

I have viewModel:
class EditFoodViewViewModel {
private var food: Food
var foodImage = Variable<NSData>(NSData())
init(food: Food) {
self.food = food
self.foodImage.value = food.image!
}
}
And ViewController:
class EditFoodViewController: UIViewController {
public var food: EditFoodViewViewModelType?
#IBOutlet weak var foodThumbnailImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage.asObservable().bind(to: foodThumbnailImageView.rx.image).disposed(by: disposeBag)
}
}
In the last line of viewController (where my UIImageView) a get error:
Generic parameter 'Self' could not be inferred
How to solve my problem? How to set image to imageView with rxSwift?

Almost invariably, when you see the error: "Generic parameter 'Self' could not be inferred", it means that the types are wrong. In this case you are trying to bind an Observable<NSData> to an Observable<Image?>.
There's a few other issues with your code as well.
it is very rare that a Subject type should be defined with the var keyword and this is not one of those rare times. Your foodImage should be a let not a var.
Variable has been deprecated; don't use it. In this case, you don't even need a subject at all.
NSData is also inappropriate in modern Swift. Use Data instead.
Based on what you have shown here, I would expect your code to look more like this:
class EditFoodViewViewModel: EditFoodViewViewModelType {
let foodImage: Observable<UIImage?>
init(food: Food) {
self.foodImage = Observable.just(UIImage(data: food.image))
}
}
class EditFoodViewController: UIViewController {
#IBOutlet weak var foodThumbnailImageView: UIImageView!
public var food: EditFoodViewViewModelType?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage
.bind(to: foodThumbnailImageView.rx.image)
.disposed(by: disposeBag)
}
}

Related

RXSwift bind view model data to view controller uiimageview

I am new to RXSwift, please help to find best solution.
I have view model with instance variable:
var capturedImageData: Data?
I need to unhide UIImageView view and set image after capturedImageData receives data, for example on capture image from camera.
You will need to observe the value of capturedImageData
You can create a behaviorRelay of capturedImageData
Something like
var capturedImageData:<Data?> = BehaviorRelay.init(value: nil)
And when you get the data, you add do something like this
capturedImageData.accept(data)
And in your viewController, you subscribe to capturedImageData
self.capturedImageData.asObservable().subscribe(onNext: { (data) in
self.imageView.image = UIImage.init(data: data)
self.imageView.isHidden = false
}).disposed(by: bag)
Something of this sort.
Haven't tested the code, but you can follow this approach.
Hope this helps
From your code, I mocked a very simple example.
From your ViewModel layer, you can try to keep an Input / Output structu to make it easy to reuse and consume. Here the input would be your Data wherever it comes from, and the output UIImage?. In short, ViewModel does the logic to transform the data, the View will load only if available.
struct ViewModel {
// input
let loadingImageData: PublishRelay<Data>
// output
let showImage: Driver<UIImage?>
init() {
let dataRelay = PublishRelay<Data>()
self.loadingImageData = dataRelay
self.showImage = dataRelay.map({ UIImage(data: $0) }).asDriver(onErrorJustReturn: nil)
}
func loadimage() {
// your code here to load Data ...
loadingImageData.accept(imageData)
}
}
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
// bind output viewmodel to UIImageView
viewModel.showImage
.drive(imageView.rx.image)
.disposed(by: disposeBag)
}
}
You can extend the logic to hide / show element the same way creating Bool events instead.
struct ViewModel {
// ...
let isImageHidden: Driver<Bool>
init() {
let dataRelay = PublishRelay<Data>()
self.loadingImageData = dataRelay
self.showImage = dataRelay.map({ UIImage(data: $0) }).asDriver(onErrorJustReturn: nil)
self.isImageHidden = showImage.map({ $0 == nil })
}
}
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
// ...
viewModel.isImageHidden
.drive(imageView.rx.isHidden)
.disposed(by: disposeBag)
}
}
Note that I use Driver to make sure it runs on main thread.

App crashes at click with message: 'this class is not key value coding-compliant for the key postReplyButton.'

My app crashes when I click a cell in my tableView of recent posts. The click is supposed to segue me to the MainTextView which has the postReplyButton. The segue worked until I started experimenting with creating comments for the posts.
Here is the MainTextView code:
import Foundation
import UIKit
import Firebase
class MainTextView: UIViewController {
#IBOutlet weak var titleText: UILabel!
#IBOutlet weak var mainText: UILabel!
#IBOutlet weak var commentPlaceHolder: UILabel!
#IBOutlet weak var newCommentLabel: UITextView!
var delegate:NewPostVCDelegate?
#IBAction func postReplyButton() {
// Firebase code here
let postRef = Database.database().reference().child("posts").childByAutoId()
let postObject = [
"comment": newCommentLabel.text,
"timestamp": [".sv": "timestamp"]
] as [String : Any]
postRef.setValue(postObject, withCompletionBlock: { error, ref in
if error == nil {
self.delegate!.didUploadPost(withID: ref.key!)
self.dismiss(animated: true, completion: nil)
} else {
// Handle error
}
})
newCommentLabel.text = String()
commentPlaceHolder.isHidden = false
}
var post: Post?
// MARK: - View Controller LifeCycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setMain()
}
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self as! UITextViewDelegate
}
private func setMain() {
guard let post = self.post else {
return
}
titleText.text = post.text
mainText.text = post.title
}
func textViewDidChange(_commentView: UITextView) {
commentPlaceHolder.isHidden = !newCommentLabel.text.isEmpty
}
}
For reference, here is my Post class code:
import Foundation
class Post {
var id:String
var title: String
var text:String
var createdAt:Date
var comment: [String] = []
init(id: String, title: String,text:String, timestamp:Double, comment: [String] = []) {
self.id = id
self.title = title
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
}
static func parse(_ key:String, data:[String:Any]) -> Post? {
if let title = data["text"] as? String,
let text = data["title"] as? String,
let timestamp = data["timestamp"] as? Double {
return Post(id: key, title: title, text: text, timestamp:timestamp, comment: [])
}
return nil
}
}
I suspect the issue may be with the delegate, which was declared as such in my NewPostViewController:
protocol NewPostVCDelegate {
func didUploadPost(withID id:String)
}
I have tried troubleshooting the storyboard, but everything seems to be in place. Is there an issue of the reuse of the protocol or perhaps the change of adding comments to the Post class itself? Maybe the issue is that I do not in fact want to upload a new post, but really I just want to add a comment to an existing post. If this is the case, how would I change the delegate or create a new one? I can provide more detail if needed. Thank you for your help.
This usually happens if you have an IBOutlet that was created previously with the same postReplyButton name. To check if your app has any other Outlet with the same name go to the Search section in your project and search for postReplyButton and see if you get multiple results for that name. If you do then click on the one which you don't need and delete it from the properties section.
If you have any Outlet which has a bad connection you will see something like this in the properties sections when you click on any one of the search result for postReplyButton
If that does not work then try renaming the Outlet entirely and see if that fixes the problem.
EDITED:
For your issue that you mentioned in the comments try this.
Instead of casting your newCommentLabel as an optional type of UITextViewDelegate just extend your viewController to conform to UITextViewDelegate. This should solve the issue.
class MainTextView: UIViewController, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self
}
}
Once you add UITextViewDelegate to your viewController you will no longer get the warning in viewDidLoad to cast newCommentLabel as an optional of type UITextViewDelegate.

MVP architecture pattern example. Swift

I want to learn classic MVP architecture pattern and for this try to implement Weather app on Swift. I learned in theory how it should work but practically stuck on basic understanding. For now I have a model:
Model
class WeatherModel: Codable {
var name: String?
var main: Main?
}
class Main: Codable {
var temperature: Float?
var pressure: Int?
var humidity: Int?
private enum CodingKeys: String, CodingKey {
case temperature = "temp"
case pressure
case humidity
}
View
final class WeatherViewController: UIViewController {
#IBOutlet weak var cityTextField: UITextField!
#IBOutlet weak var temperatureLabel: UILabel!
#IBOutlet weak var pressureLabel: UILabel!
#IBOutlet weak var humidityLabel: UILabel!
private var presenter: WeatherPresenter!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presenter = WeatherPresenter()
cityTextField.delegate = self
presenter.delegate = self
}
#IBAction func buttonClicked(_ sender: Any) {
let city = cityTextField.text
if let city = city {
presenter.loadWeatherFor(city: city)
}
}
}
extension WeatherViewController: WeatherPresenterProtocol {
// MARK: - WeatherPresenterProtocol
func showWeather(data: WeatherModel) {
if let temperature = data.main?.temperature {
self.temperatureLabel.text = String(temperature)
}
if let pressure = data.main?.pressure {
self.pressureLabel.text = Constants.pressure + String(pressure)
}
if let humidity = data.main?.humidity {
self.humidityLabel.text = Constants.humidity + String(humidity)
}
}
}
Presenter
protocol WeatherPresenterProtocol: class {
func showWeather(data: WeatherModel) // ?
}
final class WeatherPresenter {
var model: WeatherModel!
weak var delegate: WeatherPresenterProtocol?
func loadWeatherFor(city: String) {
Network.shared.getWeather(city) { [weak self] (weather, error) in
DispatchQueue.main.async {
self?.model = weather
}
}
}
}
In Presenter I receive data from a Network Service but I can't understand how to update View with this data (how to implement protocol in Presenter) because View shouldn't know about model but in my case it will know( Any idea to implement classic MVP will be appreciated!
And second question: how to implement protocol in Presenter to get Model (as it is shown in the picture I've taken from https://www.youtube.com/watch?v=qzTeyxIW_ow)
Change
func showWeather(data: WeatherModel)
to
func showWeather(temperature: String, pressure: String, humidity: String)
Move the construction of these strings from the View to the Presenter. This way, the View remains ignorant of the model.
The Presenter should not speak directly to the full View, only to the protocol.
You can try this, in the MVP the presenter will take care of updating the view, so modify your presenter to have the view property. Here the view holds nothing but the WeatherController instance.
In terms of the MVP, the UIViewController subclasses are in fact the Views and not the Presenters.
class WeatherPresenter : WeatherViewPresenter {
unowned let view: WeatherView
let weather: WeatherModel
required init(view: WeatherView, weather: WeatherModel) {
self.view = view
self.weather = weather
}
func updateWeatherView() {
//...update properties on your weather view
self.view.setTemperature(self.weather.temperature)
}
}
For example,To update the temperature label, have a protocol to set the temperature value. You can have multiple methods to set multiple properties or have one for all of your UI elements in weather view.
protocol WeatherView: class {
func setTemperature(temp: String)
//same for pressure, humid etc
}
Now write a protocol for weather presenter
protocol WeatherViewPresenter {
init(view: WeatherView, weather: WeatherModel)
func updateWeatherView()
}
Now that you have the presenter setup for weather class, you would use it like
class WeatherViewController : UIViewController, WeatherView {
var presenter: WeatherViewPresenter!
override func viewDidLoad() {
super.viewDidLoad()
let model = //your weather model
presenter = WeatherPresenter(view: self, weather: model)
}
func setTemperature(temp: String) {
self.temperatureLabel.text = temp
}
}
After you get the data from weather, just call self.presenter.updateWeatherView() to update the weather view.
For more detailed info, please refer

How do you communicate between a UIViewController and its child UIView using MVVM and RxSwift events?

I'm using MVVM, Clean Architecture and RxSwift in my project. There is a view controller that has a child UIView that is created from a separate .xib file on the fly (since it is used in multiple scenes). Thus there are two viewmodels, the UIViewController's view model and the UIView's. Now, there is an Rx event in the child viewmodel that should be observed by the parent and then it will call some of its and its viewmodel's functions. The code is like this:
MyPlayerViewModel:
class MyPlayerViewModel {
var eventShowUp: PublishSubject<Void> = PublishSubject<Void>()
var rxEventShowUp: Observable<Void> {
return eventShowUp
}
}
MyPlayerView:
class MyPlayerView: UIView {
var viewModel: MyPlayerViewModel?
setup(viewModel: MyPlayerViewModel) {
self.viewModel = viewModel
}
}
MyPlayerSceneViewController:
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
#IBOutlet weak var otherView: UIView!
var viewModel: MyPlayerSceneViewModel
fileprivate var disposeBag : DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.myPlayerView.viewModel.rxEventShowUp.subscribe(onNext: { [weak self] in
self?.viewModel.doOnShowUp()
self?.otherView.isHidden = true
})
}
}
As you can see, currently, I am exposing the myPlayerView's viewModel to the public so the parent can observe the event on it. Is this the right way to do it? If not, is there any other suggestion about the better way? Thanks.
In general, nothing bad to expose view's stuff to its view controller but do you really need two separate view models there? Don't you mix viewModel and model responsibilities?
Some thoughts:
Model shouldn't subclass UIView.
You should avoid creating own subjects in a view model. It doesn't create events by itself, it only processes input and exposes results.
I encourage you to get familiar with Binder and Driver.
Here is the code example:
struct PlayerModel {
let id: Int
let name: String
}
class MyPlayerSceneViewModel {
struct Input {
let eventShowUpTrigger: Observable<Void>
}
struct Output {
let someUIAction: Driver<PlayerModel>
}
func transform(input: Input) -> Output {
let someUIAction = input.eventShowUpTrigger
.flatMapLatest(fetchPlayerDetails) // Transform input
.asDriver(onErrorJustReturn: PlayerModel(id: -1, name: "unknown"))
return Output(someUIAction: someUIAction)
}
private func fetchPlayerDetails() -> Observable<PlayerModel> {
return Observable.just(PlayerModel(id: 1, name: "John"))
}
}
class MyPlayerView: UIView {
var eventShowUp: Observable<Void> {
return Observable.just(()) // Expose some UI trigger
}
var playerBinding: Binder<PlayerModel> {
return Binder(self) { target, player in
target.playerNameLabel.text = player.name
}
}
let playerNameLabel = UILabel()
}
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
private var viewModel: MyPlayerSceneViewModel!
private var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
let input = MyPlayerSceneViewModel.Input(eventShowUpTrigger: myPlayerView.eventShowUp)
let output = viewModel.transform(input: input)
// Drive manually
output
.someUIAction
.map { $0.name }
.drive(myPlayerView.playerNameLabel.rx.text)
.disposed(by: disposeBag)
// or to exposed binder
output
.someUIAction
.drive(myPlayerView.playerBinding)
.disposed(by: disposeBag)
}
}

Binding UITextField or button with viewModel

I have been facing an issue with binding UITextField or button with observables in viewModel.
class VM {
var emailObservable: Observable<String?> = Observable.just("")
}
I have this observable for email in my viewModel and in controller. When i try to bind my textfield with it, it gives me error
Cannot invoke 'bind' with an argument list of type '(to: Observable)'.
But when i replace the observables with Variable, it works fine.
Can someone please help me with this. I found answers which mainly include passing the observable in the init method of viewModel, but i don't want to pass it in the init method.
This is the link i found for binding but it is through init method.
How to bind rx_tap (UIButton) to ViewModel?
Instead of
emailTextfield.rx.text.asObservable().bind(to: viewModel.emailObservable).disposed(by: disposeBag)
use this code
viewModel.emailObservable.bind(to: noteField.rx.text).disposed(by: disposeBag)
Probably, you want to make two way binding, so read more about it here
I think here what you looking for:
final class ViewModel {
private let bag = DisposeBag()
let string = BehaviorSubject<String>(value: "")
init() {
string.asObservable().subscribe(onNext: { string in
print(string)
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
textField.rx.text
.orEmpty
.bind(to: viewModel.string)
.disposed(by: bag)
}
}
Note, as #MaximVolgin mentioned Variable is deprecated in RxSwift 4, so you can use BehaviorSubject or other that's up to you.
UPD.
Implementation with Observable only.
final class ViewModel {
private let bag = DisposeBag()
var string = "" {
didSet {
print(string)
}
}
init(stringObservable: Observable<String>) {
stringObservable.subscribe(onNext: { string in
self.string = string
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel(stringObservable: textField.rx.text.orEmpty.asObservable())
}
}
As you can see, your solution can be implemented using Observable, not Variable or any kind of Subject. Also should be mentioned that in most cases this is not the final logic (just bind textField or whatever to some variable). There can be some validation, enable/disable, etc. logic. For this cases RxSwift provide Driver. Also nice example about differences in using Observable and Driver for one project can be found here (by RxSwift).
Method .bind(to:) binds to an Observer, not Observable.
Variable (deprecated in RxSwift v4) is a special-purpose Subject.
Subjects are by definition both Observer and Observable.
This is what .bind(to:) does inside -
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
UPDATE:
How to avoid passing observables in .init() of VM:
// inside VM:
fileprivate let observableSwitch: BehaviorSubject<Observable<MyValue>>
fileprivate let myValueObservable = observableSwitch.switchLatest()
// instead of passing in init:
public func switch(to observable: Observable<MyValue>) {
self.observableSwitch.onNext(observable)
}
Take a subject of variable type in ViewModel class:
class ViewModel{
//MARK: - local Variables
var emailText = Variable<String?>("")
}
Now create object of viewmodel class in viewController class and bind this emailtext variable to textfield in viewcontroller.Whenever textfield text will change then it emailText of viewmodel gets value.
txtfield.rx.text
.bindTo(viewModel.emailText).addDisposableTo(disposeBag)
Try this,
override func viewDidLoad() {
super.viewDidLoad()
_ = userNameTextField.rx.text.map { $0 ?? "" }.bind(to: viewModel.userName)
}
in viewModel class,
class ViewModel{
var userName: Variable<String> = Variable("")
}

Resources