神刀安全网

Protocol-Oriented Views in Swift

Join me for a Swift Community Celebration :tada: in New York City on September 1st and 2nd. Use code NATASHATHEROBOT to get $100 off!

I recently gave a talk on Practical Protocol-Oriented-Programming (POP:boom:) in Swift. The video is still being processed. Meanwhile, here is the written-up version of the POP View part of the talk for reference (for me and anyone else!).

The Setup

Let’s say you have a simple app with an image an a button. The product manager wants the image to shake when the button is pressed:

Since this is a common animation used when the user’s username or password is wrong, it is easy to find the code for it on StackOverflow (as any good developer would :grin:).

The hardest task here is figuring out where to put the code, which is not that hard. I’m just going to subclass the ImageView and add a shake() method to it.

//  FoodImageView.swift   import UIKit   class FoodImageView:UIImageView {          // shake code goes here     func shake() {         let animation = CABasicAnimation(keyPath: "position")         animation.duration = 0.05         animation.repeatCount = 5         animation.autoreverses = true         animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))         animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))         layer.addAnimation(animation, forKey: "position")     } } 

Now, when the user presses the button, I can just call the shake method on the view:

//  ViewController.swift   import UIKit   class ViewController:UIViewController {       @IBOutletweak var foodImageView: FoodImageView!          @IBActionfunc onShakeButtonTap(sender: AnyObject) {         // shake method called here         foodImageView.shake()     } } 

Nothing exciting here. I’m done and I can move on to other tasks… Thanks StackOverflow!

Extending Functionality

However, just like in real-world development, just when you think you’re done and moving on, the designer comes over and says that they also want the button to shake with the view…

You can of course just do the same thing – subclass the button and add a shake method:

//  ShakeableButton.swift   import UIKit   class ActionButton:UIButton {       func shake() {         let animation = CABasicAnimation(keyPath: "position")         animation.duration = 0.05         animation.repeatCount = 5         animation.autoreverses = true         animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))         animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))         layer.addAnimation(animation, forKey: "position")     }   } 

And now you can just shake both views when the user clicks the button:

//  ViewController.swift   class ViewController:UIViewController {       @IBOutletweak var foodImageView: FoodImageView!     @IBOutletweak var actionButton: ActionButton!          @IBActionfunc onShakeButtonTap(sender: AnyObject) {         foodImageView.shake()         actionButton.shake()     } } 

But hopefully you stop yourself… Having the shake() code in two places violates the DRY (don’t repeat yourself) principle. If a designer comes over in the future and asks for more or less of a shake, you’ll have to change the logic in both places, which is not ideal of course.

So how do you refactor this?

The Usual Way

If you come from Objective-C, you likely just put the shake() code into a UIView Category (extension in Swift):

//  UIViewExtension.swift   import UIKit   extension UIView {          func shake() {         let animation = CABasicAnimation(keyPath: "position")         animation.duration = 0.05         animation.repeatCount = 5         animation.autoreverses = true         animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))         animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))         layer.addAnimation(animation, forKey: "position")     } } 

Now, both the UIImageView and the UIButton (and every single other view), has the shake() method available:

class FoodImageView:UIImageView {     // other customization here }   class ActionButton:UIButton {     // other customization here }   class ViewController:UIViewController {       @IBOutletweak var foodImageView: FoodImageView!     @IBOutletweak var actionButton: ActionButton!          @IBActionfunc onShakeButtonTap(sender: AnyObject) {         foodImageView.shake()         actionButton.shake()     } } 

However, as you can immediately see, there is now nothing particularly in the FoodImageView or ActionButton code that shows that there is an intention for it to shake. It’s just a random method that you know is there because you wrote the extension (aka category).

Furthermore, the Category pattern can easily get out of hand. It tends to become a dumpster for code that you don’t know where else to put. Soon, there is so much there, you don’t even know why it’s there and where it’s supposed to be used. A bit more on why Categories are considered harmful here .

So what to do… ��

Protocols FTW!

You guessed it! The Swifty solution is to use protocols! We can use the power of protocol extensions to create a Shakeable protocol with a default shake() implementation:

//  Shakeable.swift   import UIKit   protocol Shakeable { }   // we can constrain the shake method to only UIViews! extension Shakeablewhere Self:UIView {          // default shake implementation     func shake() {         let animation = CABasicAnimation(keyPath: "position")         animation.duration = 0.05         animation.repeatCount = 5         animation.autoreverses = true         animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))         animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))         layer.addAnimation(animation, forKey: "position")     } } 

Now, we just add the Shakeable protocol conformance any views that we actually intend to have shake:

class FoodImageView: UIImageView, Shakeable {     // other customization here }   class ActionButton: UIButton, Shakeable {     // other customization here }   class ViewController:UIViewController {       @IBOutletweak var foodImageView: FoodImageView!     @IBOutletweak var actionButton: ActionButton!          @IBActionfunc onShakeButtonTap(sender: AnyObject) {         foodImageView.shake()         actionButton.shake()     } } 

The first thing to notice here is the readability! Just by looking at the class declaration of the FoodImageView and the ActionButton , you can immediately see that it’s meant to shake.

If the designer comes over and also wants the view to Dim a little while shaking, we can use the same protocol extension pattern to add that functionality, making for super nice composition.

// adding the dimming functionality class FoodImageView: UIImageView, Shakeable, Dimmable {     // other implementation goes here } 

And when the product manager no longer wants the view to shake, it is super easy to refactor. Just get rid of the Shakeable protocol conformance!

class FoodImageView: UIImageView, Dimmable {     // other implementation goes here } 

Conclusion

By using protocol extensions for view composition, you’re adding super nice READABILITY, REUSABILITY, and MAINTAINABILITY to your code base.

P.S. I also recommend reading the Transparent View Controllers and Dim Backgrounds tutorial for more advanced applications of this pattern :speak_no_evil:

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Protocol-Oriented Views in Swift

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址