setting ids on item in Stackview - ios

I have an array of object which I used to create a checkbox. The model has an id, name. I created a stackView to handle checkbox with id now I want to append items of selected checkbox to an array and be able to remove them when deselected. I am able to present all the views and it works well
below is my code
NetworkAdapter.instance.getFeaturesAmeneities()
.subscribe(onNext: {feat in
guard let data = feat.data else {return}
self.features.append(contentsOf: data)
self.stackFeature.axis = .vertical
self.stackFeature.distribution = .fill
self.stackFeature.spacing = 8
data.forEach {
print($0.id)
self.stv = CheckboxStackView()
self.stv?.label.text = $0.name
self.stv?.checkBox.tag = $0.id ?? 0
self.stackFeature.addArrangedSubview(self.stv!)
}
}).disposed(by: disposeBag)
Any help is appreciated

In order to answer this question, we first have to make the stack view reactive and declarative. This means that we have to be able to set the view using a single assignment and that needs to be an observer. This is just like what the RxCocoa library does for UICollectionView, UITableView and UIPickerView.
Writing the function is a bit advanced. First we take the signature from the other views above to define the shape of the function.
func items<Sequence: Swift.Sequence, Source: ObservableType>(_ source: Source) -> (_ viewForRow: #escaping (Int, Sequence.Element, UIView?) -> UIView) -> Disposable where Source.Element == Sequence
The above probably looks daunting. It's a function that takes a source sequence of sequences and returns a function that takes a closure for assembling the views and returns a Dispoable.
The completed function looks like this:
extension Reactive where Base: UIStackView {
func items<Sequence: Swift.Sequence, Source: ObservableType>(_ source: Source) -> (_ viewForRow: #escaping (Int, Sequence.Element, UIView?) -> UIView) -> Disposable where Source.Element == Sequence {
return { viewForRow in
return source.subscribe { event in
switch event {
case .next(let values):
let views = self.base.arrangedSubviews
let viewsCount = views.count
var valuesCount = 0
for (index, value) in values.enumerated() {
if index < viewsCount {
// update views that already exist
_ = viewForRow(index, value, views[index])
}
else {
// add new views if needed
let view = viewForRow(index, value, nil)
self.base.addArrangedSubview(view)
}
valuesCount = index
}
if valuesCount + 1 < viewsCount {
for index in valuesCount + 1 ..< viewsCount {
// remove extra views if necessary
self.base.removeArrangedSubview(views[index])
views[index].removeFromSuperview()
}
}
case .error(let error):
fatalError("Errors can't be allowed: \(error)")
case .completed:
break
}
}
}
}
}
The above can be used like this:
self.stackFeature.axis = .vertical
self.stackFeature.distribution = .fill
self.stackFeature.spacing = 8
let features = NetworkAdapter.instance.getFeaturesAmeneities()
.map { $0.data }
.share(replay: 1)
features
.bind(onNext: { [weak self] in self?.features.append(contentsOf: $0) })
.disposed(by: disposeBag)
features
.bind(to: stackFeature.rx.items) { (row, element, view) in
let myView = (view as? CheckboxStackView) ?? CheckboxStackView()
myView.label.text = element.name
myView.checkBox.tag = element.id ?? 0
return myView
}
.disposed(by: disposeBag)

Related

Incorrect argument label in call (have 'named:', expected 'systemName:')

I want to show the contents of the collection view through search. However, there are many errors while binding collection views. I think the model code got weird first, where is it weird? How can we fix this error?
output.loadData.map(CollectionOfOne.init).bind(to: collectionView.rx.items) { (row,collectionView, element) -> UICollectionViewCell in
let indexPath = IndexPath(index: 1)
guard let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "friendCell", for: indexPath) as? FriendCell else {
return FriendCell()
}
cell.profileImage?.image = UIImage(systemName: element.image)
cell.nickNameLbl?.text = element.nickname
cell.listenBtn?.rx.tap.subscribe(onNext: {[unowned self] in
selectIndexPath.accept(row)
}).disposed(by: cell.disposeBag)
return cell
}
output.searchNameResult.map(CollectionOfOne.init).bind(to: collectionView.rx.items) { collectionView, index, element -> UICollectionViewCell in
let indexPath = IndexPath(index: 0)
guard let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "freindCell", for: indexPath) as? FriendCell else {
return FriendCell()
}
// cell.profileImage?.image = UIImage(systemName: (element as AnyObject).image)
cell.profileImage?.image = UIImage(named: element.img)
cell.nickNameLbl?.text = (element.nickname)
return cell
}
This is viewcontroller code
struct input {
let loadData: Signal<Void>
let searchName: Driver<Void>
let searchHashTag: Driver<Void>
}
struct output {
let loadData: BehaviorRelay<[User]>
let searchNameResult: PublishRelay<users>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let loadData = BehaviorRelay<[User]>(value: [])
let searchName = PublishRelay<users>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
// loadData.accept(response!, User)
result.onCompleted()
default:
result.onNext("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
input.searchName.asObservable().withLatestFrom(input.name).subscribe(onNext: {[weak self] nickname in
guard let self = self else { return }
api.getUserList(nickname).subscribe(onNext: { (response,statuscode) in
switch statuscode {
case .ok:
result.onCompleted()
case .noHere:
result.onNext("no user")
default:
result.onNext("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
And this is my viewmodel.
What is the problem?
You are getting the error: Incorrect argument label in call (have 'named:', expected 'systemName:') because of this other error: Value of type 'users' has no member 'img'.
I expect that you don't really want to display a single cell but the code is so muddled in general that it's hard to make out exactly what you are trying to do. I suggest you find yourself a tutor who can teach you about types, or possibly use the Swift Playgrounds app to learn the basics of how to code before you try to make a real program.

RxSwift subscriptions after didBecomeActive

I've written module based on RxSwift with Viewcontroller and ViewModel. ViewModel contains gesture's observers and images observables. Everything works well, except situation when application didBecameActive directly to mentioned module. Subscriptions of gestures don't work and imageView become blank.
They are set inside subscription to observable based on BehaviorSubjects, inside view:
func subscribePhotos(observerable: Observable<[(Int, UIImage?)]>) {
disposeBag = DisposeBag()
observerable.subscribeOnNext { [weak self] array in
array.forEach { identifier, image in
if let pictureView = self?.subviews.first(where: { view -> Bool in
guard let view = view as? PictureView else {
return false
}
return view.identifier == identifier
}) as? PictureView {
pictureView.set(image)
}
}
}.disposed(by: disposeBag)
}
In viewModel I set Observable:
var imagesObservable: Observable<[(Int, UIImage?)]> {
do {
let collection = try photosSubject.value()
if let photosObservables = collectionCreator?.getPhotosDetailsObservables(identifiers: collection.photoIdentifiers) {
let photosObservable = Observable.combineLatest(photosObservables)
return Observable.combineLatest(photosSubject, photosObservable,
resultSelector: { collection, currentArray -> [(Int, UIImage?)] in
var newArray = [(Int, UIImage?)]()
currentArray.forEach { stringIdentifier, image in
if let picture = grid.pictures.first(where: { $0. stringIdentifier == stringIdentifier }) {
newArray.append((picture.identifier, image))
}
}
return newArray
})
}
} catch { }
return Observable<[(Int, UIImage?)]>.never()
}
}
photosSubject is initialized in viewModel's init
photosSubject = BehaviorSubject<PictureCollection>(value: collection)
photosObservale
func createImageObservableForAsset(asset: PHAsset, size: CGSize) -> Observable<UIImage?> {
return Observable.create { obs in
PHImageManager.default().requestImage(for: asset,
targetSize: size,
contentMode: .aspectFit,
options: nil,
resultHandler: { image, _ in
obs.onNext(image)
})
return Disposables.create()
}
}
And in ViewController I connect them by calling method of view:
myView.pictureView.subscribePhotos(observerable: viewModel.imagesObservable)
After didBecameActive pictureView's property image of type UIImage isn't nil, but they disappear. I could listen notification didBecameActive and invoke onNext on observer, but I’m not sure is it correct way to figure out problem. Any idea what's reason of that?
Finally, I solved out this issue. Reason wasn't connected with Rx. Method drawing pictures draw(_:CGRect) was called after didBecomeActive and cleared myView. I changed method's body and now everything works well :)

do(onNext:) called twice when table view row is selected

I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!

RxSwift computed Variable combined from 2 other Variables

I need to have var isOn: Variable<Bool> that is computed from 2 other Variables. How can I subscribe to value changes of the other Variables while being able to read latest value from my isOn Variable as well?
Below is the bad solution using 2 variables of type Observable and Variable. But I am looking for the correct solution, basically to merge my isOn and isOnVariable into single one.
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
var isOn: Observable<Bool> {
return Observable.combineLatest(from.asObservable(), to.asObservable()) { [weak self] to, from in
switch (from, to) {
case (.none, .none):
self?.isOnVariable.value = false
return false
default:
self?.isOnVariable.value = true
return true
}
}
}
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
Observable.merge(from.asObservable(), to.asObservable())
.map { [weak self] (_) -> Void in
guard let `self` = self else {
return
}
switch (self.from.value, self.to.value) {
case (.none, .none):
self.isOnVariable.value = false
default:
self.isOnVariable.value = true
}
}
.subscribe()
.disposed(by: disposeBag)
Now you can subscribe to isOnVariable an listen for value changes, and in any moment you can access values from the from and to variable. I'm not sure what you are trying to do, so you can just change logics in the .map but you got the idea.
If you are interested in why I'm using weak self this is a good reading http://adamborek.com/memory-managment-rxswift/
While working with #markoaras's answer, there is another option to use combineLatest and bind it to isOn Variable. It follows the same principle.
let isFromOpen = Variable<Bool>(false)
let isToOpen = Variable<Bool>(false)
let isOn = Variable<Bool>(false)
Observable.combineLatest(from.asObservable(), to.asObservable()).map{ (from, to) -> Bool in
return from != nil || to != nil
}.bind(to: isOn).disposed(by: bag)

RxSwift: code working only first time

I'm new in RxSwift. Some strange thing happens in my code.
I have a collection view and
Driver["String"]
Data for binding.
var items = fetchImages("flower")
items.asObservable().bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)
fetchImages
Function returns data
private func fetchImages(string:String) -> Driver<[String]> {
let searchData = Observable.just(string)
return searchData.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMap
{ text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://pixabay.com/api/?key=2557096-723b632d4f027a1a50018f846&q=\(text)&image_type=photo")
.debug()
.catchError { error in
print("aaaa")
return Observable.never()
}
}
.map { (response, json) -> [String] in // again back to .Background, map objects
var arr = [String]()
for i in 0 ..< json["hits"]!!.count {
arr.append(json["hits"]!![i]["previewURL"]!! as! String)
}
return arr
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOnError({ (type) in
print(type)
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
Strange thing is this. First time when I fetch with "flower" it works and return data, but when I add this code
self.searchBar.rx_text.subscribeNext { text in
items = self.fetchImages(text)
}.addDisposableTo(self.disposeBag)
It doesn't work. It doesn't steps in flatmap callback, and because of this, doesn't return anything.
It works in your first use case, because you're actually using the returned Driver<[String]> via a bindTo():
var items = fetchImages("flower")
items.asObservable().bindTo(...
However, in your second use case, you aren't doing anything with the returned Driver<[String]> other than saving it to a variable, which you do nothing with.
items = self.fetchImages(text)
A Driver does nothing until you subscribe to it (or in your case bindTo).
EDIT: To make this clearer, here's how you could get your second use case to work (I've avoided cleaning up the implementation to keep it simple):
self.searchBar.rx_text
.flatMap { searchText in
return self.fetchImages(searchText)
}
.bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)

Resources