Accessing view controller methods inside another class [Swift] - ios

I am using UITableViewDiffableDataSource for my tableview's dataSource by creating a UITableViewDiffableDataSource class inside my ViewController (AnimalsVC). Whenever I try to get my ViewController's data array (or any other variables/functions) from inside my data source class I get this error:
Instance member 'animalsArray' of type 'AnimalsVC' cannot be used on an instance of nested type 'AnimalsVC.DataSource'
I am not sure why I'm getting this error because my DataSource class is inside my ViewController class. Here is my code:
class AnimalsVC: UIViewController {
var animalsArray = []
class DataSource: UITableViewDiffableDataSource<Int, Animal> {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
let items = animalsArray //<- This is where the error occurs
return true
}
}
}

I am not sure why getting this error because my DataSource class is inside my ViewController class.
That makes no difference. Declaring one class inside of another merely namespaces the inner class, i.e. it is now called AnimalsVC.DataSource. It does not cause one instance of the inner class to be able to see magically inside an instance the outer class (and indeed it is completely unclear what instances we would be talking about).
Your nesting of class declarations is useless, so you might as well not do it.
Instead, if DataSource needs to see inside AnimalsVC, do what you would normally do: give your DataSource instance a reference to the AnimalsVC instance:
class AnimalsVC: UIViewController {
var animalsArray = // ...
}
class DataSource: UITableViewDiffableDataSource<Int, Animal> {
weak var vc : AnimalsVC?
// ...
}
When you create your DataSource instance, set its vc to self. Now the DataSource can consult the instance properties of the AnimalsVC instance.
(Actually, what I do in my own code is give my UITableViewDiffableDataSource subclass a custom designated initializer. That way, I can create the data source and hand it a reference to the view controller all in one move.)

In order to access the outer class from the inner, you need to pass a reference in to it. It is not automatic, as with Java, for instance.
Assuming the Animal object is defined elsewhere, then what you could do is:
class AnimalsVC: UIViewController {
var animalsArray = [] as [Animal]
class DataSource: UITableViewDiffableDataSource<Int, Animal> {
var myanimal:AnimalsVC
init(animal: AnimalsVC){
myanimal = animal
super.init()
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
let items = myanimal.animalsArray //<- This should not be an error any more
return true
}
}
}

The data should be contained in the DataSource class. Move your animalsArray into DataSource class.
If you need to access it easily from anywhere, declare it under DataSource as:
static var animalsArray = [Animal]()
You can then access it from anywhere using DataSource.animalsArray, such as:
DataSource.animalsArray.append(animal)
I guess you could create it as a static var under AnimalsVC instead, but the data should really be declared in the data source.
This would make your class look like this:
class AnimalsVC: UIViewController {
class DataSource: UITableViewDiffableDataSource<Int, Animal> {
static var animalsArray = [Animal]()
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
let items = DataSource.animalsArray
return true
}
}
}

It is not possible because.
"NestedTypes" it is not possible to access instance member or function
in other class even Container class unlike "Inheritance" that you can
access instance member or function of parent class.
If you want your implementation work with no error. "Static" will help you.
struct Animal {
var label: String
}
class AnimalVC {
static var animals: [Animal] = []
var dataSource = DataSource()
class DataSource {
func add(_ animal: Animal) {
animals.append(animal)
}
func display() {
print(animals.map { $0.label })
}
func remove(_ index: Int) -> String? {
guard index < (animals.count - 1) else { return nil }
return animals.remove(at: index).label
}
}
func load() {
dataSource.display()
dataSource.add(Animal(label: "Dog"))
dataSource.display()
for i in ["Cat", "Fish", "Bird"] {
dataSource.add(Animal(label: i))
}
dataSource.display()
print(vc.dataSource.remove(AnimalVC.animals.count) ?? "Cannot delete.")
}
}
Test.
let vc = AnimalVC()
vc.load()
NestedTypes
convenient to define utility classes and structures purely for use within the context of a more complex typequote
Inheritance
Inherits characteristics from the existing class
Source
NestedTypes: https://docs.swift.org/swift-book/LanguageGuide/NestedTypes.html
Inheritance :https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html

