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?
Related
I have a table on which a cell is made using .xib. There are two views on this cell. When I click on each view, and I set the image and title for another viewController. I made a protocol for this, but I just can’t get this data on another viewController because my delegate = nil.
In this class, I set properties.
сlass DataTableViewCell: UITableViewCell {
var dataDelegete: DataAccountCellDelegete? = nil
#objc func accountViewTaped() {
dataDelegete?.navigationBarIcon(image: UIImage(named: "icon") ?? UIImage())
dataDelegete?.navigationBarTitle(title: "title")
}
#objc func settingsViewTaped() {
dataDelegete?.navigationBarIcon(image: UIImage(named: "icon") ?? UIImage())
dataDelegete?.navigationBarTitle(title: "title")
}
}
protocol DataAccountCellDelegete {
func navigationBarIcon(image: UIImage)
func navigationBarTitle(title: String)
}
In this class, I get and want to set these properties, but my delegate = nil
class AccountViewController: UIViewController {
var dataVC = DataTableViewCell()
override func viewDidLoad() {
super.viewDidLoad()
dataVC.delegete = self
}
}
extension AccountViewController: DataAccountCellDelegete{
func menuNavigationBarIcon(image: UIImage) {
menuNavigationBar.addImage(image)
}
func menuNavigationBarTitle(title: String) {
menuNavigationBar.addTitle(title)
}
}
How do I declare a delegate correctly?
The view hierarchy should be like
ViewController -> TableView -> TableViewCell
So, ViewController has the reference of TableView & TableView has the reference of the cell. So the data passing should be reversed.
TableViewCell -> TableView -> ViewController
as vadian said ,Cells are reused and being created in cellForRowAt. The variable dataVC is useless here.
I've got a UITableViewController with two custom cells - one contains a UITextField (for the user to input a title) and the other contains a UITextView (for the user to input a description). Whenever these change, I want to update my memory object (which is a struct with two variables - memoryTitle and memoryDescription).
The memoryTitle seems simple enough - on my ViewController I have the following:
#IBAction func memoryTitleChanged(_ sender: UITextField) {
memory.memoryTitle = sender.text ?? ""
}
The UITextView has confused me slightly though. There's two issues I'm having - I can't create an action in the same way I can for the UITextField, so my next thought was to make the ViewController the delegate and use textViewDidChange to update memory.memoryDescription but that brings me to my second problem.
In order to make the UITextView cell resize dynamically, I used the following tutorial which works perfectly (https://medium.com/#georgetsifrikas/embedding-uitextview-inside-uitableviewcell-9a28794daf01) to make my custom cell this:
class DetailTextTableViewCell: UITableViewCell, UITextViewDelegate {
//Found below method for resizing UITextView and cell - https://medium.com/#georgetsifrikas/embedding-uitextview-inside-uitableviewcell-9a28794daf01
#IBOutlet weak var memoryDescriptionTextView: UITextView!
var textChanged: ((String) -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
memoryDescriptionTextView.delegate = self
memoryDescriptionTextView.backgroundColor = UIColor.red
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
//Found below method for resizing UITextView and cell - https://medium.com/#georgetsifrikas/embedding-uitextview-inside-uitableviewcell-9a28794daf01
func textChanged(action: #escaping (String) -> Void) {
self.textChanged = action
}
func textViewDidChange(_ textView: UITextView) {
textChanged?(textView.text)
}
}
Now I'm stuck with DetailTextTableViewCell being the UITextView's delegate, so I'm not sure how to make it update my memory object in the ViewController when the text changes. If anyone has any ideas or guidance it'd be much appreciated!
Thank you in advance.
First, you don't need textChanged method
func textChanged(action: #escaping (String) -> Void) {
Then, what you need, is assigning your textChanged closure variable (which is good approach btw) in controller's cellForRowAt for each certain cell.
Inside closure declare, that when text view did change, certain item's (from table view data source array) property will be assigned with String parameter of closure and if you need, then reload certain cell for this IndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.textChanged = { text in
self.dataSourceArray[indexPath.row].stringProperty = text
// tableView.reloadRows(at: [indexPath], with: .none)
// if you want to reload row, move calling closure
// to `textViewDidEndEditing` instead
}
...
}
declare this protocol above your cell DetailTextTableViewCell
protocol CellDelegate {
func textViewChanged(textView : UITextView)
}
add a delegate var in your DetailTextTableViewCell
var delegate : CellDelegate?
In the cell for row of your tableView assign self to delegate property of cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.delegate = self
}
In your DetailTextTableViewCell add this line inside textViewDidChange
func textViewDidChange(_ textView: UITextView) {
textChanged?(textView.text)
delegate?.textViewChanged(textView)
}
Now implement the delegate function in your view controller
func textViewChanged(textView: UITextView) {
}
Inside cellForRowAt do
let cell = ///
cell.memoryDescriptionTextView.tag = indexPath.row
cell.memoryDescriptionTextView.delegate = self
and implement the delegate methods inside the vc
Try to enable user interaction property of text view inside cellForRowAt method.
cell.memoryDescriptionTextView.delegate = self
cell.memoryDescriptionTextView.isUserInteractionEnabled = true
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 table view with a custom cell build in it, the custom cell has two labels and one textfield. When the user edits the textfield then the label2 changes in the current cell. But I want that the value(Label2) in the second cell will also change based on the value in the current cell. Example: Value in the first cell is x, the Value in the second cell should be 2+x. How should I realize that? In my view controller I am using the following code:
var myString = [String]()
#IBOutlet weak var TableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
myString = ["a", "b"]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myString.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CalculationsTableViewCell
tableView.tableFooterView = UIView(frame: CGRectZero) //hide empty cells in UITableView
cell.Label?.text = myString[indexPath.row]
return cell
}
My TableViewCell:
#IBOutlet weak var Label: UILabel!
#IBOutlet weak var Label2: UILabel!
#IBOutlet weak var TextField: UITextField!
#IBAction func TextFieldEditingChanged(sender: AnyObject) {
var value: Double
value = NSString(string: TextField.text!).doubleValue
Label2.text = "\(value)"
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
Firstly you should / could make use of the delegate pattern to communicate from the cell back to your controller:
create a protocol with a method named e.g. userEnteredNumber
let your view controller conform to that protocol
create a field in the TableViewCell called delegate - its type should the previously created protocol
when setting up the cell in cellForRowAtIndexPath assign self to the delegate property of the cell
as soon as the user enters a value call the delegate method on the previously set delegate
Secondly you have to somehow remember the state of your application. The thing with tableviews is that the iOS can decide to reload the contents of the tableView at any point (theoretically) and your controller has to keep track of the exact content each cell should have.
I do not know what the exact requirements are for your app but you should probably assign the value that gets transmitted via the protocol method to some variable in your viewController. Then you trigger a tableView reloadData and finally you have to alter the cellForRowAtIndexPath method to set the desired properties of some of the cells based on the stored value.
i edited my question , because set textfield maybe can't be simple, need references so this is my code, but still have issue :
this code for TableViewController :
import UIKit
protocol PaymentSelectionDelegate{
func userDidSelectPayment(title: NSString?)
}
class PopPaymentTableViewController: UITableViewController {
var delegate : PaymentSelectionDelegate!
override func viewDidLoad() {
super.viewDidLoad()
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath?) {
self.dismissViewControllerAnimated(true){
if self.delegate != nil{
if let ip = indexPath{
var payItem : PayMethod
payItem = self.myList[ip.row] as! PayMethod
var title = payItem.paymentName
self.delegate.userDidSelectPayment(title)
}
}
}
}
}
and for code TransactionViewController :
import UIKit
class TransactionViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, PaymentSelectionDelegate, UITextFieldDelegate, UIPopoverPresentationControllerDelegate{
#IBOutlet var paymentTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
func resign(){
paymentTextField.resignFirstResponder()
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
if (textField == paymentTextField){
resign()
performSegueWithIdentifier("seguePayment", sender: self)
}
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
func userDidSelectPayment(title: NSString?) {
paymentTextField.text = title as! String
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "seguePayment"{
var VC = segue.destinationViewController as! UINavigationController
var controller = VC.popoverPresentationController
if controller != nil{
controller?.delegate = self
var paymentVC = PopPaymentTableViewController()
paymentVC.delegate = self
}
}
}
}
this issue is: variable delegate in TableViewController like seems always nil, so cant set value
NB : sorry i edited totally my question, because so many answer say cant be set textfield just like that
The context of where your TransactionViewController is, is not completely clear, but you are instantiating a new ViewController. If you want to refer to an existing ViewController, this is not the instance you are using here. If you want to create a new one and show it after your didSelectRowAtIndexPath, you have to make sure, that the TextField is instantiated in the init-Method of your ViewController. As you are not instantiating it from a Storyboard, it seems that your TransactionViewController is created programmatically. Probably you are setting the TextField only in viewDidLoad or something else after the init().
You are trying to set text to paymentTextField which is still no initialized.
For this you have to set text to paymentTextField in viewDidLoad method in TransactionViewController.
remove transVC.paymentTextField.text = title from didSelectRowAtIndexPath
Add it in TransactionViewController's viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.paymentTextField.text = self.payName
}
It is not correct way to do that. You have not initialised text field and trying to set it's text. First initialise text field:
transVC.paymentTextField = UITextField()
Then try to do something.