tableView ambiguous reference error
I am facing this error like many of you faced already. I have following code in my UITableViewController:
import Foundation
import RxSwift
import RxCocoa
class DiscoveryViewController : UITableViewController {
// MARK: - Properties
let viewModel = MFMovieListViewModel()
let disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
// MARK: - Rx binding
private func setupBindings() {
self.viewModel
.movies
.bind(to: tableView
.rx
.items(cellIdentifier: MovieListCell.DefaultReuseIdentifier,
cellType: MovieListCell.self)) {
(row, movie, cell) in
cell.configure(with: movie)
}.addDisposableTo(self.disposeBag)
}
}
View Model looks like:
import Foundation
import RxSwift
import RxCocoa
class MFMovieListViewModel {
// MARK: - Properties
lazy var movies: Observable<[MovieListMDB]> = {
return MFAPIClinet.sharedInstance().popularMovies()
}()
}
Don't think it's related to Xcode 8.3 or OSX 10.12, but still tried restarting but nothing is resolved. Appreciate any help provided.
Found the issue. DefaultReuseIdentifier wasn't defined in the MovieListCell. :)
Funny error message though!
Related
So I have the following function:
var categoryArray: [Category] = []
private func loadCategories() {
downloadCategoriesFromFirebase { (allCategories) in
self.categoryArray = allCategories
self.collectionView.reloadData()
}
}
that is currently in my SomeViewController. I need to move it to SomeViewModel.
Sorry if my question is not well formulated, please do ask if you need any more information since I am so new at all this.
Thanks in advance
Assuming you have a reference to SomeViewModel in your SomeViewController, you can move the code over there.
For example:
import UIKit
class ViewController: UIViewController {
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
callToViewModel()
}
func callToViewModel() {
viewModel.loadCategories {
self.collectionView.reloadData()
}
// rest of your code
}
The ViewModel could look something like this:
import Foundation
class ViewModel: NSObject {
var categoryArray: [Category] = []
override init() {
super.init()
}
func loadCategories(completion: (() -> Void)?) {
downloadCategoriesFromFirebase { (allCategories) in
self.categoryArray = allCategories
completion?()
}
}
}
When reloading your data, make sure to refer to viewModel.categoryArray instead of self.categoryArray now.
Please notice that I'm making use of a completion handler to notify the ViewController the call in the ViewModel was finished. There are other (perhaps better) ways to do this, like the Combine framework.
I am attempting to make a to-do app with a controller for both "this week" and "next week." (Objects with a date in the current week will be shown in the thisWeek view controller, while objects with a date next week will be shown in the nextWeek view controller. However, sometimes objects will not appear in the table until the app restarts, and I'm not sure why this is happening or how to fix it.
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed. I just don't know how to access the table from a different controller than the one I am in.
Attempt 1 I tried to use a singleton that looked like this:
class ToDoTables {
private init() {}
static let shared = ToDoTables()
lazy var thisTable: UITableView = {
let thisWeek = UITableView()
return thisWeek
}()
lazy var nextTable: UITableView = {
let nextWeek = UITableView()
return nextWeek
}()
}
then to be called like:
let thisTable = ToDoTables.shared.thisTable
However this did not work because I initially created the table using storyboard and just used an IBOutlet in the controller, and couldn't find way to keep the outlet and also use singleton. Next, I tried to remove the outlet and just use the code above. When I simulated the app, it crashed because the outlet couldn't be found or something like that (which was expected).
Attempt 2
I tried to access the other controller's table by creating an instance of the vc and accessing the table that way:
NextWeekController().nextTable.reloadData()
but got an error that said "Unexpectedly found nil while implicitly unwrapping an Optional value"
So I'm not sure if that is my tables need to be reloaded each time, or if it is something else.
Overview of non-functioning operations (an app restart is needed, or the controller/table needs loaded again):
something created in the nextWeek controller with a date in the current week will not appear
when an object in created in nextWeek controller, then date is changed from next week to this week
something created in the thisWeek controller with a date in the next week will not appear
when an object in created in thisWeek controller, then date is changed from this week to next week
My theory is that each table in the different controllers needs to be reloaded each time data is added/changed.
You are correct. For UITableView.reloadData(), the Apple Docs state to:
Call this method to reload all the data that is used to construct the
table
In other words, it is necessary to reload the table view's data with this method if the data is changed.
I just don't know how to access the table from a different controller
than the one I am in.
One solution is to create global references to all your view controllers through one shared instance.
// GlobalVCs.swift
class GlobalVCs {
static var shared = GlobalVCs()
var nameOfVC1!
var nameOfVC2!
var nameOfVC3!
// and so on
private init() {}
}
Then set the global vc references from each view controller's viewDidLoad() method.
Example:
// VC1.swift
override func viewDidLoad() {
GlobalVCs.shared.vc1 = self
}
Then you can access a view controller anywhere with GlobalVCs.shared.vc1, GlobalVCs.shared.vc2, etc.
Sorry, but need to say that this is wrong approach. We need to follow to MCV model where we have data model, view and controller. If you example for to-do model you could have:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct ToDoItem: Codable, Identifiable {
static let collection = "todos"
#DocumentID var id: String?
var uid: String;
var done: Bool;
var timestamp: Timestamp;
var description: String
}
Then you need to have repository for this type of data, for example if we are using Firebase we can have
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
class BaseRepository<T> {
#Published var items = Array<T>()
func add(_ item: T) { fatalError("This method must be overridden") }
func remove(_ item: T) { fatalError("This method must be overridden") }
func update(_ item: T) { fatalError("This method must be overridden") }
}
and then we have ToDoRepository (singleton object)
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import Resolver
import CoreLocation
import Combine
class ToDoRepository: BaseRepository<ToDoItem>, ObservableObject {
var db = Firestore.firestore()
uid: String? {
didSet {
load()
}
}
private func load() {
db.collection(ToDoItem.collection).whereField("uid", isEqualTo: uid).order(by: "timestamp").addSnapshotListener {
( querySnapshot, error ) in
if let querySnapshot = querySnapshot {
self.items = querySnapshot.documents.compactMap { document -> ToDoItem? in
try? document.data(as: ToDoItem.self)
}
}
}
}
}
Now we need to register repository in AppDelegate+Injection.swift:
import Foundation
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// Services
register { ToDoRepository() }.scope(application)
}
}
After that we can use ToDoRepository in any controller :
import UIKit
import Combine
import Resolver
class MyController: UIViewController {
private var cancellables = Set<AnyCancellable>()
#LazyInjected var toDoRepository: ToDoRepository
// and listen and update any table view like this
override func viewDidLoad() {
super.viewDidLoad()
toDoRepository.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] todos
self?.todos = todos
tableView.reloadData()
}
.store(in: &cancellables)
}
}
I'm having problems with deallocating an observable, even using a disposedBag. It only occurs in iOS 10.
I have to associate a TextField (SwiftMaskField) value to a variable at viewModel, so I'm doing:
class BaseViewController: UIViewController, Storyboarded {
#IBOutlet weak var txtField: SwiftMaskField!
var viewModel: BaseViewModel!
override func viewDidLoad() {
super.viewDidLoad()
bindUI()
}
private func bindUI() {
txtField.rx.text.orEmpty.bind(to: viewModel.myString).disposed(by: viewModel.bag)
viewModel.showLoading.asObservable().skip(1).subscribe(onNext: { [unowned self] in
self.showLoading()
}).disposed(by: viewModel.bag)
}
...
}
class BaseViewModel {
var showLoading = BehaviorRelay<Void>(value: ())
var myString = BehaviorRelay<String>(value:"")
let bag = DisposeBag()
func foo() {
showLoading.accept(())
}
func foo2() {
print(myString.value)
}
...
}
When I do a popViewController, my app crashes with the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance
0x7fbaafe45a20 of class SwiftMaskText.SwiftMaskField was deallocated
while key value observers were still registered with it. Current
observation info: (
Context: 0x0,
Property: 0x6000002596e0> )'
It only occurs in iOS 10 (haven't tested previous versions), further versions don't crash.
Also, I'm using RxSwift 5.0.
Your bag has to be above your view in the class hierarchy. Try this:
class BaseViewController: UIViewController, Storyboarded {
private let bag = DisposeBag()
#IBOutlet weak var txtField: SwiftMaskField!
var viewModel: BaseViewModel!
override func viewDidLoad() {
super.viewDidLoad()
bindUI()
}
private func bindUI() {
txtField.rx.text.orEmpty.bind(to: viewModel.myString).disposed(by: bag)
viewModel.showLoading.asObservable().skip(1).subscribe(onNext: { [unowned self] in
self.showLoading()
}).disposed(by: bag)
}
...
}
That will likely fix it, let me know if it doesn't.
Also, your view model shouldn't need a dispose bag, if it does you are probably doing something wrong in there.
I've been trying to get started with Realm (version 4.3.0) as a database option with Xcode 11. With my Googling skills I could not get an answer for my problems. I've tried to use the Official Realm documentation but it appears that the way they do things is not working with Xcode 11. Basic code:
import UIKit
import RealmSwift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
class Test: Object {
#objc dynamic var text = ""
#objc dynamic var internalId = 0
}
let newTest = Test()
newTest.text = "Text" // Errors happen here
print("text: \(newTest.text)")
}
I get an errors that I definitely was not expecting:
Consecutive declarations on a line must be separated by ';'
Expected '(' in argument list of function declaration
Expected '{' in body of function declaration
Expected 'func' keyword in instance method declaration
Expected declaration
Invalid redeclaration of 'newTest()'
Also when I'm trying to initialize and write to Realm with:
let realm = try! Realm()
try! realm.write { // Error here
realm.add(newTest)
}
I get the error of "Expected declaration"
From what I've read, Realm seems like a really nice database option for iOS, but with these problems, I cannot get up and running. Any help would be greatly appreciated.
Let's rearrange the code so the objects and functions are in their correct place.
import UIKit
import RealmSwift
//this makes the class available throughout the app
class Test: Object {
#objc dynamic var text = ""
#objc dynamic var internalId = 0
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//create a new realm object in memory
let newTest = Test()
newTest.text = "Text"
print("text: \(newTest.text)")
//persist the object to realm
let realm = try! Realm()
try! realm.write {
realm.add(newTest)
}
//or read objects
let results = realm.objects(Test.self)
for object in results {
print(object.text)
}
}
}
Like #108g commented:
I was trying to create an instance at class level. So I moved the creation, modification and the print inside viewDidLoad() method. Then I moved my Test class into a new file.
So the code that works:
ViewController.swift
import UIKit
import RealmSwift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let newTest = Prompt()
newTest.text = "Text"
print("text: \(newTest.text)")
let realm = try! Realm()
try! realm.write {
realm.add(newTest)
}
}
}
And RealmTest.swift (new file)
import Foundation
import RealmSwift
class Prompt: Object {
#objc dynamic var text = ""
#objc dynamic var internalId = 0
}
I have a simple ViewModel with one property:
class ViewModel {
var name = Variable<String>("")
}
And I'm binding it to its UITextField in my ViewController:
class ViewController : UIViewController {
var viewModel : ViewModel!
#IBOutlet weak var nameField : UITextField!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Binding
nameField.rx.text
.orEmpty
.bind(to: viewModel.name)
.disposed(by: disposeBag)
}
}
Now, I want to Unit Test it.
I'm able to fill the nameField.text property though Rx using .onNext event - nameField.rx.text.onNext("My Name Here").
But the viewModel.name isn't being filled. Why? How can I make this Rx behavior work in my XCTestCase?
Please see below the result of my last test run:
I believe the issue you are having is that the binding is not explicitly scheduled on the main thread. I recommend using a Driver in this case:
class ViewModel {
var name = Variable<String>("")
}
class ViewController: UIViewController {
let textField = UITextField()
let disposeBag = DisposeBag()
let vm = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text
.orEmpty
.asDriver()
.drive(vm.name)
.disposed(by: disposeBag)
}
// ..
With a Driver, the binding will always happen on the main thread and it will not error out.
Also, in your test I would call skip on the binding:
sut.nameTextField.rx.text.skip(1).onNext(name)
because the Driver will replay the first element when it subscribes.
Finally, I suggest using RxTest and a TestScheduler instead of GCD.
Let me know if this helps.
rx.text relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Thus, explicitly send a onNext events will not send action for these event. You should send action manually.
sut.nameField.rx.text.onNext(name)
sut.nameField.sendActions(for: .valueChanged)