using swift, I'm trying to create a dynamic and generic control to reuse it, basically, the control should have a general behavior inside.
To be more specific, I have a UIScrollView and it's filled using UIViews, when you click over an UIView, the background should change.
That is working correctly.
But, for the implementation, my class of the generic control, accepts a Selector as parameter.
Both works separately, but together are not working.
The specific part of codes are:
Generic class
let clickAgendaEvent = UITapGestureRecognizer(target: self, action: #selector (self.agendaClicked (_:)))
cellSubView.addGestureRecognizer(clickAgendaEvent)
cell.addSubview(cellSubView)
let itemClickedEvent = UITapGestureRecognizer(target: viewController.self, action: self.agendaItemClicked! )
cell.addGestureRecognizer(itemClickedEvent)
And a ViewController with an implementation like this:
#objc func eventDailyAgenda(sender:UIView!){
print("Item clicked!")
}
As you can see, the second event, is not inside of the generic class, the second event is a separated implementation if the ViewController.
But, the generic class, would be implemented for other UIViewController.
Someone have an idea about how can handle it?
Look into UIGestureRecognizerDelegate to handle both gestures simultaneously. Check out the callback gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) . Return true to have both handled simultaneously.
The easier way to do it, is implementing UIGestureRecognizerDelegate.
But for my specific case didn't work, because, I have different classes implementing the behavior.
But, I found a way to do it.
And is implementing NSObject (if you are in a UIViewController you don't have to implement it.
For my case I have my class:
class MyClass:NSObject {
func createAll(){
let clickAgendaEvent = UITapGestureRecognizer(target: self, action: #selector (self.agendaClicked (_:)))
cellSubView.addGestureRecognizer(clickAgendaEvent)
cellSubView.tag = index
cell.addSubview(cellSubView)
}
#objc func agendaClicked(_ sender:AnyObject) {
baseView.setTransparentToSubViews()
print("Here")
let tap = sender as! UITapGestureRecognizer
tap.view?.backgroundColor = UIColor().hexStringToUIColor(hex: "#e0e0e0")
if let v = tap.view {
// use button
print("The tag is \(v.tag)")
}
if let c: NSObject.Type = NSClassFromString(viewController.className) as? NSObject.Type{
let c_tmp = c.init()
c_tmp.perform(Selector(("test")))
c.perform(Selector(("static_test")))
}
}
}
Add in your ViewController
#objc public func test(){
print("This is Test!!!")
}
#objc public class func static_test(){
print("This is Static Test")
}
And this extension:
import Foundation
import UIKit
extension UIViewController {
var className: String {
return NSStringFromClass(self.classForCoder)
}
}
Now, you can execute all the methods you want.
For this case, once the user touch an element, the next event is fired from the touch, and not is necessary to add a new delegate.
Maybe is not the better way, but is totally functional.
Related
I'm using BulletinBoard (BLTNBoard) to create dialogs in my iOS app. There's an option to embed image inside it. I would like to extend it's functionality and allow user to manipulate this image using tap gesture. But eventually when I assign a gesture to it's imageView using addGestureRecognizer nothing happens.
Here's how I initiliaze bulletin and add gesture to the image:
class ViewController: UIViewController {
lazy var bulletinManager: BLTNItemManager = {
let rootItem: BLTNPageItem = BLTNPageItem(title: "")
return BLTNItemManager(rootItem: rootItem)
}()
override func viewDidLoad() {
//etc code
let bulletinManager: BLTNItemManager = {
let item = BLTNPageItem(title: "Welcome")
item.descriptionText = "Pleas welcome to my app"
item.actionButtonTitle = "Go"
item.alternativeButtonTitle = "Try to tap here"
item.requiresCloseButton = false
item.isDismissable = false
item.actionHandler = { item in
self.bulletinManager.dismissBulletin()
}
item.alternativeHandler = { item in
//do nothing by now
}
//
item.image = UIImage(named: "welcome")
//adding gesture to its imageView
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
return BLTNItemManager(rootItem: item)
}()
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
and nothing happens at all (no message printed in console).
However if I assign action inside alternative button it works as expected:
item.alternativeHandler = { item in
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
}
I guess the only thing which can prevent me to assign the tap event to it properly is that imageView becomes available much later than the bulletin is created (for example only when it is shown on the screen).
Could you please help and correct my code. Thanks
upd.
Ok, based on Philipp's answer I have the following solution:
class myPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
let imageView=super.imageView
imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: #selector(tapTap))
imageView?.addGestureRecognizer(tap)
return contentViews
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
When you're working with an open source library, it's easy to check out the source code to find the answer.
As you can see here, image setter doesn't initiate the image view.
Both makeContentViews makeArrangedSubviews (which are responsible for views initializing) doesn't have any finish notification callbacks.
Usually in such cases I had to fork the repo and add functionality by myself - then I'll make a pull request if I think this functionality may be needed by someone else.
But luckily for you the BLTNPageItem is marked open, so you can just subclass it. Override makeContentViews and add your logic there, something like this:
class YourOwnPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
// configure the imageView here
return contentViews
}
}
I needed to delegate a click action for my UIView class to my UIViewController class since Swift does not support multiple class inheritance. So i wanted it such that once a button is clicked on my subview, a function in my BrowserViewController class is called.
I am using a protocol to achieve this, but on the function does not triggered when the button is tapped. Please help me out.
View Controller
class BrowseViewController: UIViewController {
var categoryItem: CategoryItem! = CategoryItem() //Category Item
private func setupExplore() {
//assign delegate of category item to controller
self.categoryItem.delegate = self
}
}
// delegate function to be called
extension BrowseViewController: ExploreDelegate {
func categoryClicked(category: ProductCategory) {
print("clicked")
let categoryView = ProductByCategoryView()
categoryView.category = category
categoryView.modalPresentationStyle = .overCurrentContext
self.navigationController?.pushViewController(categoryView, animated: true)
}
}
Explore.swift (subview)
import UIKit
protocol ExploreDelegate: UIViewController {
func categoryClicked(category: ProductCategory)
}
class Explore: UIView {
var delegate: ExploreDelegate?
class CategoryItem: UIView {
var delegate: ExploreDelegate?
var category: ProductCategory? {
didSet {
self.configure()
}
}
var tapped: ((_ category: ProductCategory?) -> Void)?
func configure() {
self.layer.cornerRadius = 6
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.categoryTapped)))
self.layoutIfNeeded()
}
#objc func categoryTapped(_ sender: UIGestureRecognizer) {
delegate?.categoryClicked(category: ProductCategory.everything)
self.tapped?(self.category)
}
}
}
Simply add a print statement inside categoryTapped.
You will then know if it is actually being tapped.
A million things could go wrong, for example, you may have forget to set the UIView to allow intertaction.
After checking that. Next add another print statement inside categoryTapped which shows you whether or not the delegate variable is null.
You'll quickly discover the problem using simple print statements.
print("I got to here!")
It's that easy.
And what about
if delegate == nil { print("it is nil!! oh no!" }
else { print("phew. it is NOT nil.") }
Debugging is really that easy at this level.
Next add a print statement inside setupExplore()
func setupExplore() {
print("setup explore was called")
....
See what happens.
I don't see any piece of code which sets the delegate.
First of all, define delegate as a property inside CategoryItem class, Then you must set the current instance of BrowseViewController to the delegate variable of CategoryItem. Now you can expect your method being called.
There are a few things that could cause the delegate method to not be triggered in this code:
Ensure that isUserInteractionEnabled = true on your CategoryItem. This is probably best done in either the configure() function in the CategoryItem or in the setupExplore() function of the BrowseViewController.
Make sure that the setupExplore() function on the BrowseViewController is being called, and that the category is being set on the CategoryItem to trigger the configure function. Otherwise, either the delegate or the gesture recognizer might not being set.
Side Note - weak vs strong delegate
On a side note, it is usually best practice to make your delegate properties weak var rather that having them be a strong reference, as this makes them prone to strong retain cycles.
Therefore, you might want to consider making the var delegate: ExploreDelegate? on your CategoryItem into weak var delegate: ExploreDelegate?. For more information on this problem, view this post.
Shorter explanation:
You often want to extend on "target" ... and targets are usually Any?. But you can't have an extension on Any. How to do it?
Consider this,
extension UIViewController {
func add(tap v:UIView, _ action:Selector) {
let t = UITapGestureRecognizer(target: self, action: action)
v.addGestureRecognizer(t)
}
}
Excellent, you can now...
self.tap(redButton, #selector(clickedRedButton))
... in any view controller.
But you can do the same thing to just about any target.
So, to use the extension on a UITableViewCell say, you have to also have....
extension UIGestureRecognizerDelegate {
func add(tap v:UIView, _ action:Selector) {
let t = UITapGestureRecognizer(target: self, action: action)
v.addGestureRecognizer(t)
}
}
The target argument of UITapGestureRecognizer is actually Any?
But, you can not do this ...
extension Any {
What's the solution? How to make an extension that will work on the Any?, as for example in the first argument of UITapGestureRecognizer ?
Or as Conner'c comment suggests, is there a way to:
extension UIViewController or UIView {
rather than copying and pasting it twice?
"Any" is adhered to (passively) by every struct/class. An extension to Any would add that functionality to every single type in the language and your code. This isn't currently possible, and I doubt it ever would be (or should be).
Anyway, here are a few ways to solve this problem.
My preference is a protocol extension that adds the functionality:
protocol TapGestureAddable {
func addTapGestureRecognizer(to view: UIView, with action: Selector) -> UITapGestureRecognizer
}
extension TapGestureAddable {
func addTapGestureRecognizer(to view: UIView, with action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: self, action: action)
view.addGestureRecognizer(recognizer)
return recognizer
}
}
extension UIViewController: TapGestureAddable { }
extension UIView: TapGestureAddable { }
This forces you to knowingly choose to add the functionality to a given class, (a good thing IMO) without having to duplicate any meaningful code.
Possibly a better option would be to make this logic an extension of UIView instead:
extension UIView {
func addTapGestureRecognizer(with responder: Any, for action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: responder, action: action)
self.addGestureRecognizer(recognizer)
return recognizer
}
func addTapGestureRecognizer(with action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: self, action: action)
self.addGestureRecognizer(recognizer)
return recognizer
}
}
Otherwise, just make a global function:
func addTapGestureRecognizer(to view: UIView, with responder: Any, for action: Selector) -> UITapGestureRecognizer {
let recognizer = UITapGestureRecognizer(target: responder, action: action)
view.addGestureRecognizer(recognizer)
return recognizer
}
Any isn't a class in the way that NSObject is. It is merely a keyword that indicates to the Swift compiler that a variable/constant/parameter may refer to any object or struct instance, so it isn't possible to extend Any.
If you consider what you are trying to do, you would have a subtle difference between your two extensions anyway;
The UIViewController extension needs to accept a target view (your v) parameter
While, for a UIView extension, you don't need v as this will be self; it doesn't make sense to install a gesture recogniser on some other UIView.
For the UIView extension, you may want to specify a different target for the selector.
You don't add a gesture recogniser to the UIViewController, so it doesn't make, semantically, to extend UIViewController in this way.
So, to me, it seems that the logical extension looks somthing like:
extension UIView {
func add(_ action:Selector,tapHandler target:Any = self) {
let t = UITapGestureRecognizer(target: target, action: action)
self.addGestureRecognizer(t)
}
}
Now, in a UIViewController you can say something like:
self.redButton.add(Selector(("handleTap")), tapHandler: self)
While in a UIView subclass you can say:
self.add(Selector(("handleTap")))
Not sure if my thinking here is correct but I have similar animations I use throughout my iOS project and I would like to condense it to 1 file and reuse wherever I want.
A brief example. In my animations file I have a scale animation
Animations.swift
class Animations {
class func scaleSmall(_ view: UIView) {
let scaleAnim = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
scaleAnim?.toValue = NSValue(cgSize: CGSize(width: 0.9, height: 0.9))
view.layer.pop_add(scaleAnim, forKey: "scaleSmallAnim")
}
}
Here I have one of my many swift files in my View folder and I would like to add that animation to the button
Button.swift
class Button: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
}
}
I thought I would be able to reference the animation from an additional file however everytime I do it this way I get the same error
Argument of '#selector' refers to instance method 'scaleSmall' that is not exposed to Objective-C
Am I referencing this function wrong?
try changing class func scaleSmall(_ view: UIView) {
to
#objc class func scaleSmall(view: UIView) {
I've confirmed my comment, so I'm posting an answer. Methods for UIButton need to be bridged to Obj-C. That's what #Kostas Tsoleridis suggests with his answer as well - it is not mixing two languages in one file, you are just marking the method for the compiler. Other solution would be to inherit from NSObject by your Animations class.
Now, as your confusion mentioned in a comment - it worked, because your Button class inherits from UIButton which is both from Obj-C world, and also inherits from NSObject down the chain.
To also address the issue mentioned in a comment under #Kostas Tsoleridis answer (and to be honest I should have thought about it before) - you can't pass self as a target and use a method from another class (even a static one). To solve this, you can use a singleton instance of your Animations class, something like this :
class Animations {
static let sharedInstance = Animations()
#objc class func scaleSmall(_ view: UIView) {
// your code
}
}
let button = UIButton()
button.addTarget(Animations.sharedInstance, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
I try figure out why self point to the GameViewController instead of Answer
GameViewController.swift
class GameViewController: UIViewController {
var gameplay = QuestionsController(colors: colors)
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(gameplay.answersController.answers[0].button)
}
func didPressAnswerButton(sender: UIButton!) {
sender.setTitle("Im from GameViewController class", forState: .Normal)
}
}
QuestionsController.swift
class QuestionsController {
var question: Question
var answersController: AnswersController
}
AnswersController.swift
class AnswersController {
var answers = [Answer]()
func prepareAnswers() {
let answer = Answer()
answers.append(answer)
}
}
Answer.swift
class Answer{
let button: UIButton
func prepareButton() {
let answerButton = AnswerButton(type: .System)
answerButton.addTarget(self, action: "didPressAnswerButton:", forControlEvents: .TouchUpInside)
button = answerButton
}
func didPressAnswerButton(sender: UIButton!) {
sender.setTitle("Im from Answer class", forState: .Normal)
}
}
addTarget:action:forControlEvents: tells the control (answerButton in this case) what method to call, and what object to call it on, when the user taps the button. Looking at your code in more detail:
answerButton.addTarget(self, action: "didPressAnswerButton:", forControlEvents: .TouchUpInside)
When the user taps a button, the TouchUpInside event fires on the answerButton, and when that happens we want to invoke a method didPressAnswerButton: on an Answer object
So, we need to tell answerButton what do do when this TouchUpEvent fires. You do this calling the addTarget:action:forControlEvents method on the answerButton
The self argument tells the answerButton what object to notify about the event: it is the target. In this context, self is an Answer object.
The "didPressAnswerButton:" argument indicates what method the answerButton should call in response to the tap event: this is the action
This is the target-action mechanism of Objective-C/Cocoa. It's a very common pattern, it's worth it to read the linked documentation to learn a bit more about how it works. The key is that this is based on Objective-C* message passing: in the code above, "didPressAnswerButton:" indicates a selector, which when paired with a target (self), tells the answerButton how to send a "message" to the target when the user taps the button.
Also, note that when you are editing a storyboard and ctrl-drag from a button to your view controller and select a method, you are also setting up a target/action using this same mechanism. You select the target object by dragging to the view controller icon (or some other icon), and then you pick the action/selector when clicking on a method name in the popup.
* Target-Action was originally designed for Objective-C, but for the common case of implementing a view controller, you can assume Swift works the same way. Just note when reading documentation that Swift uses simple strings for actions, whereas Objective-C uses #selector(...).