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
// ...
}
}
}
Related
I have a controller called MyController, which is UIViewController. Inside of the controller there's CollectionView. Let's call it myCollectionView. How would I create custom CollectionViewDelegate or DataSource such that I could use them for myCollectionView. I've already attempted to create custom delegate and data source but my collection view is just empty. I found that actually my custom delegate and data source's methods do not even get called which is why I basically see an empty collection view. What I've tried is:
final class MyController: UIViewController {
private lazy var collectionView = methodCreatesCollectionView()
private var items: [String]?
private var customDataSource: CustomDataSource?
init() {
super.init(nibName: nil, bundle: nil)
view.addSubview(collectionView)
customDataSource = CustomDataSource(parent: self)
collectionView.reloadData()
}
func fill(with items: [String]) {
self.items = items
}
func methodCreatesCollectionView() -> UICollectionView {
let cv = UICollectionView(
frame: self.view.bounds
)
cv.dataSource = customDataSource
return cv
}
}
What my custom data source looks like
private class CustomDataSource: NSObject, UICollectionViewDataSource {
weak var parent: MyController?
init(parent: MyController) {
self.parent = parent
}
func collectionView(
_: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
guard let numberOfItems = parent?.items?.count else {
return 0
}
return numberOfItems
}
func numberOfSections(
in _: UICollectionView
) -> Int {
return 1
}
}
Alright, I found my own mistake.
If anyone else is doing it like me, remember to call everything related to collection view before it gets called first time.
So you have to initialise your data source/delegate before you add the collection view into the view hierarchy because the way it gets created is lazy. So it'll be created only when it's accessed/called first time.
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
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.)
I'm trying to implement the delegation pattern on Swift. The process consists in a popover that is displayed from a UIMenuItem in a text selection on a textView. This popover is a TableViewController that contains some colors. When a cell (or color) is tapped, the selected text changes its color from black to the selected color. I have the following protocol in the sending class:
protocol SelectedColorDelegate {
func didSelectColorCell(color: UIColor)
}
Then in the sending class I created this property:
var colorCellDelegate: SelectedColorDelegate?
In the method didSelectRowAtIndexPath of the tableViewController (popover) that is the sending class, I assigned the required parameter:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let color = arrayOfColorValues[indexPath.row]
self.colorCellDelegate?.didSelectColorCell(color: color)
}
In my receiving class that is a ViewController I set the protocol SelectedColorDelegate, and conformed to it with this method, aimed to change the textColor:
func didSelectColorCell(color: UIColor) {
let textRange = noteTextView.selectedRange
let string = NSMutableAttributedString(attributedString: noteTextView.attributedText)
string.addAttribute(NSForegroundColorAttributeName, value: color, range: textRange)
noteTextView.attributedText = string
noteTextView.selectedRange = textRange
}
But the last method is never called, tapping the cell of the popover does nothing, what am I missing or doing wrong? Thanks!! :)
First of all define your protocol as only for classes
protocol SelectedColorDelegate: class {
func didSelectColorCell(color: UIColor)
}
Secondly we want our delegate to be weakly retained
weak var colorCellDelegate: SelectedColorDelegate?
Finally set delegate when you show other view or in viewDidLoad eg:
class YourViewController: SelectedColorDelegate {
final override func viewDidLoad() {
super.viewDidLoad()
self.colorCellDelegate = self
}
}
Tutorial - How To Make Weak Delegates In Swift
In PopOverTableViewController, setup should look like -
class PopOverTableViewController: UITableViewController, SelectedColorDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.colorCellDelegate = self
}
}
Did you do: xxTableViewController.colorCellDelegate = self in xxViewController?
And your delegate declaration should be weak:
weak var colorCellDelegate: SelectedColorDelegate?
I have UITableView with two static cells. Each cell has custom class and independently validate account name, when I fill text field in the cell. (This part of code I got as is and I am not allowed to rewrite it). The cell delegates about changes if validation is correct to delegate (SocialFeedSelectCellDelegate). Originally, this tableView appeared in SignUpViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate, SocialFeedSelectCellDelegate only.
Problem : The same UITableView should appear in two different places (SignUpViewController and SettingsViewController). Also SignUpViewController and SettingsViewController should know about success or fail of account validation.
What I tried : I created SocialFeedTableViewController: UITableViewController, SocialFeedSelectCellDelegate for the tableView with two cells. Set view in SocialFeedTableViewController as container view for SignUpViewController and SettingsViewController. I used second delegation (from SocialFeedTVC to SignUp and Settings) to notify SignUp and Settings about validation changes. I think it is bad idea, because of double delegation. Teammate said me that it is hard to understand.
Question: What is the best and simple design solution for the problem?
Why is the double delegation a problem? As far as I see it you have 2 table views, 1 for each controller. Then each controller sets the delegate to each of the table view as self. Even if not it is quite common to change the delegate of the object in runtime. It is also normal to have 2 delegate properties with the same protocol simply to be able to forward the message to 2 objects or more.
There are many alternatives as well. You may use the default notification center and be able to forward the messages this way. The only bad thing about it is you need to explicitly resign the notification listener from the notification center.
Another more interesting procedure in your case is creating a model (a class) that holds the data from the table view and also implements the protocol from the cells. The model should then be forwarded to the new view controller as a property. If the view controller still needs to refresh beyond the table view then the model should include another protocol for the view controller itself.
Take something like this for example:
protocol ModelProtocol: NSObjectProtocol {
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?)
}
class DelegateSystem {
class Model: NSObject, UITableViewDelegate, UITableViewDataSource, ModelProtocol {
// My custom cell class
class MyCell: UITableViewCell {
weak var modelDelegate: ModelProtocol?
var indexPath: NSIndexPath?
func onTextChanged(field: UITextField) { // just an example
modelDelegate?.cellDidUpdateText(self, text: field.text) // call the cell delegate
}
}
// some model values
var firstTextInput: String?
var secondTextInput: String?
// a delegate method from a custom protocol
func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?) {
// update the appropriate text
if cell.indexPath?.row == 0 {
self.firstTextInput = text
} else {
self.secondTextInput = text
}
}
// table view data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = MyCell() // create custom cell
cell.indexPath = indexPath // We want to keep track of the cell index path
// assign from appropriate text
if cell.indexPath?.row == 0 {
cell.textLabel?.text = self.firstTextInput
} else {
cell.textLabel?.text = self.secondTextInput
}
cell.modelDelegate = self // set the delegate
return cell
}
}
// The first view controller class
class FirstViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
let model = Model() // generate the new model
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when second view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// probably from some button or keyboard done pressed
func presentSecondController() {
let controller = SecondViewController() // create the controller
controller.model = model // assign the same model
self.navigationController?.pushViewController(controller, animated: true) // push it
}
}
// The second view controller class
class SecondViewController: UIViewController {
var tableView: UITableView? // most likely from storyboard
var model: Model? // the model assigned from the previous view controller
override func viewDidLoad() {
super.viewDidLoad()
refresh() // refresh when first loaded
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
refresh() // Refresh each time the view appears. This will include when third view controller is popped
}
func refresh() {
if let tableView = self.tableView {
tableView.delegate = model // use the model as a delegate
tableView.dataSource = model // use the model as a data source
tableView.reloadData() // refresh the view
}
}
// from back button for instance
func goBack() {
self.navigationController?.popViewControllerAnimated(true)
}
}
}
Here the 2 view controllers will communicate with the same object which also implements the table view protocols. I do not suggest you to put all of this into a single file but as you can see both of the view controllers are extremely clean and the model takes over all the heavy work. The model may have another delegate which is then used by the view controllers themselves to forward additional info. The controllers should then "steal" the delegate slot from the model when view did appear.
I hope this helps you understand the delegates are not so one-dimensional and a lot can be done with them.