I want to implement a shake to undo on my iPhone App, in Swift.
For now it works, but it didn't display an Alert asking to validate the undo gesture (with buttons "Cancel" and "Undo"). I can add this alert in the right place myself, but I'm not certain I should. Some articles make me think that the alert should appear automatically, in the undo/redo process, so there's something I missed, perhaps...
Here are the relevant bits of code
In the viewController
override func becomeFirstResponder() -> Bool {
return true
}
and
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
snowflake.undoLastAction()
}
}
and in my snowflake class, the action is inserting or modifying point in an array, so I store the value before the change in oldPathPoints, then
// Action du undo
undoManager?.registerUndo(withTarget: self, handler: { (targetSelf) in
snowflake.pathPoints = oldPathPoints
})
and finally the undo method
func undoLastAction() {
undoManager?.undo()
createPath()
}
So,
I succeeded to do it (with the help of a 5-years Book from Matt Neuburg!)
So, there's nothing to make in the viewController, I deleted becomeFirstResponder and motionEnded.
All is in the SnowFlake class: I added
let undoer = UndoManager()
override var undoManager : UndoManager? {
return self.undoer
}
override var canBecomeFirstResponder: Bool { return true }
So I communicate the undoManager I use and the class (a view here) can become firstResponder
after the manipulation of the view that can be undone (in my case in touchesEnded)
, I call
self.becomeFirstResponder()
I deleted the undoLastAction function (useless now), and
changed the undo action to a fonction
changePathUndoable(newPathPoints: finalPathPoints)
the function is like that
func changePathUndoable(newPathPoints: [CGPoint]) {
let oldPathPoints = Snowflake.sharedInstance.pathPoints
Snowflake.sharedInstance.pathPoints = newPathPoints
// Action du undo
undoer.setActionName("cutting")
undoer.registerUndo(withTarget: self, handler: { (targetSelf) in
targetSelf.changePathUndoable(newPathPoints: oldPathPoints)
})
Snowflake.sharedInstance.createPath()
self.setNeedsDisplay()
}
I had to create the function changePathUndoable instead of calling undoer.registerUndo directly just for having the redo action to work (I didn't test redo before).
I think that all the steps are necessary to do what I wanted: make the view receive the shake event, displays this dialog
and be able to undo/redo the modifications.
Related
Is possible to turn off the backswipe for certain part of the screen? In particular, I was thinking of using this method:
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer!) -> Bool {
return false;
}
I was thinking to recognize what part of the screen the user has pressed on, if it is within a certain interval, I would return true, else false. Not sure how to pickup where the user has pressed.
I assume you are talking about the functionality provided by the UINavigationController.
First your class should conform to the UIGestureRecognizerProtocol something like this:
class MyController: UIViewController, UIGestureRecognizerProtocol
Then you need to register to become the delegate for the navigation controller so probably in your viewDidLoad or viewWillAppear (depending on needs) you have this:
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
Then you can use the following to check where in the view the gesture start:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
if location.y > 500 {
return false
}
return true
}
Obviously this is just a test case where it ignores the gesture when it starts above 500 in the vertical and you can change that to whatever you want.
This question already has answers here:
Detect shake gesture IOS Swift
(7 answers)
Closed 5 years ago.
Hi I want to identify the iPhone SHAKE when user shakes their phone, either in background mode or in foreground mode of the app.
Please assist me.
Thanks in advance.
Try something like this:
override func motionBegan(_ motion: UIEventSubtype, with event: UIEvent?) {
print("Device was shaken!")
}
The main trick is that you need to have some UIView (not UIViewController) that you want as firstResponder to receive the shake event messages. Here's the code that you can use in any UIView to get shake events:
class ShakingView: UIView {
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if event?.subtype == .motionShake {
// Put in code here to handle shake
}
if super.responds(to: #selector(UIResponder.motionEnded(_:with:))) {
super.motionEnded(motion, with: event)
}
}
override var canBecomeFirstResponder: Bool { return true }
You can easily transform any UIView (even system views) into a view that can get the shake event simply by subclassing the view with only these methods (and then selecting this new type instead of the base type in IB, or using it when allocating a view).
In the view controller, you want to set this view to become first responder:
override func viewWillAppear(_ animated: Bool) {
shakeView.becomeFirstResponder()
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
shakeView.resignFirstResponder()
super.viewWillDisappear(animated)
}
Don't forget that if you have other views that become first responder from user actions (like a search bar or text entry field) you'll also need to restore the shaking view first responder status when the other view resigns!
This method works even if you set applicationSupportsShakeToEdit to NO.
For objective c version refer link How do I detect when someone shakes an iPhone?
I have used collectionView and also implemented pull to sync (pull_Down_To_Get_Data_From_Server) in my ViewController(named : "DashboardViewController"). I tried the shake gesture
code(given below) in my DashboardViewController and its not working but similar code is working in another viewController of same app.
I used the canBecomeFirstResponder in viewDidLoad in first try and in viewDidAppear secondly but it was also useless.
Requirement:
Actualy i want to know the cases(if any) in which ".motionShake" event do not trigger directly and we have to implement another way? Also that i did not find anything fruitful
on google.
Image of storyboard may help in understanding all the utilities used
override func viewDidLoad() {
// call super
super.viewDidLoad()
self.becomeFirstResponder() // for shake gesture detection
}
// it will make first responder to detect shake motion event
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
// get detection of shake motion event
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if motion == .motionShake
{
print("Shake detected")
}
}
We ran into something similar while working on Yoshi
We ended up creating an extension on UIWindow, that contained the motionBegan event and forwarded the event. Not sure what caused it, but it seemingly broke with swift 3.0.
everyone knows that when you drag outside a button it don't cancel the highlight state right away by UIButton's default. UIControlEventTouchDragExit triggers when 70 pixels away. I want that distance to be 0. So after searching the solution of it, I tried to create a subclass like this:
import UIKit
class UINewButton: UIButton {
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
print("here")
let touchOutside = !CGRectContainsPoint(self.bounds, touch.locationInView(self))
if touchOutside {
let previousTochInside = CGRectContainsPoint(self.bounds, touch.previousLocationInView(self))
if previousTochInside {
print("Sending UIControlEventTouchDragExit")
self.sendActionsForControlEvents(.TouchDragExit)
self.highlighted = false
self.selected = false
}else{
print("Sending UIControlEventTouchDragOutside")
self.sendActionsForControlEvents(.TouchDragOutside)
}
}else{
let previousTouchOutside = !CGRectContainsPoint(self.bounds, touch.previousLocationInView(self))
if previousTouchOutside{
print("Sending UIControlEventTouchDragEnter")
self.sendActionsForControlEvents(.TouchDragEnter)
}else{
print("Sending UIControlEventTouchDragInside")
self.sendActionsForControlEvents(.TouchUpInside)
}
}
return super.continueTrackingWithTouch(touch, withEvent: event)
}
}
and create a button like this in a UIViewController
#IBOutlet var confirmButton: UINewButton!
I assumed when a UIButton being touched and dragged. It would call the function in this sequence:
beginTrackingWithTouch(when touched) -> continueTrackingWithTouch(when dragged) -> endTrackingWithTouch(when left)
But here is the weird part. Even though I override the function continueTrackingWithTouch, it still not been called. Cause the console window didn't show "here" where I put there in it. And the result remain the default distance 70. how come is that?
I tried to call the three functions mentioned above and return true if it needs one.
What did I missed?
After reading this article: UIControlEventTouchDragExit triggers when 100 pixels away from UIButton
Still not helping :( (plus it written in objective-C...)
Isn't the distance of 70px a property of the function so I can just changed?(How can I see the original function by the way? There is no detail in Apple Developer Documentation...)
Should I use button.addtarget in the UIViewController? But it seems like another way to do it.
Here is another question:
If I want to cancel the highlight state when dragged outside the button, is this right?
self.highlighted = false
self.selected = false
I don't know which one is the right one so I used it all.
please help! Just a newbie in swift but I have been stuck in this problem for 3 days. QQ
In Swift 3 the function signature has changed. It's now:
func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
API Reference
I got a iPad-designed app using a SplitViewController showing two views, one with a contacts list and another with details of this contact. The SplitView works well on iPad but has some problems on iPhones.
There is a UIControl that take all the size of the Master View, that check if there is any .touchDown interaction by the user and some methods called to enable or disable this UIControl depending if we are on editing contact mode or not allowing user to interact with the screen or not :
private var disableInteractionClosure: (()->())?
private lazy var interactionOverlay: UIControl = {
let c = UIControl()
c.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
c.addTarget(self, action: "interactionOverlayAction", forControlEvents: .TouchDown)
return c
}()
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
navigationController!.view.addSubview(interactionOverlay)
}
}
func interactionOverlayAction() {
disableInteractionClosure!()
}
func enableInteraction() {
disableInteractionClosure = nil
interactionOverlay.removeFromSuperview()
}
Basically the UIControl is used to block user from switching between contact while user is editing another contact/ creating a new one by blocking interaction with the contact list and if changes have been made on the editing/creating view it fires a method that shows a pop up saying "modifications have been made do you want to continue without saving ? cancel or continue " :
func cancel() {
self.view.endEditing(true)
let closure: ()->() = {
self.layoutView.willResign()
self.delegate?.tabDetailsControllerDidCancel(self)
}
if layoutView.hasChanges() {
MKAlertViewController().instantaneousAlert(title: "Modification apportées", message: "Êtes-vous sur de vouloir continuer sans sauvegarder les modifications ?", confirmButtonTitle: "Oui", cancelButtonTitle: "Annuler", confirmed: { () -> Void in
closure()
}, canceled: { () -> Void in
})
} else {
closure()
}
}
It works fine on iPad because the UIControl is only above the Master View and is enabled when in editing mode on the Detail View (iPad 3D Debugging view), so the pop up shows only when manually cancelling the editing/creating or when trying to change contact while editing/creating, but as the splitView don't work the same on iPads and iPhones and it appears that on iPhone the Master View is placed above the Detail View, the UIControl is also above (iPhone 3D Debugging view), and it causes to block interactions on all the screen and wherever I click the cancel pop-up is shown.
Can you guys explain me a way to enable/show this UIControl only when the MasterView is showing and not everywhere. Thanks.
I ended up using the viewWillDisappear on the detail view :
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParentViewController() || self.isBeingDismissed() {
if editingMode {
shared.contactsListController.disableInteractionWhileEditingWithClosure({ (_) in
self.tabDetailsController.cancel()
})
shared.contactsListController.disableToolbar()
} else {
shared.contactsListController.enableInteraction()
shared.contactsListController.enableToolbar()
}
self.navigationController?.toolbar.alpha = 1
}
}
and modifying the disableInteractionWhileEditingWithClosure method on the master view:
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
view.addSubview(interactionOverlay) // this line
}
}
and it works ! :)