Accessing a UITableViewController delegate from a TableView header - swift

I'm having an issue calling a segue from a TableView header that is associated with a programatically created TableView. As a stop-gap I have saved an object reference to the main UITableViewController using a Singleton but this is not the ideal solution.
My TableViewController has multiple sections with either 1 or 2 rows within each section depending on whether the top-level section row is selected. The second row is effectively used to expand on the content that is displayed on the selected row.
The second row contains a custom cell that contains a slider menu, and depending on the menu item selected, 1 of 5 subviews are displayed in the container view.
One of these subviews contains a programatically generated TableView with its own custom header and a custom cell. This cell contains a header and an embedded button. When the button is pressed in the header, I want to segue to another navigation controller, however, my problem is I cannot correctly initialise a delegate to access the main TableViewController.
It gets a bit more complex, as the custom cell creates its own TableView and handles its own functions for operations that are normally performed on the main controller using overrides e.g. didSelectRowAt, numberOfRowsInSelection, headerForRowAt, etc.
class pendingNotificationCustomCell: UITableViewCell, UITableViewDataSource, UITableViewDelegate {
var dataSample:[String] = ["Row 1,"Row 2", "Row 3"]
var PendingTableView: UITableView?
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
print("##### awakeFromNib")
super.awakeFromNib()
// Initialization code
setUpTable()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style , reuseIdentifier: reuseIdentifier)
setUpTable()
}
func setUpTable() {
PendingTableView = UITableView(frame: CGRect.init(), style:UITableView.Style.plain)
PendingTableView?.delegate = self
PendingTableView?.dataSource = self
PendingTableView?.register(MyCell.self, forCellReuseIdentifier: "pendingnotificationsCellID")
PendingTableView?.register(PendingNotificationsHeader.self, forHeaderFooterViewReuseIdentifier: "pendingHeaderID")
PendingTableView?.sectionHeaderHeight = 40
PendingTableView?.allowsSelection = true
PendingTableView?.allowsMultipleSelection = false
self.addSubview(PendingTableView!)
}
I can easily setup protocols and associated delegates to access functions on the controller that manages the main TableView custom cells. However, as the problematic custom cell does not inherit functions relating to performing segues I need to access the function on the main controller.
I've experimented quite a bit with this and haven't been able to come up with a viable solution other than the Singleton hack.
let pendingCell = pendingNotificationCustomCell()
pendingCell.delegate4 = mStats.mainController
When I try assigning delegate4 with an initialised outlet that references the main TableViewController it always has a value of 'nil' when it gets there. Even when I assign it with the Singleton value in the class the generates the second
TableView. The commented out line fails whereas calling the method using the mStats Singleton works fine.
//delegate4.callSegueFromCell2(myData: myData)
mStats.mainController?.callSegueFromCell2(myData: myData)
The delegate4 above, which is commented out, is set in the cell header classes as follows:
protocol MyCustomCellDelegator3 {
func callSegueFromCell2(myData dataobject: AnyObject)
}
class PendingNotificationsHeader: UITableViewHeaderFooterView {
var delegate4:MyCustomCellDelegator3!
var MainTableViewController: MainTableViewController?
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let nameLabel: UILabel = {
let label = UILabel()
label.text = "Pending"
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = .white
return label
}()
let priceAlertButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add new", for: .normal)
button.titleLabel?.font = UIFont (name: "Helvetica", size: 15)
button.setTitleColor(.white, for: .normal)
button.addTarget(self,action: #selector(createNewPriceAlertButtonPressed), for: .touchUpInside)
return button
}()
#IBAction func createNewPriceAlertButtonPressed(_ sender: Any) {
print ("##### New Price Alert Label Pressed")
// Get the view
var mydata = self
//delegate4.callSegueFromCell2(myData: myData)
mStats.mainController?.callSegueFromCell2(myData: myData)
}
Appreciate any guidance.

let pendingCell = pendingNotificationCustomCell()
looks odd.
You should set up your cells in tableView(cellForRow:at:) UITableViewDataSource method.

