Search code examples
iosanimationdesign-patternscore-animationcocoa-design-patterns

Where should custom, reusable animation code go?


Overview: I currently have a custom UIView subclass that implements custom animation logic, and I'm not sure that the view class is the best place to put that code.

I'm making an iOS app in Swift that uses a UIView subclass I call DoorView. A DoorView represents a sliding door which, in response to a swipe gesture, performs a sliding animation to open.

Here's a complete animation as I have it now:

enter image description here

In trying to keep my View Controller light, I put the actual core animation code that handles these animations into my DoorView class. My View Controller handles the gesture by checking if it matches the gesture required to open a given door, and if so, calls an open() method on the DoorView.

So in ViewController:

@IBAction func swipe(sender: UISwipeGestureRecognizer) {
        if (sender.direction.rawValue == currentDoorView.door.swipeDirection.rawValue) {
            self.currentDoorView.open()
        }
    }

And here's the open() method in my DoorView class: Note: this is just the sliding animation, and the check for a Sliding-type will be used in the future to differentiate from other types of doors (e.g. hinged).

func open(withDuration duration: CFTimeInterval = 1.0) {

    /// We only slideOpen if switch statement below determines self.door is Sliding,
    /// so we downcast as SlidingDoor so we can switch on door.slideDirection.
    /// For each slideDirection case, a different translation is created,
    /// which is then passed into the slideAnimation below.
    func slideOpen() {

        let slidingDoor = self.door as! SlidingDoor
        let translation: CATransform3D

        switch slidingDoor.slideDirection {
        case .Down:
            let height = baseLayer.bounds.height
            translation = CATransform3DMakeTranslation(0, height, 0)
        case .Left:
            let width = openingLayer.bounds.size.width
            translation = CATransform3DMakeTranslation(-width, 0, 0)
        case .Right:
            let width = openingLayer.bounds.size.width
            translation = CATransform3DMakeTranslation(width, 0, 0)
        case .Up:
            let height = baseLayer.bounds.height
            translation = CATransform3DMakeTranslation(0, -height, 0)
        }

        let slideAnimation = {
            (completion:(() -> ())?) in
            CATransaction.begin()
            CATransaction.setCompletionBlock(completion)
            CATransaction.setAnimationDuration(duration)
            self.openingLayer.transform = translation
            CATransaction.commit()
        }

        /// Actual call to slideAnimation closure. 
        /// Upon completion, notify delegate and call walkThroughDoor()
        slideAnimation({
            self.delegate?.doorDidOpen(self)
            self.walkThroughDoor()
        })
    }

    /// Switch to determine door type, and thus appropriate opening animation.
    switch self.door {
    case is Sliding:
        slideOpen()
    default:
        print("is not sliding")
    }
}

So is it okay to put animation logic in a view class? It was my first instinct because a) these animations are specific to my DoorView, and b) because animateWithDuration is a class method of UIView, so there seems to be some precedent for animations to be handled by views/view classes themselves.

But I continue to develop my app, I will be adding more door types with their own animations, and I fear DoorView will grow too fat with animation code. Should I at that point simply start making DoorView subclasses (i.e. SlidingDoorView, HingedDoorView, etc.)?

Or should view animations be handled by the view controller? My problem with that, besides VC bloat, is that if I want to use DoorViews in other view controllers I'll need to duplicate code. This way, my DoorViews come packaged with their own animations and all my VCs need to do is call open().


Solution

  • There's two options I think you can do it and both equally acceptable.

    1. Using extension to seperate your animation code from the door view code

    Put in a new enum for the door type in your main DoorView class, so you know what type of door it is.

    Then create a new swift file and call it DoorAnimation.swift and put inside:

    extension DoorView {
      func open(withDuration duration: CFTimeInterval = 1.0) {
        switch doorType {
          case .Hinged:
            openHingedDoor(duration)
          case .Sliding:
            openSlidingDoor(duration)
        }
      }
    
      func openSlidingDoor(withDuration duration: CFTimeInterval = 1.0) {
        // Enter the custom code 
        // You can call use self to access the current door view instance
      }
    
      func openHingedDoor(withDuration duration: CFTimeInterval = 1.0) {
        // Enter hinged door animation code
      }
    }
    

    2. Subclass DoorView

    Within DoorView create an function:

    func open() {
      print("WARNING: This requires an override.")
    }
    

    Then just override open() inside each of your subclasses.

    Both structures have their advantages and disadvantages. I'd say if the doors don't do too much option 1 is great. If they have a lot of functionality, it would be better to do option 2.