How do I bind a ViewModel to a Collationview? - swift

I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
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:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?

Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}

Related

SwiftUI: How to Call a Function when Photo Picker Closes

I'm using a photo picker in my SwiftUI class to load photos and videos into an array. Right now I'm displaying those images after they've been selected in the picker. Works fine.
Instead, I'd like to run a function when I click the "Add" button and upload the objects in that array to Cloudinary for processing and storage. I can manually make this happen with a separate button outside the picker, but for the best UX, I think this function should run automatically when that "Add" button is selected.
How do I run a function when that "Add" button is clicked? Do I need to check if the array is not empty and some other condition exists instead?
Here's an image of the picker:
Here's the picker code:
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
#ObservedObject var mediaItems: PickedMediaItems
var didFinishPicking: (_ didSelectItems: Bool) -> Void
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .any(of: [.images, .videos, .livePhotos])
config.selectionLimit = 0
config.preferredAssetRepresentationMode = .current
let controller = PHPickerViewController(configuration: config)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(with: self)
}
class Coordinator: PHPickerViewControllerDelegate {
var photoPicker: PhotoPicker
init(with photoPicker: PhotoPicker) {
self.photoPicker = photoPicker
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
photoPicker.didFinishPicking(!results.isEmpty)
guard !results.isEmpty else {
return
}
for result in results {
let itemProvider = result.itemProvider
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first,
let utType = UTType(typeIdentifier)
else { continue }
if utType.conforms(to: .image) {
self.getPhoto(from: itemProvider, isLivePhoto: false)
} else if utType.conforms(to: .movie) {
self.getVideo(from: itemProvider, typeIdentifier: typeIdentifier)
} else {
self.getPhoto(from: itemProvider, isLivePhoto: true)
}
}
}
private func getPhoto(from itemProvider: NSItemProvider, isLivePhoto: Bool) {
let objectType: NSItemProviderReading.Type = !isLivePhoto ? UIImage.self : PHLivePhoto.self
if itemProvider.canLoadObject(ofClass: objectType) {
itemProvider.loadObject(ofClass: objectType) { object, error in
if let error = error {
print(error.localizedDescription)
}
if !isLivePhoto {
if let image = object as? UIImage {
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: image))
}
}
} else {
if let livePhoto = object as? PHLivePhoto {
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: livePhoto))
}
}
}
}
}
}
private func getVideo(from itemProvider: NSItemProvider, typeIdentifier: String) {
itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
if let error = error {
print(error.localizedDescription)
}
guard let url = url else { return }
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
guard let targetURL = documentsDirectory?.appendingPathComponent(url.lastPathComponent) else { return }
do {
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
}
try FileManager.default.copyItem(at: url, to: targetURL)
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: targetURL))
}
} catch {
print(error.localizedDescription)
}
}
}
}
}
I added this class to the View:
class PickerStatus: ObservableObject {
var status: Bool = false
}
Then added this line to the PhotoPicker:
#ObservedObject var finishedSelection: PickerStatus
Then in the Coordinator, I added this:
for result in results {
self.photoPicker.finishedSelection.status = true
...
}
Now in my View I can set the instance of the ObservedObject, pass it into my child views including the PhotoPicker and check the value of that same Observed Object:
#ObservedObject var finishedSelection = PickerStatus()

Instance method 'items' requires that 'listen' conform to 'Sequence'

