Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hidden animation bugs #72

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 12 additions & 4 deletions TZStackView.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
5F50E9F2F7E5B2DA68C946E0 /* ExplicitIntrinsicContentSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F50E5EB8202F5247F8517F3 /* ExplicitIntrinsicContentSizeView.swift */; };
5F50EAD959E8ACC5929DBD75 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; };
5F50EF474D670FC33E8E80EA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F50ED5A43FBFC32B9B9E1AA /* Images.xcassets */; };
7E44D3921C906B1800A3D266 /* TZFuncAnimationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44D3911C906B1800A3D266 /* TZFuncAnimationDelegate.swift */; };
7E44D3941C906C3F00A3D266 /* HidingAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44D3931C906C3F00A3D266 /* HidingAnimationTests.swift */; };
A45441C21B9B6D71002452BA /* TZStackView.h in Headers */ = {isa = PBXBuildFile; fileRef = A45441C11B9B6D71002452BA /* TZStackView.h */; settings = {ATTRIBUTES = (Public, ); }; };
A45441C61B9B6D71002452BA /* TZStackView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; };
A45441C71B9B6D71002452BA /* TZStackView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A45441BF1B9B6D71002452BA /* TZStackView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */; settings = {ASSET_TAGS = (); }; };
A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CD1B9B6D9C002452BA /* TZStackView.swift */; settings = {ASSET_TAGS = (); }; };
A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */; settings = {ASSET_TAGS = (); }; };
A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */; settings = {ASSET_TAGS = (); }; };
A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CC1B9B6D9C002452BA /* TZSpacerView.swift */; };
A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CD1B9B6D9C002452BA /* TZStackView.swift */; };
A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */; };
A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */; };
DB41AF6A1B294B8E003DB902 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB41AF691B294B8E003DB902 /* NSLayoutConstraintExtension.swift */; };
DB5B70851B2A1963006043BD /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70841B2A1963006043BD /* TestView.swift */; };
DB5B70871B2B8816006043BD /* TZStackViewTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */; };
Expand Down Expand Up @@ -67,6 +69,8 @@
5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TZStackViewTests.swift; sourceTree = "<group>"; };
5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5F50EFD0C46B7C7F989F10E1 /* TZStackViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TZStackViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7E44D3911C906B1800A3D266 /* TZFuncAnimationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZFuncAnimationDelegate.swift; sourceTree = "<group>"; };
7E44D3931C906C3F00A3D266 /* HidingAnimationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HidingAnimationTests.swift; sourceTree = "<group>"; };
A45441BF1B9B6D71002452BA /* TZStackView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TZStackView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A45441C11B9B6D71002452BA /* TZStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TZStackView.h; sourceTree = "<group>"; };
A45441C31B9B6D71002452BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -128,6 +132,7 @@
isa = PBXGroup;
children = (
5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */,
7E44D3931C906C3F00A3D266 /* HidingAnimationTests.swift */,
DB5B70841B2A1963006043BD /* TestView.swift */,
DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */,
5F50E05A91CC731E5AFD4E94 /* Supporting Files */,
Expand Down Expand Up @@ -174,6 +179,7 @@
A45441CD1B9B6D9C002452BA /* TZStackView.swift */,
A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */,
A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */,
7E44D3911C906B1800A3D266 /* TZFuncAnimationDelegate.swift */,
A45441D41B9B6E46002452BA /* Supporting Files */,
);
path = TZStackView;
Expand Down Expand Up @@ -326,6 +332,7 @@
DB5B70871B2B8816006043BD /* TZStackViewTestCase.swift in Sources */,
DB5B70851B2A1963006043BD /* TestView.swift in Sources */,
5F50EAD959E8ACC5929DBD75 /* NSLayoutConstraintExtension.swift in Sources */,
7E44D3941C906C3F00A3D266 /* HidingAnimationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -346,6 +353,7 @@
files = (
A45441D21B9B6D9C002452BA /* TZStackViewAlignment.swift in Sources */,
A45441D01B9B6D9C002452BA /* TZSpacerView.swift in Sources */,
7E44D3921C906B1800A3D266 /* TZFuncAnimationDelegate.swift in Sources */,
A45441D11B9B6D9C002452BA /* TZStackView.swift in Sources */,
A45441D31B9B6D9C002452BA /* TZStackViewDistribution.swift in Sources */,
);
Expand Down
31 changes: 31 additions & 0 deletions TZStackView/TZFuncAnimationDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// TZAnimationDelegate.swift
// TZStackView
//
// Created by CosynPa on 3/5/16.
// Copyright © 2016 Tom van Zummeren. All rights reserved.
//

import Foundation
import QuartzCore

class TZFuncAnimationDelegate {
private var completionFunc: ((CAAnimation, Bool) -> ())?

init(completion: (CAAnimation, Bool) -> ()) {
completionFunc = completion
}

@objc func animationDidStart(anim: CAAnimation) {

}

@objc func animationDidStop(anim: CAAnimation, finished: Bool) {
completionFunc?(anim, finished)
}

func cancel(anim: CAAnimation) {
completionFunc?(anim, false)
completionFunc = nil
}
}
96 changes: 50 additions & 46 deletions TZStackView/TZStackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@

