How to correctly subclass UIControl?

后端 未结 7 652
北海茫月
北海茫月 2020-12-04 09:26

I don\'t want UIButton or anything like that. I want to subclass UIControl directly and make my own, very special control.

But for some rea

7条回答
  •  再見小時候
    2020-12-04 09:55

    Swift

    These is more than one way to subclass UIControl. When a parent view needs to react to touch events or get other data from the control, this is usually done using (1) targets or (2) the delegate pattern with overridden touch events. For completeness I will also show how to (3) do the same thing with a gesture recognizer. Each of these methods will behave like the following animation:

    You only need to choose one of the following methods.


    Method 1: Add a Target

    A UIControl subclass has support for targets already built in. If you don't need to pass a lot of data to the parent, this is probably the method you want.

    MyCustomControl.swift

    import UIKit
    class MyCustomControl: UIControl {
        // You don't need to do anything special in the control for targets to work.
    }
    

    ViewController.swift

    import UIKit
    class ViewController: UIViewController {
    
        @IBOutlet weak var myCustomControl: MyCustomControl!
        @IBOutlet weak var trackingBeganLabel: UILabel!
        @IBOutlet weak var trackingEndedLabel: UILabel!
        @IBOutlet weak var xLabel: UILabel!
        @IBOutlet weak var yLabel: UILabel!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Add the targets
            // Whenever the given even occurs, the action method will be called
            myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)
            myCustomControl.addTarget(self, action: #selector(didDragInsideControl(_:withEvent:)),
                                  forControlEvents: UIControlEvents.TouchDragInside)
            myCustomControl.addTarget(self, action: #selector(touchedUpInside), forControlEvents: UIControlEvents.TouchUpInside)
        }
    
        // MARK: - target action methods
    
        func touchedDown() {
            trackingBeganLabel.text = "Tracking began"
        }
    
        func touchedUpInside() {
            trackingEndedLabel.text = "Tracking ended"
        }
    
        func didDragInsideControl(control: MyCustomControl, withEvent event: UIEvent) {
    
            if let touch = event.touchesForView(control)?.first {
                let location = touch.locationInView(control)
                xLabel.text = "x: \(location.x)"
                yLabel.text = "y: \(location.y)"
            }
        }
    }
    

    Notes

    • There is nothing special about the action method names. I could have called them anything. I just have to be careful to spell the method name exactly like I did where I added the target. Otherwise you get a crash.
    • The two colons in didDragInsideControl:withEvent: means that two parameters are being passed to the didDragInsideControl method. If you forget to add a colon or if you don't have the correct number of parameters, you will get a crash.
    • Thanks to this answer for help with the TouchDragInside event.

    Passing other data

    If you have some value in your custom control

    class MyCustomControl: UIControl {
        var someValue = "hello"
    }
    

    that you want to access in the target action method, then you can pass in a reference to the control. When you are setting the target, add a colon after the action method name. For example:

    myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown) 
    

    Notice that it is touchedDown: (with a colon) and not touchedDown (without a colon). The colon means that a parameter is being passed to the action method. In the action method, specify that the parameter is a reference to your UIControl subclass. With that reference, you can get data from your control.

    func touchedDown(control: MyCustomControl) {
        trackingBeganLabel.text = "Tracking began"
    
        // now you have access to the public properties and methods of your control
        print(control.someValue)
    }
    

    Method 2: Delegate Pattern and Override Touch Events

    Subclassing UIControl gives us access to the following methods:

    • beginTrackingWithTouch is called when the finger first touches down within the control's bounds.
    • continueTrackingWithTouch is called repeatedly as the finger slides across the control and even outside of the control's bounds.
    • endTrackingWithTouch is called when the finger lifts off the screen.

    If you need special control of the touch events or if you have a lot of data communication to do with the parent, then this method may work better then adding targets.

    Here is how to do it:

    MyCustomControl.swift

    import UIKit
    
    // These are out self-defined rules for how we will communicate with other classes
    protocol ViewControllerCommunicationDelegate: class {
        func myTrackingBegan()
        func myTrackingContinuing(location: CGPoint)
        func myTrackingEnded()
    }
    
    class MyCustomControl: UIControl {
    
        // whichever class wants to be notified of the touch events must set the delegate to itself
        weak var delegate: ViewControllerCommunicationDelegate?
    
        override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
    
            // notify the delegate (i.e. the view controller)
            delegate?.myTrackingBegan()
    
            // returning true means that future events (like continueTrackingWithTouch and endTrackingWithTouch) will continue to be fired
            return true
        }
    
        override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
    
            // get the touch location in our custom control's own coordinate system
            let point = touch.locationInView(self)
    
            // Update the delegate (i.e. the view controller) with the new coordinate point
            delegate?.myTrackingContinuing(point)
    
            // returning true means that future events will continue to be fired
            return true
        }
    
        override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
    
            // notify the delegate (i.e. the view controller)
            delegate?.myTrackingEnded()
        }
    }
    

    ViewController.swift

    This is how the view controller is set up to be the delegate and respond to touch events from our custom control.

    import UIKit
    class ViewController: UIViewController, ViewControllerCommunicationDelegate {
    
        @IBOutlet weak var myCustomControl: MyCustomControl!
        @IBOutlet weak var trackingBeganLabel: UILabel!
        @IBOutlet weak var trackingEndedLabel: UILabel!
        @IBOutlet weak var xLabel: UILabel!
        @IBOutlet weak var yLabel: UILabel!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            myCustomControl.delegate = self
        }
    
        func myTrackingBegan() {
            trackingBeganLabel.text = "Tracking began"
        }
    
        func myTrackingContinuing(location: CGPoint) {
            xLabel.text = "x: \(location.x)"
            yLabel.text = "y: \(location.y)"
        }
    
        func myTrackingEnded() {
            trackingEndedLabel.text = "Tracking ended"
        }
    }
    

    Notes

    • To learn more about the delegate pattern, see this answer.
    • It is not necessary to use a delegate with these methods if they are only being used within the custom control itself. I could have just added a print statement to show how the events are being called. In that case, the code would be simplified to

      import UIKit
      class MyCustomControl: UIControl {
      
          override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
              print("Began tracking")
              return true
          }
      
          override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
              let point = touch.locationInView(self)
              print("x: \(point.x), y: \(point.y)")
              return true
          }
      
          override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
              print("Ended tracking")
          }
      }
      

    Method 3: Use a Gesture Recognizer

    Adding a gesture recognizer can be done on any view and it also works on a UIControl. To get similar results to the example at the top, we will use a UIPanGestureRecognizer. Then by testing the various states when an event is fired we can determine what is happening.

    MyCustomControl.swift

    import UIKit
    class MyCustomControl: UIControl {
        // nothing special is required in the control to make it work
    }
    

    ViewController.swift

    import UIKit
    class ViewController: UIViewController {
    
        @IBOutlet weak var myCustomControl: MyCustomControl!
        @IBOutlet weak var trackingBeganLabel: UILabel!
        @IBOutlet weak var trackingEndedLabel: UILabel!
        @IBOutlet weak var xLabel: UILabel!
        @IBOutlet weak var yLabel: UILabel!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // add gesture recognizer
            let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognized(_:)))
            myCustomControl.addGestureRecognizer(gestureRecognizer)
        }
    
        // gesture recognizer action method
        func gestureRecognized(gesture: UIPanGestureRecognizer) {
    
            if gesture.state == UIGestureRecognizerState.Began {
    
                trackingBeganLabel.text = "Tracking began"
    
            } else if gesture.state == UIGestureRecognizerState.Changed {
    
                let location = gesture.locationInView(myCustomControl)
    
                xLabel.text = "x: \(location.x)"
                yLabel.text = "y: \(location.y)"
    
            } else if gesture.state == UIGestureRecognizerState.Ended {
                trackingEndedLabel.text = "Tracking ended"
            }
        }
    }
    

    Notes

    • Don't forget to add the colon after the action method name in action: "gestureRecognized:". The colon means that parameters are being passed in.
    • If you need to get data from the control, you could implement the delegate pattern as in method 2 above.

提交回复
热议问题