Related

How to make Base class property generic in swift so can use with multiple model type?

I want the property "cellViewModel" as generic so I can reuse BaseCustomCell with a different types of models.
Ex.
struct CELLVIEWMODEL {
var name: String
var address: String
}
class BaseCustomCell: UITableViewCell {
var cellViewModel: CELLVIEWMODEL //should support different model types CELLVIEWMODEL1,CELLVIEWMODEL2
{
didSet() {
setValuesInSubClasses
}
}
func setValuesInSubClasses() {
//subclass will implement
}
}
class subCell: BaseCustomCell {
override func setValuesInSubClasses() {
//set value from cellViewModel
}
}
//This is how i am setting from cellForRowAtIndexPath method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: viewModel.getCellId(), for: indexPath) as! BaseCustomCell
cell.cellViewModel = viewModel.getCellModelAtIndexPath(indexPath) //this will set values for subclasses
return cell
}
Now, I am creating new BaseCustomCell each time for different types of cellViewModel. Could you help with any solution?
There are several ways to achieve your goals.
You can make the actual BaseCustomCell glass generic, but be aware if you use Storyboards, this isn't a solution, since you'd need to hardcode the generic type into the storyboard.
The other solution that works with storyboards too is to declare your viewmodel as a protocol, then you can replace it with any concrete implementation of the protocol.
protocol CellViewModel {
var name: String { get }
var address: String { get }
}
class BaseCustomCell: UITableViewCell {
var cellViewModel: CellViewModel {
didSet() {
setValuesInSubClasses
}
}
func setValuesInSubClasses() {
//subclass will implement
}
}
class SubCell: BaseCustomCell {
override func setValuesInSubClasses() {
//set value from cellViewModel
}
}
And then your viewModel.getCellModelAtIndexPath should have a return type of CellViewModel, so it can return any type that conforms to the protocol.
So you simply need to declare your concrete cell view models like class FirstCellViewModel: CellViewModel { ... }, etc. and you can return them from getCellModelAtIndexPath

Updating the tableview of superclass

