I have collection view where you can select 4 buttons, it is like a quiz with A, B, C, D. I need to store which one they clicked before they go to the next question (They will swipe to go to the next question since it is a collection view) The controller looks like this:
First: Essentially the code used to display the image above, I have created a database which is parsed using this:
struct Question {
let fact: String
let question: String
let answers: [String: String]
let correctAnswer: String
let revenue: String
init?(with dictionary: [String: Any]) {
guard let fact = dictionary["fact"] as? String,
let question = dictionary["question"] as? String,
let answerA = dictionary["answer_a"] as? String,
let answerB = dictionary["answer_b"] as? String,
let answerC = dictionary["answer_c"] as? String,
let answerD = dictionary["answer_d"] as? String,
let revenue = dictionary["revenue"] as? String,
let correctAnswer = dictionary["correctAnswer"] as? String else { return nil }
self.fact = fact
self.question = question
self.revenue = revenue
var answersDict = [String: String]()
answersDict["answer_a"] = answerA
answersDict["answer_b"] = answerB
answersDict["answer_c"] = answerC
answersDict["answer_d"] = answerD
self.answers = answersDict
self.correctAnswer = correctAnswer
}
Second: Then I display using this code:
extension QuestionCell {
func configure(with model: Question) {
factLabel.text = model.fact
questionLabel.text = model.question
revenueLabel.text = model.revenue
let views = answersStack.arrangedSubviews
for view in views {
view.removeFromSuperview()
}
for (id, answer) in model.answers {
print(index)
print(id)
let answerLabel = UILabel()
answerLabel.text = answer
answersStack.addArrangedSubview(answerLabel)
let answerButton = UIButton()
let imageNormal = UIImage(named: "circle_empty")
answerButton.setImage(imageNormal, for: .normal)
let imageSelected = UIImage(named: "circle_filled")
answerButton.setImage(imageSelected, for: .selected)
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
answersStack.addArrangedSubview(answerButton)
}
}
}
Is there a way to store the button I clicked? Thanks
Well there is one sure shot way out of this situation.
Make a custom cell for the collectionView.
Add button outlets and action the the customCell's class
Create and make use of delegate methods in customCell's class and when implementing the customCell in your ViewController set the delegate to
self.
Trigger the delegate methods when button actions are done (inside your custom cell).
Provide your customCell the current indexpath when using it in cellForItemAt method.
Make use of that indexPath to decide which button was triggered.
You should be thinking something close to this approach for a robust solution.
The way I've handled this in the past is to use the tag on a UIButton and just keep track of which tag is currently selected. With this approach I can use the same IBAction for every button and all I need to do is pull the tag from the sender in the function body. While maybe not as flexible and robust is an approach using subclassing, it's a bit quicker to implement.
First set your tags when you create your buttons (I use 100-104 to avoid conflicts with other buttons). Since you're creating your buttons in a CollectionView, you'll need to set the tag in your configure() function:
func configure(with model: Question) {
...
for (id, answer) in model.answers {
...
answerButton.setImage(imageSelected, for: .selected)
answerButton.tag = index
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
}
}
Create an instance variable:
var selectedAnswerIndex = -1
Then assign this IBAction to each of you buttons:
func answerPressed(_ sender: UIButton){
selectedAnswerIndex = sender.tag
hilightNewOne(sender: sender)
}
Related
I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}
I am writing swift code with the goal of displaying a increasing number on every tableview cell. Right now the int is not being display. So the first tableview cell should say 1 and the 2nd should say 2. You can see in the gif below what is going along with the tableview cell and nothing is appearing in them when the button is clicked. The func below is when the button is clicked.
var pageNumber = 1
var itemName : [Player] = []
func enterData() {
theScores.reloadData()
let appDeldeaget = UIApplication.shared.delegate as! AppDelegate
let context = appDeldeaget.persistentContainer.viewContext
// Simpler way to create a new Core Data object
let theTitle = Player(context: context)
// Simpler way to set the position attribute
theTitle.positon = Int64(pageNumber)
print(pageNumber)
// pageNumber must be of type Int64, otherwise use Int64(pageNumber)
do {
try context.save()
itemName.append(theTitle)
pageNumber += 1
} catch {
// handle errors
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let title = itemName[indexPath.row]
let cell = theScores.dequeueReusableCell(withIdentifier: "MyCell", for : indexPath)
cell.selectionStyle = .default
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
cell.textLabel?.text = "\(text)"
cell.textLabel?.textAlignment = .center
cell.layoutMargins = UIEdgeInsets.zero
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
return cell
}
Here's why it doesn't work. You have this:
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
This is a really complicated way to try and do this, and it doesn't work as written. The problem is that the value of position is an Int64 and you need a string. But using as? like that doesn't turn it into a string. When that line of code runs, Swift says, can I just make this into a string? But it can't. So the as? String is nil, and your table cells don't include the number because the conversion failed.
A better way would be something like
if let position = title.value(forKey: "positon") {
cell.textLabel?.text = "Item : \(positon))"
}
But that's only if you really want to use value(forKey:) for some reason. You probably don't need that because normally Xcode creates a subclass of NSManagedObject for each entity with named properties. So even better would be
cell.textLabel?.text = "Item: \(title.position)"
These both work because string interpolation knows how to convert an integer to a string.
You probably should call .reloadData() after context.save()
I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}
I would like an NSPopUpButton to display a different title than the title of the menu item that is selected.
Suppose I have an NSPopUpButton that lets the user pick a list of currencies, how can I have the collapsed/closed button show only the currencies abbreviation instead of the menu title of the selected currency (which is the full name of the currency)?
I imagine I can override draw in a subclass (of NSPopUpButtonCell) and draw the entire button myself, but I would prefer a more lightweight approach for now that reuses the system's appearance.
The menu items have the necessary information about the abbreviations, so that's not part of the question.
Subclass NSPopUpButtonCell, override drawTitle(_:withFrame:in:) and call super with the title you want.
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
var attributedTitle = title
if let popUpButton = self.controlView as? NSPopUpButton {
if let object = popUpButton.selectedItem?.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
attributedTitle = NSAttributedString(string:shortTitle, attributes:title.attributes(at:0, effectiveRange:nil))
}
}
}
return super.drawTitle(attributedTitle, withFrame:frame, in:controlView)
}
In the same way you can override intrinsicContentSize in a subclass of NSPopUpButton. Replace the menu, call super and put the original menu back.
override var intrinsicContentSize: NSSize {
if let popUpButtonCell = self.cell {
if let orgMenu = popUpButtonCell.menu {
let menu = NSMenu(title: "")
for item in orgMenu.items {
if let object = item.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
menu.addItem(withTitle: shortTitle, action: nil, keyEquivalent: "")
}
}
}
popUpButtonCell.menu = menu
let size = super.intrinsicContentSize
popUpButtonCell.menu = orgMenu
return size
}
}
return super.intrinsicContentSize
}
Ok, so I found out how I can modify the title. I create a cell subclass where I override setting the title based on the selected item. In my case I check the represented object as discussed in the question.
final class MyPopUpButtonCell: NSPopUpButtonCell {
override var title: String! {
get {
guard let selectedCurrency = selectedItem?.representedObject as? ISO4217.Currency else {
return selectedItem?.title ?? ""
}
return selectedCurrency.rawValue
}
set {}
}
}
Then in my button subclass I set the cell (I use xibs)
override func awakeFromNib() {
guard let oldCell = cell as? NSPopUpButtonCell else { return }
let newCell = MyPopUpButtonCell()
newCell.menu = oldCell.menu
newCell.bezelStyle = oldCell.bezelStyle
newCell.controlSize = oldCell.controlSize
newCell.autoenablesItems = oldCell.autoenablesItems
newCell.font = oldCell.font
cell = newCell
}
A downside though is that I have to copy over all attributes of the cell I configured in Interface Builder. I can of course just set the cell class in Interface Builder, which makes this superfluous.
One thing I haven't figured out yet is how I can have the button have the correct intrinsic content size now. It still tries to be as wide as the longest regular title.
And the second thing I haven't figured out is how to make this work with bindings. If the buttons content is provided via Cocoa Bindings then I can only bind the contentValues, and the cell's title property is never called.
I have a collection view where it needs to store which button is pressed. This is like a quiz where they have options A, B, C, D. The problem is that it will select the item with this code:
extension QuestionCell {
func configure(with model: Question) {
factLabel.text = model.fact
questionLabel.text = model.question
revenueLabel.text = model.revenue
let views = answersStack.arrangedSubviews
for view in views {
view.removeFromSuperview()
}
for (id, answer) in model.answers {
print(index)
let answerLabel = UILabel()
answerLabel.text = answer
answersStack.addArrangedSubview(answerLabel)
let answerButton = UIButton()
answerButton.tag = index
let imageNormal = UIImage(named: "circle_empty")
answerButton.setImage(imageNormal, for: .normal)
let imageSelected = UIImage(named: "circle_filled")
answerButton.setImage(imageSelected, for: .selected)
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
answersStack.addArrangedSubview(answerButton)
}
}
But then if you swipe a couple times and then come back to this question the image will be white again. How can I fix this problem. Also I attached how the screen looks.
This is how it looks like
~ Thanks
You could solve this with an Answer class like this. In case you're new to classes, I should point out that you won't want to write this code inside another class definition (like your ViewController class.)
class Answer{
var wasSelected = false
var text : String
init(text : String) {
self.text = text
}
}
class ButtonWithAnswer : UIButton{
var answer : Answer?
}
But now your model.answers will look like this, and will be of type [String : Answer]:
model.answers = ["id1" : Answer(text : "answer 1"), "id2" : Answer(text: "answer 2"), "id3":Answer(text: "answer 3")]
The idea is that instead of having model.answers contain strings, they contain a new custom type that we made up called Answer that can keep track of the text for the answer (which is a property called text of type String) as well as whether or not it was selected (which is a property called wasSelected of type Bool). Your for loop will now look like this:
for (id, answer) in model.answers {
let answerLabel = UILabel()
answerLabel.text = answer.text //this comes from our new class definition
answersStack.addArrangedSubview(answerLabel)
//Change this to our new custom button subclass
let answerButton = ButtonWithAnswer()
//this line straight up won't work since you got rid of index
//answerButton.tag = index
//use this instead
answerButton.answer = answer
let imageNormal = UIImage(named: "circle_empty")
answerButton.setImage(imageNormal, for: .normal)
let imageSelected = UIImage(named: "circle_filled")
answerButton.setImage(imageSelected, for: .selected)
//make it so that any answers that were previously selected are now selected when the view reloads
answerButton.isSelected = answer.wasSelected
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
answersStack.addArrangedSubview(answerButton)
}
There's one last piece of this. We need to make sure that we make our answer.wasSelected equal to true when we tap the button that corresponds with that answer. So in answerPressed we should do something like this.
func answerPressed(_ sender : UIButton){
let answerForButton = (sender as! ButtonWithAnswer).answer!
answerForButton.wasSelected = true
}
That should about cover it. If this looks like something totally new then you should look up tutorials on classes, objects, and init methods in Swift before you dive in or ask a programmer for help in person. But I explained it about as well as I can without actually being there in person to give you a complete walkthrough.