The issue related to setting the delegate on the cell and not passing it on to the section header. I ended up solving the issue as follows:
I implemented a function and delegate in the MainViewController called API_Segue_01
final class MainTableViewController: UITableViewController, API_Segue_01 {
func API_Segue_01(myData dataobject: AnyObject) {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
self.performSegue(withIdentifier: "showCreateNotificationsViewController", sender:dataobject )
}
/// Rest of class code
/// ...
In the custom cell I created a header prototype to reference the external function:
protocol API_Segue_01 {
func API_Segue_01(myData dataobject: AnyObject)
}
I defined a var to hold the reference to the MainTableViewController in the custom cell and associated header
class pendingNotificationCustomCell: UITableViewCell, UITableViewDataSource, UITableViewDelegate {
var funcCallTo: API_Segue_01!
/// Rest of class code
/// ...
class PendingNotificationsHeader: UITableViewHeaderFooterView {
var funcCallTo:API_Segue_01!
/// Rest of class code
/// ...
When creating an instance of the custom cell I provided a reference to the MainTableViewController
pendingCell.funcCallTo = mainTableViewController
As the action button is located in the cell header I passed the reference to the mainTableViewController within the viewForHeaderInSection as follows
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
print("##### viewForHeaderInSection")
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "pendingHeaderID") as! PendingNotificationsHeader
header.funcCallTo = self.funcCallTo
return header
}
I was then able to use this reference to call the API_Segue_01 function
#IBAction func createNewPriceAlertButtonPressed(_ sender: Any) {
print ("##### New Price Alert Label Pressed")
// Get the view
var mydata = self /
funcCallTo.API_Segue_01(myData: mydata)
}

Related

How to make delegate between a cell with a UIView and a view controller?

I have a view controller which holds a tableview. Inside that tableview I have a dynamic cell. Inside of that cell I have a UIView. Inside the UIView I have a label with a tap recognizer which is supposed to perform a segue to a different view controller. I have tried making a delegate between the view controller by putting vc.delegate = self in the viewdidload, but it did not work. I have also tried putting the same line inside cellforrowat, but it still doesn't do anything. How can I make a delegate that communicates from my cell with UIView to this view controller? Also, the thing that will be communicating is my UIView class and not my cell class.
If I understand it correctly. Create a delegate protocol for your cell view, put delegate property to the cell and pass it via the tableView method tableView(_:cellForRowAt:) to the controller.
Protocol implementation:
protocol MyCellViewDelegate: class {
func viewDidTap(_ view: MyCellView)
}
Cell View Implementaiton:
class MyCellView: UIView {
private let tapGesture = UITapGestureRecognizer()
weak var delegate: MyCellViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
tapGesture.addTarget(self, action: #selector (viewDidTap))
}
#objc
func viewDidTap() {
delegate?.viewDidTap(self)
}
}
Cell Implementation:
Then, in your Cell implementation. Pass the delegate reference to the MyCellView, which will be handled in the Controller.
class MyCell: UITableViewCell {
private let myContentView = MyCellView()
weak var delegate: MyCellViewDelegate? {
didSet { myContentView.delegate = delegate }
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
}
Then you should be able to set delegate in the TableView DataSource delegate methods.
Controller Implementation:
extension MyController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue the cell...
cell.delegate = self
return cell
}
}
And it will force you to add delegates.
extension MyController: MyCellViewDelegate {
func viewDidTap(_ view: MyCellView) {
// Do some stuff
}
}
However, TableView could "steal" your Tap Gesture action. Maybe, if you set tableView.isUserInteractionEnabled = false, it could work.
The easy way is NotificationCenter. Just post the notification and receive at ViewController.
Though, you achieve this as well by using the delegate where you need to pass the information as below. This is very high level where I am assuming you have below view as a separate class.
UIView -> [Cell -> TableView] -> UIViewController
You can use the completion handler as well.

How to activate share extension (UIActivityViewController) when a button tapped (which is in UICollectionViewController)?

I'm trying to add some buttons or labels to one of UICollectionViewCell, but it seems I'm not be able to activate UIActivityViewController when the share button tapped.
Here are my codes:
class VerticalCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
containerView.addSubview(shareButton)
shareButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16).isActive = true
shareButton.rightAnchor.constraint(equalTo: favoriteButton.leftAnchor, constant: -16).isActive = true
shareButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
shareButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
shareButton.addTarget(self, action: #selector(handleShareButton), for: .touchUpInside)
}
#objc func handleShareButton(sender: UIButton) {
let shareText = NSLocalizedString("I found something interesting you may want to take a look at.", comment: "share")
let shareImage = #imageLiteral(resourceName: "Bear")
let activityViewController : UIActivityViewController = UIActivityViewController(activityItems: [shareText, shareImage as Any], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
self.present(activityViewController, animated: true, completion: nil)
}
}
The errors are:
Value of type 'VerticalCollectionViewCell' has no member 'view'
and
Value of type 'VerticalCollectionViewCell' has no member 'present'.
I know presentViewController is a UIViewController method, UITableViewCell does not has a method called presentViewController, and UITableViewCell should never handle any business logic. It should be implemented in a view controller.
But I have no idea how to fix this.
you call do in this way
class Cell: UITableViewCell {
#IBOutlet let shareButton: UIButton!
}
class VC: UIViewController, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? Cell {
cell?.shareButton.addTarget(self, action: #selector(VC.shareButtonPressed(_:)), for: .touchUpInside)
}
}
func shareButtonPressed(_ button: UIButton) {
//code here
}
}
What you can do, is define a protocol of the cell, which your tableview can conform to. When the share button on a collectionView is tapped, call this delegate method, passing in relevant information like the cell tag or the model for that collectionView cell. Using that information you can then present the ActivityController from your viewController.

