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.
Related
I have a xibs, in my homeVC, I m passing a viewModel in didselectItem in collectionView's method. After that, I m navigating detailVC and I assign my delegate to self there but I could not my print data
HomeVC
protocol HomeViewControllerDelegate {
func didTappedIndex(viewModel: HomeCollectionViewCellViewModel)}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let game = games[indexPath.row]
self.delegate?.didTappedIndex(viewModel: HomeCollectionViewCellViewModel(with: game))
self.tabBarController?.navigationController?.pushViewController(DetailViewController(nibName: "DetailViewController", bundle: nil), animated: true)
}
DetailVC
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
HomeViewController().delegate = self
}
}
extension DetailViewController: HomeViewControllerDelegate {
func didTappedIndex(viewModel: HomeCollectionViewCellViewModel) {
print(viewModel.title)
}
}
when you call
HomeViewController().delegate = self
you are creating a new instance of HomeViewController(). You are probably confusing this instance with one you've created somewhere else. You want to assign the delegate of the correct HomeViewController.
one option would be to add an instance variable and shared function to the HomeViewController that allows it to keep track of its instance:
private static var instance: HomeViewController?
internal class func shared() -> HomeViewController {
guard let currentInstance = instance else {
instance = HomeViewController()
return instance!
}
return currentInstance
}
then you can set the delegate in DetailViewController like this:
HomeViewController.shared().delegate = self
Another option would be to have a Coordinator where you keep track of all view controllers. Then you could pass the DetailViewController the HomeViewController instance.
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
// ...
}
}
}
I have a main view controller executed first which looks something like below,
MainViewController
class MainViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: DataSource!
SomeAction().call() {
self.dataSource.insert(message: result!, index: 0)
}
}
DataSource of the collectionview
class DataSource: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var conversation: [messageWrapper] = []
override init() {
super.init()
}
public func insert(message: messageWrapper, index: Int) {
self.conversation.insert(message, at: index)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return conversation.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let textViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "textViewCell", for: indexPath) as! TextCollectionViewCell
let description = conversation[indexPath.row].description
textViewCell.textView.text = description
return textViewCell
}
}
So, when the MainViewController is executed there is one added to the datasource of the collectionview which works perfectly fine.
Problem
Now, I have another class which looks something like
SomeController
open class SomeController {
let dataSource: DataSource = DataSource()
public func createEvent() {
self.dataSource.insert(message: result!, index: 1)
}
}
When I add some data from the above controller, the conversation is empty which doesn't have the existing one record and throw Error: Array index out of range. I can understand that it is because I have again instantiated the DataSource.
How to add/remove data from other class?
Is it the best practice to do it?
Can I make the conversation as global variable?
The Datasource class had been re initialised with it's default nil value, you have to pass the updated class to the next controller to access its updated state.
How to add/remove data from other class?
You should use class Datasource: NSObject {
And your collection view delegates on your viewcontroller class.
pass your dataSource inside prepareForSegue
Is it the best practice to do it?
Yes
Can I make the conversation as global variable?
No, best to use models / mvc style. Data on your models, ui on your viewcontrollers.
It seems your initial count is 1 but you insert at index 1(out of index)
Use self.dataSource.insert(message: result!, index: 0) insteaded
Or use append.
I currently have a Viewcontroller which does the heavy lifting of retrieving the data. I then need to push this data to my UICollectionView inside of a UIContainerView. I have tried pushing via a segue but I need to keep refreshing the data so I keep getting the error:
'There are unexpected subviews in the container view. Perhaps the embed segue has already fired once or a subview was added programmatically?'
I then went on to investigate delegates and made a protocol to share the data between the two classes, however how can I initiate a a reloadData function from my initial view controller?
I'm still very new to delegates but I've setup the following:
protocol ProductsViewControllerDelegate {
var catalogue : CatalogueData {get set}
}
I then get my first view controller to inherit this delegate and pass data to it. I then created a delegate instance in the collection view and at the segue I set the collectionview.delegate = self however how do I refresh the data when I pass new data to it?
UPDATE
So I believe my problem is that the data isn't passing in the first place. My setup is as follows:
View controller:
struct CatalogueData {
var data : NSArray
var numberOfRows : Int
var numberOfColumns : Int
var tileName : XibNames
}
class ProductsViewController:UIViewController, CollectionCatalogueDelegate{
internal var catalogue: CatalogueData!
override func viewDidLoad() {
super.viewDidLoad()
let catdta : CatalogueData = CatalogueData(data: ProductCodes,
numberOfRows: 2,
numberOfColumns: 4,
tileName: XibNames.ProductDisplayTall)
self.catalogue = catdta
}
}
The second collection view inside of the container view is setup like so:
protocol CollectionCatalogueDelegate {
var catalogue : CatalogueData! {get set}
}
class CollectionCatalogue: UICollectionViewController, UICollectionViewDelegateFlowLayout{
#IBOutlet var cv: UICollectionView?
var delegate : CollectionCatalogueDelegate?{
didSet{
cv?.reloadData()
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return (self.delegate?.catalogue.data.count)!
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
self.cv?.register(UINib(nibName: (self.delegate?.catalogue?.tileName.rawValue)!, bundle: nil), forCellWithReuseIdentifier: (self.delegate?.catalogue?.tileName.rawValue)!)
let c:ProductCollectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: selectedXib.rawValue, for: indexPath) as! ProductCollectionCell
c.layer.borderColor = UIColor.black.cgColor
c.layer.borderWidth = 0.5
c.layer.cornerRadius = 3
let mng = (self.delegate?.catalogue?.data)![indexPath.row] as? NSManagedObject
'...........DO STUFF WITH DATA HERE ETC
}
}
And dependant on other actions I will then want to update the 'catalogue' in the first view controller and it display the results on the collection view.
I am trying to create a subclass of UITableView in Swift.
var tableScroll = HorizontalScrollTableView(frame: CGRectMake(15, 15, cell.contentView.frame.width - 30, cell.contentView.frame.height - 30))
cell.contentView.addSubview(tableScroll)
I add the table in the following way and then I have a HorizontalScrollTableView swift file with
import UIKit
class HorizontalScrollTableView : UITableView {
func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 1
}
...
However the Table presents like a normal default table and the functions in HorizontalScrollTableView do not override the default ones.
You're probably looking to override numberOfSections instead of numberOfSectionsInTableView:, which is a method on the UITableViewDataSource protocol. However, I believe it's more wise to create your own subclass of UITableViewController or your class conforming to UITableViewDataSource and set it as your table view's delegate rather than of the table view itself. Subclassing UIView and its descendants is usually reserved for customizing view-specific functionality and attributes, such as adding custom drawing or custom touch handling.
I went ahead and made a simple example using a Playground. You can see table view isn't subclassed, but instead there is a view controller which serves as the table view's delegate and a stand-in data source for the table view.
The data source provides the row data for the table view and the delegate (which is also the view controller) provides the cells.
This is a skeleton for how I normally set up my table views, although I would use an XIB generally.
import UIKit
// data for table view is array of String
let rowData = [ "Chicago", "Milwaukee", "Detroit", "Lansing" ]
//
// simple subclass of UITableViewCell that has an associated ReuseIdentifier
// and a value property. Setting the value property changes what the cell displays
//
public class TableViewCell : UITableViewCell
{
public static let ReuseIdentifier = "TableViewCell"
var value:AnyObject? {
didSet {
self.textLabel!.text = value as? String
}
}
}
//
// Simple implementation of a table view data source--just contains one String per row
// You could change your rowData to be an array of Dictionary for richer content possibilities.
// You could also load your data from a JSON file... for example
//
class TableViewDataSource : NSObject, UITableViewDataSource
{
var rowData:[String]?
init( rowData:[String] )
{
self.rowData = rowData
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return rowData?.count ?? 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
assert( indexPath.section == 0 ) // we only have 1 section, so if someone section ≠ 0, it's a bug
var cell:TableViewCell! = tableView.dequeueReusableCellWithIdentifier( "Cell" ) as? TableViewCell
if cell == nil
{
cell = TableViewCell( style: .Default, reuseIdentifier: "Cell" )
}
cell.value = self.rowData![ indexPath.row ]
return cell
}
}
// This is the view controller for our table view.
// The view controller's view happens to be a table view, but your table view
// could actually be a subview of your view controller's view
class TableViewController : UIViewController, UITableViewDelegate
{
// data source for our table view, lazily created
lazy var tableViewDataSource:TableViewDataSource = TableViewDataSource( rowData: rowData )
override func loadView()
{
// our view is a UITableView
let tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self.tableViewDataSource // using a self-contained data source object for this example
self.view = tableView
}
}
let window:UIWindow! = UIWindow()
window.rootViewController = TableViewController()
window.makeKeyAndVisible() // click the "preview eyeball" to see the window