import UIKit

struct TZAnimationDidStopQueueEntry: Equatable {
let view: UIView
let hidden: Bool
}

func ==(lhs: TZAnimationDidStopQueueEntry, rhs: TZAnimationDidStopQueueEntry) -> Bool {
return lhs.view === rhs.view
}

public class TZStackView: UIView {

public var distribution: TZStackViewDistribution = .Fill {
Expand Down Expand Up @@ -51,8 +42,6 @@ public class TZStackView: UIView {

private var spacerViews = [UIView]()

private var animationDidStopQueueEntries = [TZAnimationDidStopQueueEntry]()

private var registeredKvoSubviews = [UIView]()

private var animatingToHiddenViews = [UIView]()
Expand Down Expand Up @@ -84,7 +73,7 @@ public class TZStackView: UIView {
}

private func addHiddenListener(view: UIView) {
view.addObserver(self, forKeyPath: "hidden", options: [.Old, .New], context: &kvoContext)
view.addObserver(self, forKeyPath: "hidden", options: [.New], context: &kvoContext)
registeredKvoSubviews.append(view)
}

Expand All @@ -96,55 +85,70 @@ public class TZStackView: UIView {
}

public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let view = object as? UIView, change = change where keyPath == "hidden" {
if let view = object as? UIView where keyPath == "hidden" {
let hiddenCallbackKey = "TZSV-hidden-callback"

let hidden = view.hidden
let previousValue = change["old"] as! Bool
if hidden == previousValue {
return

let previouseKeys = Set(view.layer.animationKeys() ?? [])

if let callbackAnimation = view.layer.animationForKey(hiddenCallbackKey) {
(callbackAnimation.delegate as! TZFuncAnimationDelegate).cancel(callbackAnimation)
view.layer.removeAnimationForKey(hiddenCallbackKey)
}

// Canceling the previouse callback will reset the hidden property, so we set it back without triggering KVO
view.layer.hidden = hidden
if hidden {
animatingToHiddenViews.append(view)
}

// Perform the animation
setNeedsUpdateConstraints()
setNeedsLayout()
layoutIfNeeded()

removeHiddenListener(view)
view.hidden = false

if let _ = view.layer.animationKeys() {
UIView.setAnimationDelegate(self)
animationDidStopQueueEntries.insert(TZAnimationDidStopQueueEntry(view: view, hidden: hidden), atIndex: 0)
UIView.setAnimationDidStopSelector("hiddenAnimationStopped")
let afterKeys = Set(view.layer.animationKeys() ?? [])
let addedKeys = afterKeys.subtract(previouseKeys)

view.layer.hidden = false // This will set view.hidden without triggering KVO

let animationFinishFunc = { [weak self, weak view] () in
view?.layer.hidden = hidden
if let selv = self, strongView = view {
if let index = selv.animatingToHiddenViews.indexOf(strongView) {
selv.animatingToHiddenViews.removeAtIndex(index)
}
}
}

// Try to find the animation object associated with the hidding process.
if let hidingAnimation = addedKeys.first.flatMap({ key in view.layer.animationForKey(key)}) {
let callbackAnimation = CAAnimationGroup()
callbackAnimation.animations = []
callbackAnimation.delegate = TZFuncAnimationDelegate { _ in
animationFinishFunc()
}
animation(callbackAnimation, copyTimingFrom: hidingAnimation, superLayer: view.layer)

view.layer.addAnimation(callbackAnimation, forKey: hiddenCallbackKey)
} else {
didFinishSettingHiddenValue(view, hidden: hidden)
animationFinishFunc()
}
}
}

private func didFinishSettingHiddenValue(arrangedSubview: UIView, hidden: Bool) {
arrangedSubview.hidden = hidden
if let index = animatingToHiddenViews.indexOf(arrangedSubview) {
animatingToHiddenViews.removeAtIndex(index)
}
addHiddenListener(arrangedSubview)
}

func hiddenAnimationStopped() {
var queueEntriesToRemove = [TZAnimationDidStopQueueEntry]()
for entry in animationDidStopQueueEntries {
let view = entry.view
if view.layer.animationKeys() == nil {
didFinishSettingHiddenValue(view, hidden: entry.hidden)
queueEntriesToRemove.append(entry)
}
}
for entry in queueEntriesToRemove {
if let index = animationDidStopQueueEntries.indexOf(entry) {
animationDidStopQueueEntries.removeAtIndex(index)
}
}
private func animation(animation: CAAnimation, copyTimingFrom other: CAAnimation, superLayer: CALayer) {
// 1. When a CAAnimation is added to a layer, its beginTime will be adjusted to current time if its beginTime is 0.
// 2. The beginTime of the animation objects added by the system, is the block animation's delay time. So if it's non zero, it should be converted to the layer's time space
animation.beginTime = other.beginTime == 0 ? 0 : superLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) + other.beginTime
animation.duration = other.duration
animation.speed = other.speed
animation.timeOffset = other.timeOffset
animation.repeatCount = other.repeatCount
animation.repeatDuration = other.repeatDuration
animation.autoreverses = other.autoreverses
animation.fillMode = other.fillMode
}

public func addArrangedSubview(view: UIView) {
Expand Down