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.
Related
I have a class in which I define a CollectionView which I use as a custom TabBar. It has three cells in it, each representing another tab. When I select a tab (which is thus a cell of the CollectionView), I want to update the text of a label inside my view.
In tabs.swift (where all the magic happens to set up the custom tabbar), I added the following function:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let uvController = UserViewController()
uvController.longLabel.text = "Test"
}
In UserViewController, I call it like this:
let ProfileTabs: profileTabs = {
let tabs = profileTabs()
return tabs
}()
It shows all the tabs I want, but when I select it, the label doesn't update. However, when I perform a print action, it does return the value of the label:
print(uvController.longLabel.text)
This returns the value I defined when I set up the label, so I can in fact access the label, but it doesn't update as I want it to do. Any insight on why this is not happening?
let uvController = UserViewController()
This line is the problem.
You instantiate the a new UserViewController instead of referencing to your current UserViewController, so that the label is not the same one. You can print(UserViewController) to check it out, the address should be different.
My suggestion for you can define a protocol in Tabs.swift and make your UserViewController a delegate of it, to receive the update action.
In the same time, let ProfileTabs: profileTabs is not a good naming convention as well, usually custom class should be in Capital letter instead of the variable.
This line - let uvController = UserViewController() creates a new instance of UserViewController which is not on the screen. You need to reference the one already shown to the user. You can do something like this :
The fastest way.
Just pass the instance in ProfileTabs initializer. Something like this:
class ProfileTabs {
let parentViewController: UserViewController
init(withParent parent: UserViewController) {
self.parentViewController = parent
}
// and then change to :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
parentViewController.longLabel.text = "Test"
}
}
The cleaner way. Use delegates.
protocol ProfileTabsDelegate: class {
func profileTabs(didSelectTab atIndex: Int)
}
class ProfileTabs {
weak var delegate: ProfileTabsDelegate?
init(withDelegate delegate: ProfileTabsDelegate) {
self.delegate = delegate
}
// and then change to :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.profileTabs(didSelectTab: indexPath.item)
}
}
And then in UserViewController
extension UserViewController: ProfileTabsDelegate {
func profileTabs(didSelectTab atIndex: Int) {
longLabel.text = "Test"
}
}
By using delegates, I'm trying to pass data from a view controller to a TableView class that will display the correct number of cells. First, I needed to pass the array to that class (using a String here instead for debugging)
Inside my ViewController:
protocol GroupBillVCDelegate{
func passFriendArray(string: String)
}
...
class GroupBillViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var delegate:GroupBillVCDelegate? // for delegate passing message
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (tableView == self.FriendTableView){
let friendListCell = tableView.dequeueReusableCell(withIdentifier: "friendListCell") as! CategoryRow
// transfer friends array to CategoryRow
self.delegate?.passFriendArray(string: "Hello There")
return friendListCell
}
}
}
Inside the TableViewCell class:
class CategoryRow: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, GroupBillVCDelegate {
var s:String = String()
func passFriendArray(string: String) {
s = string
print(string)
}
...
// used for horizontal scrolling
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
passFriendArray(string: s) // message supposed to appear here
print ("TEST")
return 10
}
...
}
I have trouble setting up the delegate. When I run, the console does not display the message. Why is it not being passed?
Think of a delegate as an intercom on your desk that can be connected to your assistant's office.
This line:
var delegate:GroupBillVCDelegate? // for delegate passing message
Defines the intercom, but doesn't connect it to anything. If you press the button on the intercom, it doesn't connect to anything.
The code self.delegate?.passFriendArray(string: "Hello There")
uses "optional chaining" to send a message to the delegate if there is one, and drop the message if there is no delegate.
The thing you're missing is assigning something to the delegate:
self.delegate = someObjectThatConformsToGroupBillVCDelegateProtocol
A delegate is a one-to-one relationship.
All that being said, it doesn't really make sense to make a table view cell the delegate of a view controller. The table view has more than one cell. Which cell should be the view controller's delegate? Why that cell and not another one?
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.
Im attempting to set up a colletionView inside a collectionView with separation between the data sources so its easier for me to manipulate UI elements.
I have my main ViewController which sets the datasource and delegate of the "outer" collection view.
class DownloadCollectionController: UIViewController {
#IBOutlet weak var outerCollectionView: UICollectionView!
var outerDataProvider : OuterDatasourceDelegate!
var outerDelegate: OuterDatasourceDelegate!
override func viewDidLoad() {
super.viewDidLoad()
// set the datasource fot the outer collection
outerDataProvider = OuterDatasourceDelegate()
// crashes on this line when selecting a cell
outerCollectionView.dataSource = outerDataProvider
outerDelegate = OuterDatasourceDelegate()
outerCollectionView.delegate = outerDelegate
}
}
The OuterDatasourceDelegate class which has some functions to populate the cell and then the willDisplayAt function the sets the datasource and delegate for the 'inner" collection view:
class OuterDatasourceDelegate: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return GlobalOperationStore.sharedInstance.globalPackArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// cell stuff...
return outerCell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// set the datasource and delegate for the inner collection
guard let outerViewCell = cell as? OuterCollectionCell else { return }
let dataProvider = InnerDatasourceDelegate()
let delegate = InnerDatasourceDelegate()
outerViewCell.initializeCollectionViewWithDataSource(dataProvider, delegate: delegate, forItem: indexPath.item)
outerViewCell.innerCollectionView.reloadData()
outerViewCell.innerCollectionView.layoutIfNeeded()
}
}
This InnerDataSourceDelegate class just has the same functions as above that are required to populate the cells.
And my custom cell class that has reference to the "inner" collectionView and contains the initializeCollectionViewWithDataSource function called above:
class OuterCollectionCell: UICollectionViewCell {
var collectionViewDataSource : UICollectionViewDataSource!
var collectionViewDelegate : UICollectionViewDelegate!
// define the smaller collection view that is in the cell
#IBOutlet var innerCollectionView: UICollectionView!
// set delegate and datasource for new collectionView
func initializeCollectionViewWithDataSource<D: UICollectionViewDataSource, E: UICollectionViewDelegate>(_ dataSource: D, delegate :E, forItem item: Int) {
self.collectionViewDataSource = dataSource
self.collectionViewDelegate = delegate
innerCollectionView.delegate = self.collectionViewDelegate
innerCollectionView.dataSource = self.collectionViewDataSource
innerCollectionView.tag = item
innerCollectionView.reloadData()
innerCollectionView.layoutIfNeeded()
}
It displays fine when it is first loaded:
But when i select a cell it prints as i have asked:
cell no: 0 of collection view: 2
And then i get a fatal error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
In my ViewController class on this line:
outerCollectionView.dataSource = outerDataProvider
because outerCollectionView is nil. But i can not figure out why it is nil. Im not sure why its going through viewDidLoad again as this loaded when the ViewController now won't load again until i leave the VC and come back?
----- EDIT ------
I have a UICollectionView and want to be able to perform custom behaviour when the user scrolls through implementing the scrollView delegate methods. Is it possible to have two separate objects that act as the collectionView delegate and scrollView delegate when working with a collectionView?
You cannot have separate delegates. UICollectionView is a subclass of UIScrollView, and overrides its delegate property to change its type to UICollectionViewDelegate (which is a subtype of UIScrollViewDelegate). So you can only assign one delegate to a collection view, and it may implement any combination of UICollectionViewDelegate methods and UIScrollViewDelegate methods.
However, you can forward the UIScrollViewDelegate methods to another object without much difficulty. Here's how you'd do it in Swift; it would be very similar in Objective-C (since this is all done using the Objective-C runtime):
import UIKit
import ObjectiveC
class ViewController: UICollectionViewController {
let scrollViewDelegate = MyScrollViewDelegate()
override func respondsToSelector(aSelector: Selector) -> Bool {
if protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, false, true).types != nil || protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, true, true).types != nil {
return scrollViewDelegate.respondsToSelector(aSelector)
} else {
return super.respondsToSelector(aSelector)
}
}
override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? {
if protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, false, true).types != nil || protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, true, true).types != nil {
return scrollViewDelegate
} else {
return nil
}
}
Note that MyScrollViewDelegate probably has to be a subclass of NSObject for this to work.
If I understand you correctly, then you just need your view controller to subclass UICollectionViewController or UICollectionViewDelegate. Then you can access the scrollView delegate methods since they are inherited by the collectionView
Create subclass of UICollectionViewController and write scroll view delegates into it.
class CustomCollectionViewController: UICollectionViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
}
In your target class
class MyCollectionViewController: CustomCollectionViewController, UICollectionViewDelegateFlowLayout {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}