NSCollectionView crashes when creating items

My NSCollectionView crashes when calling makeItem(withIdentifier identifier: String, for indexPath: IndexPath). numberOfItemsInSection returns the correct value. If I call makeItem... in viewDidLoad rather than in itemForRepresentedObject I see an error indicating that the indexPath is out of bounds. How can this be?
The collection view loads like this:
class TagCollectionViewController: NSViewController, NSCollectionViewDataSource {
fileprivate static let itemIdentifier = "TagItem"
#IBOutlet var collectionView: NSCollectionView!
fileprivate var tags = List<Tag>.init()
override func viewDidLoad() {
super.viewDidLoad()
let nib = NSNib(nibNamed: "TagCollectionViewItem", bundle: nil)
collectionView.register(nib, forItemWithIdentifier: TagCollectionViewController.itemIdentifier)
collectionView.dataSource = self
}
(The List collection is a Realm class)
During viewWillAppear() the tags collection is populated from a ReSwift state:
override func viewWillAppear() {
for image in mainStore.state.selectedImages {
for tag in image.tags {
tags.append(tag)
}
}
super.viewWillAppear()
}
Solved it.
When I created the .xib for the item I added an NSCollectionViewItem object but didn't wire up the view to my custom view.
To recap, for anyone who gets caught out by this, the steps to creating a NSCollectionViewItem are:
Create the nib and configure your views
Add an NSCollectionViewItem object to your nib
Wire up your view (and any other views) to the object
Register the nib with the collectionView in your view controller

Proper style in Swift: Guard Statement

I am getting my feet wet with Swift and I am wondering what proper style for guards is. Am I using the guard statement correctly? It feels a little clunky, but I prefer it to an if let statement. Or could this be simplified?
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView?
...
required init() {
super.init(nibName:nil, bundle:nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// MARK: - User Interface
func setupUI() {
...
// tableView
tableView = UITableView()
guard let tableView = tableView else {
Log.msg("tableView could not be initialized")
return
}
tableView.delegate = self
tableView.dataSource = self
tableView.separatorInset = UIEdgeInsetsZero
tableView.layoutMargins = UIEdgeInsetsZero
tableView.accessibilityIdentifier = "tableView"
tableView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(tableView)
...
}
}
Log is a custom logging class that has optimized console output.
Although I think this question should be moved to the codereview site, I'll mention this:
You are using the guard statement correctly from a syntactic point of view. It's a way to check something and return early if the condition isn't met. Your overall implementation needs a bit of refactoring though so you don't need the guard at all.
When creating a view controller, do this instead:
class ViewController: UIViewController {
private var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = {
let tableView = UITableView()
tableView.property = ...
return tableView
}()
view.addSubview(tableView)
// setup constraints on tableView...
}
}
By using an implicitly unwrapped optional for your tableView property you forego the need for a guard and testing the tableView for nil wherever you need to use it, but you also get the benefit of deferred initialization (in viewDidLoad()).

drawRect in UIView class not called when custom UICollectionViewCell is selected in swift

I have a UIView subclassed on a custom collectionView cell. It displays fine but in the "didSelect" delegate method in my ViewController I set a property for the UIView which refreshes with a setNeedsDisplay and drawRect isn't being fired. Does it have anything to do with the dequeueReusableCell? How would that be coded? This is in swift 1.2 not obj-c.
class setSelection: UIView {
private var _checked: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setNeedsDisplay()
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
. . . code . . .
}
var checked: Bool {
get {
return _checked
}
set {
_checked = newValue
self.setNeedsDisplay()
println(_checked)
}
}
This is the UIView class. the checked property is being set in the collectionView didSelectCell. The setNeedsDisplay in the property doesn't fail but drawRect will never be called again.
** UPDATE ** The init functions are not required and should be removed.
SOLVED!! I knew it had something to do with cell reference. In researching the problem i realized that in collectionView didSelectItemAtIndexPath and didDeselectItemAtIndexPath I was referencing the reusable cell which is only valid for cells in view and are only updated in cellForItemAtIndexPath:
let selectedCell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as! ImageDataCell
which would select the appropriate cell but do nothing with it. Instead I needed to reference the actual cell:
let selectedCell = collectionView.cellForItemAtIndexPath(indexPath) as! ImageDataCell
now the view is refreshed while displayed when this method is executed. You still need to set the cell in cellForItemAtIndexPath because that refreshes cells that move back into frame but doesn't refresh a cell being selected or deselected.