I want to bind the value of tableViewCell. But there is an error like the title. I've never seen this error before and I want to know how to fix it.
func bindViewModel() {
let input = ListenViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList.bind(to: tableView.rx.items) { tableView, index, element -> UITableViewCell in
guard let cell = self.tableView.dequeueReusableCell(withIdentifier: "ListeningTableViewCell") as? ListeningTableViewCell else {
return ListeningTableViewCell()}
cell.listeningData = element
}.disposed(by: disposeBag)
}
This is my ViewController Code
class ListenViewModel: ViewModelType {
private let disposeBag = DisposeBag()
static var loadData = PublishRelay<listen>()
struct input {
let loadData: Signal<Void>
}
struct output {
// let isEnabled: Driver<Bool>
let result: Signal<String>
let loadApplyList: PublishRelay<listen>
}
func transform(_ input: input) -> output {
let api = ProfileAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<listen>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getListenigList("admin123#gmail.com").subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
case .noHere: result.onNext("fail")
default:
print("Default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: "실패"), loadApplyList: loadApplyList)
}
}
And this is my ViewModel Code.
How to fix this error?
To bind to a table view's items, you need a sequence of things, like an array, for example. Each element of that sequence will be displayed on a cell.
You've got a listen object here (output.loadApplyList), which isn't a sequence of things. If you just want to just display one cell, you should create a sequence with a single element.
There is a simple way to create a sequence with only a single element: CollectionOfOne, so you should map your listen objects to that.
output.loadApplyList.map(CollectionOfOne.init).bind(to: tableView.rx.items) { ... }

RxSwift withLatestFrom with resultSelector doesn't compile

I have a Driver of type Bool and a BehaviorRelay of type Page (which is a custom enum).
enum Page {
case option1(CustomClass1, CustomClass2)
case option2(CustomClass3)
case option3(CustomClass4)
var property1: CustomClass2? {
switch self {
case .option1(_, let custom):
return custom
case .option2, .option3:
return nil
}
}
}
I have the Driver<Bool> in another ViewModel.
class ViewModel1 {
struct Output {
let hasItems: Driver<Bool>
}
let output: Output
init() {
let hasItemsRelay: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
self.output = Output(
hasItems: hasItemsRelay.asDriver()
)
}
}
And I have a BehaviorRelay<Page?> in my base class.
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
init() {
self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
}
In ViewModel2 class I'm trying to catch an event on the hasItems driver of ViewModel1.Input and when I get an event, I need the current value of currentPageRelay and later on do stuff with it. So basically withLatestFrom is the thing I need to use.
class ViewModel2 {
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.unwrap()
.drive(onNext: { (property1, hasItems) in
// do stuff
}
.disposed(by: disposeBag)
}
}
Xcode completely loses it on the withLatestFrom. No code completion and it gives the following compile error:
Expression type '(Bool, _)' is ambiguous without more context
I'm completely in the dark about this one. I've already tried everything, providing the correct classes in the parameter list below it, so that it knows what to expect etc, but no luck so far.
I think because .withLatestFrom requires both types it operates on to be of the same observable trait. So both should be either Observable, Driver, Signal, etc.
If you want to keep your Driver in your viewModel a Driver you could add an .asObservable() after the .hasItems:
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
let disposeBag = DisposeBag()
init() {
// self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.asObservable()
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.asDriver(onErrorJustReturn: nil)
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)
}
}
Or add a .asDriver() to currentPageRelay in the withLatestFrom(..):
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay.asDriver()) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)

RxSwift and MVVM: observable not executing without binding

I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())

Correct usage of RxSwift with TableView

I don't know how transfer the data between ModelView and ViewController. In
SelectModelViewController
class SelectModelViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var errorLabel: UILabel!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
var markViewModel : MarkViewModel?
let markService = MarkService()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
markViewModel = MarkViewModel(markService: markService)
markViewModel?.data.asObservable()
.bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (_, element, cell) in
cell.textLabel?.text = element
}
.addDisposableTo(disposeBag)
}
}
MarkViewModel has an error. I'am doing something wrong
struct MarkViewModel {
let markService: MarkService
var data: Driver<[Mark]>
init(markService: MarkService) {
self.markService = markService
data = markService.get()
.map { result in
switch result {
case.success(let marks):
return marks.map { mark in
return mark
}
case .error(let error):
print(error)
}
}.asDriver(onErrorJustReturn: .error(.other))
}}
MarkService
struct MarkService {
func get() -> Observable<Result<[Mark]>> {
return URLSession.shared.rx.json(url: URL(string: API.BaseURL)!)
.retry(3)
.map {
var marks = [Mark]()
guard let json = $0 as? [String: Any],
let items = json["RBMarks"] as? [[String : Any]] else {
return .error(.badJSON)
}
for item in items {
if let mark = Mark(json: item) {
marks.append(mark)
} else {
return .error(.badJSON)
}
}
return .success(marks)
}
.catchErrorJustReturn(.error(.noInternet))
}}
First, we can return Observable<Result<[Mark]>> from MarkService instead of Any. This will be useful later on to display them.
struct MarkService {
func get() -> Observable<Result<[Mark]>> {
return URLSession.shared.rx.json(url: URL(string: API.BaseURL)!)
.retry(3)
.map {
var marks = [Mark]()
guard let json = $0 as? [String: Any],
let items = json["RBMarks"] as? [[String : Any]] else {
return .error(.badJSON)
}
for item in items {
if let mark = Mark(json: item) {
marks.append(mark)
} else {
return .error(.badJSON)
}
}
return .success(marks)
}
.catchErrorJustReturn(.error(.noInternet))
}
}
Then, let's remove the subscription to data in MarkViewModel. We'll also transform the marks to something that is more suited for presentation.
struct MarkViewModel {
let markService: MarkService
var data: Driver<[String]>
var marks: Variable<[Mark]>
let disposeBag = DisposeBag()
init(markService: MarkService) {
self.markService = markService
data = markService.get()
.map { result in
guard case .success(let marks) = result else {
return ["Error while getting marks"]
}
return marks.map { mark in
"For assignment \(mark.assignmentName), you were marked with \(mark.grade)/10"
}
}
.asDriver(onErrorJustReturn: .error(.other))
}
}
Now, in view controller, we can use RxSwift's table view bindings to display those data
let disposeBag = DisposeBag()
func viewDidLoad() {
viewModel.data
.bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (_, element, cell) in
cell.textLabel?.text = element
}
.addDisposableTo(disposeBag)
}
This is obviously only an example of how you could do it and code will change depending on the requirements for specific views.