How to detect user interactions on a whole viewcontroller

Viewed 2049

I would like to detect the user interactions on a UIViewController like

  1. Editing Text Fields
  2. Selecting picture from gallery
  3. Picker view selections

Its indented to show a notification like "You may lose data you have entered" when user goes out of page. Can be back button.

Have seen many answers but i need the best and proper way of doing this functionality.

8 Answers

my two cents.

  • what are you asking is to implement a NON-iOS behaviour, see for example iOS prefs: is very rare to ask user to be careful or his data will be lost.

  • the iOS logic is usually:

    a) automatically save every data (to help user..) and save it

    b) collect data as user moves, and eventually at the end save or throw it away

    c) iOS user has in his "DNA" this kind of behaviour, so is weird to ask to save... it seems windows-style use of UI.

    d) as a programmer what You want to implement is a bit lengthy, as You are not follow Apple HIG, so there is no specific call to get it. What other guys are saying is generally correct, you have to map multiple callbacks/delegates.

The best way to use in Swift is to take the advantage of swift property observer. Let's assume you have user profile view with:

  • First, last and middle name field to enter
  • Image view to select and show user picture
  • A date of birth field to select dates from picker view

Your model will look this:

struct ProfileModel {
    var isUpdated: Bool = false
    var firstName: String {
        willSet {
            if newValue != firstName {
                isRecentUpdated = true
            }
        }
    }
    var middleName: String {
        willSet {
            if newValue != middleName {
                isRecentUpdated = true
            }
        }
    }
    var lastName: String {
        willSet {
            if newValue != lastName {
                isRecentUpdated = true
            }
        }
    }
    var profilePhoto: URL {
        willSet {
            if newValue != profilePhoto {
                isRecentUpdated = true
            }
        }
    }
    var dob: Date {
        willSet {
            if newValue != dob {
                isRecentUpdated = true
            }
        }
    }
}

Here, model has state variable isUpdated which track model state change whenever any of property modified to new value - have kept check if indeed user action has changed existing value in model.

Whenever, you launch profile screen you can set isUpdated to false so that you can track updated profile data on each launch of profile view.

OR

If you are populating this model from server then after every fetch you can reset the model state (isUpdated flag) which then can track any further change.

Hope it helps.

That main problem is - user could make changes but become same object. Means change 10500 times filed and select same that was on start. Is Recent Updated ? I guess no !.

How to get diff ? - Compare a hash ! How to do that ? - use "==". That all.

A bit of abstract code.

  1. Override back button. (Yeah, its bad practice but there is not documented way to own back button process)

  2. Make a diff

  3. Show alert

struct CustomModel : Hashable, Equatable {
        var name : String
        //your custom model

public var hashValue: Int { return name.hashValue } static func ==(lhs: CustomModel, rhs: CustomModel) -> Bool { return lhs.hashValue == rhs.hashValue } } class VoteViewController : UIViewController { private var originalModel : CustomModel! private var mutableCopy : CustomModel! override func viewDidLoad() { super.viewDidLoad() mutableCopy = originalModel navigationItem.hidesBackButton = true let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(VoteViewController.back(sender:))) navigationItem.leftBarButtonItem = newBackButton } func back(sender: UIBarButtonItem) { // ChechDataModification guard mutableCopy == originalModel else { presentAlertPop() return } _ = navigationController?.popViewController(animated: true) } func presentAlertPop(){ //You may lose data you have entered ! } }

Use this to do it. This is for the whole screen in a particular VC. If you want to detect user interaction with a specific UI element, there are different methods for each element. A text view for example has it's own delegate method to detect when something has modified or changed it or touches it.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
}

For scrolling, you can use this method

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    <#code#>
}

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    <#code#>
}

you can save the data that is there on the viewcontroller when the viewcontroller is presented and upon pressing the back button you can check if current data in the input fields is different than the one you saved.. you can display popup of unsaved data.I think it should help.

By setting userInteractionEnabled to NO for your scroll view, the view controller will start receiving touch events since UIViewController is a subclass of UIResponder. You can override one or more of these methods in your view controller to respond to these touches:

touchesBegan: withEvent:

touchesMoved: withEvent:

touchesEnded: withEvent:

touchesCancelled: withEvent:

class ViewController: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
        @IBOutlet weak var textView: UITextView!
        @IBOutlet weak var lableView: UILabel! 
       @IBOutlet weak var textField: UITextField! 
   var arrayViews = NSMutableArray()

    override func viewDidLoad() {
        super.viewDidLoad()
        arrayViews.add(textView)
        arrayViews.add(lableView)
        arrayViews.add(textField)



    }

    override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
        touched(touches)
    }

    override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
        touched(touches)
    }

    func touched(touches: NSSet!) {
        // Get the first touch and its location in this view controller's view coordinate system
        let touch = touches.allObjects[0] as UITouch
        let touchLocation = touch.locationInView(self.view)

        for viewLocate in arrayViews {
            // Convert the location of the obstacle view to this view controller's view coordinate system
            let viewLocateFrame = self.view.convertRect(viewLocate.frame, fromView: viewLocate.superview)

            // Check if the touch is inside the obstacle view
            if CGRectContainsPoint(viewLocateFrame, touchLocation) {
                println("Touch!")
            }
        }
    }

}

This should work please let me know if it doesnt

  1. create a boolean flag to show the current screen state
  2. use delegate methods to update the screen state
  3. check the state before the user leaves the screen

Perhaps try either adding hit tests to the main view or creating a custom container to hold the view(s). This approach is quite simple and easy to use, and should catch any touch (buttons, text fields, scrolls, etc.)

 override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {

     // Do whatever you need to in here

}

OR use the hit point to fetch some kind of UIView touched

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    let subview = super.hitTest(point, withEvent: event)
    return subview == self.nextPageBtn || subview == self.backPageBtn ? subview : nil

}
Related