App Crashes when trying to make view draggable with protocol - ios

So I wrote a protocol to make what ever UIView that conforms it draggable. However when I test this in the simulator it crashes when I try to drag the view. and displays this in the log
libc++abi.dylib: terminating with uncaught exception of type NSException
The protocol:
protocol Draggable {}
extension Draggable where Self: UIView {
func wasDragged (gestrue: UIPanGestureRecognizer) {
let translation = gestrue.translation(in: UIScreen.main.focusedView)
if let label = gestrue.view {
label.center = CGPoint(x: label.center.x + translation.x, y: label.center.y + translation.y)
}
gestrue.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: UIScreen.main, action: Selector(("wasDragged:")))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
and in a custom label class I conformed it:
class DraggableLabel: UILabel, Draggable {
}
Then I called the setGesutre function in viewDidLoad of the view controller:
override func viewDidLoad() {
super.viewDidLoad()
draggableLabel.setGesture()
}
OK I admit I don't really know what I'm doing.

The action wasDragged(gesture:) needs to be accessible for message-dispatch via the Objective-C runtime. Use the #objc annotation to make a method available for message dispatch. Methods of NSObject subclasses are automatically #objc methods.
The bad news is that this will only work for Objective-C-compatible classes or extensions. Protocol extensions like yours are not compatible, so you cannot put action methods into those extensions.
Your options are to add this functionality to a subclass or a plain class extension:
extension DraggableLabel {
func wasDragged (gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: UIScreen.main.focusedView)
center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: self,
action: #selector(wasDragged(sender:)))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
(Notice that I also changed the target of the gesture recognizer to the view instead of the main screen. Did you intend to use the responder chain to propagate the event to the right view?)
The obvious disadvantage is the reduced flexibility compared to the protocol oriented approach. If that's a problem I would look into class composition. Create a class that encapsulates the gesture recognizer and its action method. Give it a view property and configure everything when that property is set:
class DraggingBehavior: NSObject {
#IBOutlet var view: UIView? {
didSet {
guard let view = view else { return }
let gesture = UIPanGestureRecognizer(target: self, action: #selector(wasDragged(sender:)))
view.addGestureRecognizer(gesture)
view.isUserInteractionEnabled = true
}
}
func wasDragged(sender: UIGestureRecognizer) {
print("Was dragged")
// put the view translation code here.
}
}
The #IBOutlet makes this class compatible with Interface Builder. Drag in a Custom Object, set its class to DraggingBehavior, connect the view outlet to the view you would like to make draggable.

Related

BulletinBoard assign gesture for ImageView

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
}
}

Adding a UIView/UIGestureRecognizer to the presented view in a UIPresentationController

I am trying to recreate the bottom drawer functionality seen in Maps or Siri Shortcuts by using a UIPresentationController by having it recognise user input and updating the frameOfPresentedViewInContainerView accordingly. However I want this mechanism to work independently of the presented UIViewController as much as possible so I'm trying to have the presentation controller add a handle area above the view. Ideally the view of the presented controller and the handle are should both recognise user input.
This works for the presented view, however any view I add to it responds to no UIGestureRecognizer at all. Am I missing something?
class PresentationController: UIPresentationController {
private let handleArea: UIView = UIView()
override var frameOfPresentedViewInContainerView: CGRect {
// Return some frame for now
return CGRect(x: 0, y: 250, width: containerView!.frame.width, height: 500)
}
override func presentationTransitionWillBegin() {
// Unwrap presented view
guard let presentedView = self.presentedView else {
return
}
// Set color
self.handleArea.backgroundColor = UIColor.green
// Add to view hierachy
presentedView.addSubview(self.handleArea)
// Set constraints
self.handleArea.trailingAnchor.constraint(equalTo: presentedView.trailingAnchor).isActive = true
self.handleArea.leadingAnchor.constraint(equalTo: presentedView.leadingAnchor).isActive = true
self.handleArea.bottomAnchor.constraint(equalTo: presentedView.topAnchor).isActive = true
self.handleArea.heightAnchor.constraint(equalToConstant: 56).isActive = true
self.handleArea.translatesAutoresizingMaskIntoConstraints = false
// These don't help
self.handleArea.isUserInteractionEnabled = true
presentedView.isUserInteractionEnabled = true
presentedView.bringSubviewToFront(self.handleArea)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if completed {
// Add gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onHandleAreaTapped(sender:)))
self.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// Remove subview
self.handleArea.removeFromSuperview()
}
// MARK: - Responder
#objc private func onHandleAreaTapped(sender: UITapGestureRecognizer) {
print("tap") // No output
}
}
I managed to solve it by adding both the handle area and the view of the presentedViewController to a custom view and then overriding the presentedView property and returning my custom view.