I followed a tutorial to make a MVVP model tableview
My tableViewController is called MyProfileController and looks like this:
class MyProfileController: UITableViewController {
fileprivate var viewModel: ProfileViewModel?
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UserInfoCell.self, forCellReuseIdentifier: UserInfoCell.identifier)
viewModel = ProfileViewModel()
tableView.dataSource = self.viewModel
}
}
}
Rather than defining UITableViewDataSource in MyProfileController, I create a view model called ProfileViewModel and pass it to tableView.dataSource. The ProfileViewModel is defined like the following:
class ProfileViewModel: NSObject {
fileprivate var profile: UserProfile?
var items = [ProfileViewModelItem]()
init() {
super.init()
//...
}
}
extension ProfileViewModel: UITableViewDataSource {
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserInfoCell
cell.userDetailTextView.delegate = self
return cell
}
// ...
}
extension ProfileViewModel: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
////////////////
// ERROR HERE //
// tableView.beginUpdates()
// tableView.endUpdates()
////////////////
}
}
I'm setting a delegate to UITextView inside the cellForRowAt indexPath method so that textViewDidChange delegate method will be called when user types in the textview. Up to this point works. The problem is that I cannot update the tableView from here. How can I update the tablView of MyProfileController?
You can use closures to send messages to your table view controller.
Declare a closure variable in your data source object.
class ProfileViewModel: NSObject {
var textViewDidChange: (() -> Void)?
// If you need to send some data to your controller, declare it with type. In your case it's string.
// var textViewDidChange: ((String) -> Void)?
}
Send your message from your text field delegate to your newly created variable like this.
func textViewDidChange(_ textView: UITextView) {
self.textViewDidChange?()
// If you need to send your string, do it like this
// self.textViewDidChange?(textView.text)
}
As you can guess, your variable textViewDidChange is still nil so no message will pass through. So we should declare that now.
In your view controller where you have access to your data source, set the value of your closure.
class MyProfileController: UITableViewController {
fileprivate var viewModel: ProfileViewModel?
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UserInfoCell.self, forCellReuseIdentifier: UserInfoCell.identifier)
viewModel = ProfileViewModel()
tableView.dataSource = self.viewModel
// Set the closure value here
viewmodel.textViewDidChange = { [unowned self](/* if you are sending data, capture it here, if not leave empty */) in
// Do whatever you like with your table view here.
// [unowned self] could sound tricky. It's whole another subject which isn't in the scope of this question. But of course there are some great answers here about it. Simply put, if you don't use it, you'll create a memory leak.
}
}
}
There are lots of ways to do this. And it depends on your team's coding pattern rules or whatever should we call that.
But this is what I usually do: The view model has a protocol for reloadingData. Or better yet, the protocol of all my view models has a base class for such reloadData, like so:
protocol ProfileDelegate: BaseViewModelDelegate {
}
class ProfileViewModel: NSObject {
//....
}
And here goes the BaseViewModelDelegate:
/// The Base Delegate of all ViewModels.
protocol BaseViewModelDelegate: NSObjectProtocol {
/// Ask the controller to reload the data.
func reloadTableView()
/// Presents an alert/
func presentAlert(title: String, message: String, okayButtonTitle: String, cancelButtonTitle: String?, withBlock completion: LLFAlertCallBack?)
}
As you can see, there's a reloadTableView method. And that's where I reload the tableView of my controllers if needed. But again, there are lots of ways to do this. I hope this helps!
You can have your DataSource out of your view controller, but it’s important to follow the correct separation, I suggest this kind of approach because it can help you with tests.
Use a protocol to define the behavior of your view model (for testing you can have a mock view model that implement this protocol):
protocol ProfileViewModelType {
var items: [ProfileViewModelItem]
var textViewDidChange: ((UITextView) -> Void)?)
//...
}
Then implement your viewModel with the data:
class ProfileVieModel: ProfileViewModelType {
var items = [ProfileViewModelItem]()
//...
}
Then inject in your DataSource object the view model and use it to populate your table view and to manage all the callbacks:
class ProfileTableViewDataSource: NSObject, UITableViewDataSource {
private var viewModel: ProfileViewModelType!
init(viewModel: ProfileViewModelType) {
self.viewModel = viewModel
}
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
viewModel.textViewDidChange?(textView)
}
}
Finally in your view controller you can observe the view model callbacks and manage there your actions:
class YourViewController: UIViewController {
private var dataSource: ProfileTableViewDataSource?
private var viewModel: ProfileViewModelType = ProfileViewModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource = ProfileTableViewDataSource(viewModel: viewModel)
tableView.dataSource = dataSource
bindViewModel()
}
func bindViewModel() {
viewModel.textViewDidChange = { [weak self] textView in
// ...
}
}
}

what is the function of NSObject in StoryBoard / Interface Builder?

I am currently following a video tutorial course about test driven development of iOS in Swift, but when testing Table View in View Controller, I get stuck, since I don't understand why we need NSObject in Interface builder like the picture below:
Movie Library Data Service is inheritted NSObject class:
the class of MovieLibraryDataService is like this:
import UIKit
class MovieLibraryDataService: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
and the MovieLibraryDataService class will be used in XCTestCase is like this :
#testable import FilmFest
class LibraryViewControllerTests: XCTestCase {
var sut: LibraryViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
sut = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LibraryViewControllerID") as! LibraryViewController
_ = sut.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// MARK: Nil Checks
func testLibraryVC_TableViewShouldNotBeNil() {
XCTAssertNotNil(sut.libraryTableView)
}
// MARK: Data Source
func testDataSource_ViewDidLoad_SetsTableViewDataSource() {
XCTAssertNotNil(sut.libraryTableView.dataSource)
XCTAssertTrue(sut.libraryTableView.dataSource is MovieLibraryDataService)
}
// MARK: Delegate
func testDelegate_ViewDidLoad_SetsTableViewDelegate() {
XCTAssertNotNil(sut.libraryTableView.delegate)
XCTAssertTrue(sut.libraryTableView.delegate is MovieLibraryDataService)
}
// MARK: Data Service Assumptions
func testDataService_ViewDidLoad_SingleDataServiceObject() {
XCTAssertEqual(sut.libraryTableView.dataSource as! MovieLibraryDataService, sut.libraryTableView.delegate as! MovieLibraryDataService)
}
}
and the definition of LibraryViewController:
import UIKit
class LibraryViewController: UIViewController {
#IBOutlet weak var libraryTableView: UITableView!
#IBOutlet var dataService: MovieLibraryDataService!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
}
}
I really don't understand why I need to make that MovieLibraryDataService class
I usually use:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
but why do I need to write :
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
MovieLibraryDataService is just another class which implements UITableViewDataSource and UITableViewDelegate, with the difference that the storyboard instantiates it and that storyboard-created instance is bound via the IBOutlet #IBOutlet var dataService: MovieLibraryDataService!. All objects in the storyboard are storyboard-created, that’s why one has to bind them to variables to use if they are not bound to other variables you use already.
Naming the variable dataService is just a fancy way of saying that it is supposed to serve, in this case as dataSource and delegate for a tableView, since it implements those delegate-protocols.
Since the dataService is instantiated by the storyboard, you could try if you can bind the dataService inside the storyboard to tableView. This is possible because you have the dataService referencable in the storyboard. This would replace setting dataSource and delegate in the viewController.
Another example
AppDelegate is also an NSObject inside the Storyboard so the UIApplication/NSApplication inside the storyboard is able to reference that to use, avoids having to set up AppDelegate yourself outside a storyboard. (Would otherwise be ugly, maybe that’s macOS only though, since mac apps have to show a Menu even if its empty.)
Edit
You can use NSObject on the Storyboard for different purposes, one of them could be also the delegation. Instead of setting it programatically like:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
you can hold control and then set the corresponding delegates like below:
Original Answer
Because MovieLibraryDataService that conforms to UITableViewDataSource and UITableViewDelegate and not your LibraryViewController.
If you want to change it as you are always used to, change your code to:
class LibraryViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var libraryTableView: UITableView!
var dataService = MovieLibraryDataService()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
Personally I would suggest to keep it as you have it, as View Controllers tend to grow and become like we call it Massive View Controller

Protocol Design in Swift

I have a single view controller class that I reuse on multiple screens, on different screens I want to supply a different data source. My VC looks something like this:
class SpotViewController: UIViewController {
var dataSource: SpotDataSource!
}
Now I want to have two different Data Source Classes
class PersonalSpotDataSource {}
class ExploreSpotDataSource {}
Now I want to create some sort of a shared protocol that makes the above two classes implmenent the following properties
var spots: [Spot]
var title: String
And then I want to that shared construct to conform UITableViewDataSource since it has everything it needs which is just the list of spots and the title. And then going back to my first class I can supply either class (Personal or explore) as my VC's data source.
Is this possible?
So, first define your protocol:
protocol SpotDataSource {
var spots: [Spot] { get }
var title: String { get }
}
Then define your two classes to conform to that protocol:
class PersonalSpotDataSource: SpotDataSource {
let spots = [Spot(name: "foo"), Spot(name: "bar")]
let title = "Foobar"
}
class ExploreSpotDataSource: SpotDataSource {
let spots = [Spot(name: "baz"), Spot(name: "qux")]
let title = "Bazqux"
}
Clearly, your implementations of these will be more sophisticated than the above, but this illustrates the basic idea.
Anyway, having done that, define a UITableViewDataSource that uses this protocol:
class SpotTableViewDataSource: NSObject {
let spotDataSource: SpotDataSource
init(spotDataSource: SpotDataSource) {
self.spotDataSource = spotDataSource
super.init()
}
}
extension SpotTableViewDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return spotDataSource.spots.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SpotCell", for: indexPath) as! SpotCell
cell.spotLabel.text = spotDataSource.spots[indexPath.row].name
return cell
}
}
Finally, define you table view controller to use this UITableViewDataSource, passing it whatever SpotDataSource you want:
class PersonalSpotTableViewController: UITableViewController {
private let dataSource = SpotTableViewDataSource(spotDataSource: PersonalSpotDataSource())
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
}
}
Note, in that table view controller class, I defined the data source to be stored property because table view controllers don't keep strong reference to their data sources and/or delegates, but we want it to stay around.
Obviously, it would have been more graceful if we could have just put the UITableViewDataSource methods inside the SpotDataSource protocol's default implementation, but sadly we can't. So you have to create a concrete object that conforms to UITableViewDataSource and uses your SpotDataSource protocol, like outlined above.