Passing UIView control events through UIView extension

This might be a weird question, but i'm trying to code like a pro which obviously i am not.
Right now i have an extension which uses UIView and my concept is making it like an alert
For example, i coded the following:
extension UIView {
typealias completionHandler = (_ success:Bool) -> Void
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval) {
let luna = UIView()
luna.frame = CGRect(x: 16, y: 30, width: screenWidth - 30, height: 60)
luna.center.x = self.center.x
luna.backgroundColor = .white
luna.addShadow(radius: 11, opacity: 0.2)
luna.layer.cornerRadius = 10
}
}
And on my other ViewController, I use this to present Luna
#IBAction func presentLuna(_ sender: Any) {
self.view.showLuna(title: "Oooh", message: "Oops, something went horribly wrong!", dissmiss: 2.5);
}
At this very specific moment, I've been digging StackOverFlow for a day to find an answer. How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
This is maybe what #rmaddy means by subclassing UIView:
First, create a subview so you can use it to respond to touch events:
class LunaView: UIView {
typealias LunaViewCompletionBlock = (_ isSuccessful: Bool) -> Void
var label: UILabel
//other things you need
var completionHandler: LunaViewCompletionBlock?
init(frame: CGRect, /* other properties such as title and colour */, completionHandler: LunaViewCompletionBlock?) {
self.completionHandler = completionHandler
self.label = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
//this is how we handle touch
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTouch)))
addSubview(label)
// set up frame or constraints(if you use autolayout) for label and other views
// configure label.text and other things
}
func handleTouch() {
completionHandler(/*true or false*/)
}
/*other methods needed in this class*/
}
Then, to use it, simply do this in the extension:
extension UIView {
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval, completionHandler: LunaView.LunaViewCompletionBlock) {
let luna = LunaView(frame: /*size*/, /*other things like title*/, completionHandler: completionHandler)
// set luna's center, shadow, auto-dismiss time, colour.....
}
}
Answering your question:
How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
You can't detect a touch event on just a general UIView you need a UIControl. Since the UIControl type inherits from UIView you will be able to do any of your typical drawing or view hierarchy stuff but you can also setup touch actions and callbacks using addTarget(:action:for:) or other UIControl mechanisms.

Swift3 Extension on "Any?" -ish class?

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")))

How to add gesture recognizer with class method as selector?

I'm trying to add a gesture recognizer on my UIImageView from my UIViewController extensions methods.
The method I want to fire when the image is tapped is a class method declared in my swift extensions :
class func openPopViewWithText(text: String!) {
print("fire!")
}
I add the selector from my extensions class too. This is how I add the selector :
infoImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(UIViewController.openPopViewWithText(_:)) ))
And this is the error that I get when I tap the ImageView :
[XXXDisplayStatsViewController openPopViewWithText:]: unrecognized selector sent to instance 0x7fd31520ada0
Complete code in my extensions class :
public extension UIViewController {
func addInfoImageViewWithText(infoText: String){
let margins : CGFloat = 30.0
let infoImageView : UIImageView = UIImageView(image: UIImage(named: "info"));
let yOrigin : CGFloat = ((self.view.y + self.view.height) - infoImageView.height) - margins
infoImageView.frame = CGRectMake(margins, yOrigin, infoImageView.width + 5, infoImageView.height + 5)
self.view.insertSubview(infoImageView, atIndex: 0)
self.view.bringSubviewToFront(infoImageView)
infoImageView.userInteractionEnabled = true
infoImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(UIViewController.openPopViewWithText(_:)) ))
}
class func openPopViewWithText(text: String!) {
print("fire!")
}
}
The problem is that first argument in your openPopViewWithText function is String rather than a UITapGestureRecognizer. When you tap on the UIImageView gesture recognizer is fired and it passes the gesture recognizer instance to the selector so you can track its properties. So what you should do is change the current function signature to this:
class func openPopViewWithText(recognizer: UITapGestureRecognizer) {
print("fire!")
}
Other than that, you should not use class functions because you're not be able to access any UIViewController properties.

Resources