Generic view controller won't work with delegate and extension

I already posted a question but it was not clear about what I want. As #AlainT suggested, I filed a new one.
I have a typealias tuple
public typealias MyTuple<T> = (key: T, value: String)
A protocol:
public protocol VCADelegate: class {
associatedtype T
func didSelectData(_ selectedData: MyTuple<T>)
}
A view controller (VCA) with a table view
class VCA<T>: UIViewController, UITableViewDelegate, UITableViewDataSource {
var dataList = [MyTuple<T>]()
weak var delegate: VCADelegate? // Error: can only be used as a generic constraint
// ...
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
In another view controller (VCB), I create a VCA and pass through a dataList
func callVCA() {
let vcA = VCA<String>()
vcA.dataList = [(key: "1", value:"Value 1"),
(key: "2", value:"Value 2")]
}
What I want to do is to have a dataList without knowing key's data type in VCA. Only when VCB calls VCA then I know the data type of the key. Creating a generic view controller will cause an issue with delegate. Any way to solve this problem without having to change to closure completion?
The other issue of using a generic view controller is I can't extend it. Any idea?
This is a standard type-erasure situation, though in this particular case I'd just pass a closure (since there's only one method).
Create a type eraser instead of a protocol:
public struct AnyVCADelegate<T> {
let _didSelectData: (MyTuple<T>) -> Void
func didSelectData(_ selectedData: MyTuple<T>) { _didSelectData(selectedData)}
init<Delegate: VCADelegate>(delegate: Delegate) where Delegate.T == T {
_didSelectData = delegate.didSelectData
}
}
Use that instead of a delegate:
class VCA<T>: UIViewController, UITableViewDataSource UITableViewDelegate {
var dataList = [MyTuple<T>]()
var delegate: AnyVCADelegate<T>?
// ...
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
Your underlying problem is that protocols with associated types are not proper types themselves. They're only type constraints. If you want to keep it a PAT, that's fine, but then you have to make VCA generic over the Delegate:
class VCA<Delegate: VCADelegate>: UIViewController, UITableViewDelegate {
var dataList = [MyTuple<Delegate.T>]()
weak var delegate: Delegate?
init(delegate: Delegate?) {
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
}
required init(coder: NSCoder) { super.init(nibName: nil, bundle: nil) }
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
class VCB: UIViewController, VCADelegate {
func didSelectData(_ selectedData: MyTuple<String>) {}
func callVCA() {
let vcA = VCA(delegate: self)
vcA.dataList = [(key: "1", value:"Cinnamon"),
(key: "2", value:"Cloves")]
}
}
As a rule, protocols with associated types (PATs) are a very powerful, but special-purpose tool. They aren't a replacement for generics (which are a general purpose tool).
For this particular problem, though, I'd probably just pass a closure. All a type eraser is (usually) is a struct filled with closures. (Some day the compiler will probably just write them for us, and much of this issue will go away and PATs will be useful in day-to-day code, but for now it doesn't.)

Resources