From 5f013c4df4386978e5be47d1f6bbd83dc14352b5 Mon Sep 17 00:00:00 2001 From: CosynPa Date: Wed, 9 Mar 2016 22:32:25 +0800 Subject: [PATCH 1/3] Add a animation delegate class --- TZStackView.xcodeproj/project.pbxproj | 12 ++++++--- TZStackView/TZFuncAnimationDelegate.swift | 31 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 TZStackView/TZFuncAnimationDelegate.swift diff --git a/TZStackView.xcodeproj/project.pbxproj b/TZStackView.xcodeproj/project.pbxproj index b9e35a1..cf969b2 100644 --- a/TZStackView.xcodeproj/project.pbxproj +++ b/TZStackView.xcodeproj/project.pbxproj @@ -13,13 +13,14 @@ 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 */; }; 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 */; }; @@ -67,6 +68,7 @@ 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TZStackViewTests.swift; sourceTree = ""; }; 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; A45441C31B9B6D71002452BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -174,6 +176,7 @@ A45441CD1B9B6D9C002452BA /* TZStackView.swift */, A45441CE1B9B6D9C002452BA /* TZStackViewAlignment.swift */, A45441CF1B9B6D9C002452BA /* TZStackViewDistribution.swift */, + 7E44D3911C906B1800A3D266 /* TZFuncAnimationDelegate.swift */, A45441D41B9B6E46002452BA /* Supporting Files */, ); path = TZStackView; @@ -346,6 +349,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 */, ); diff --git a/TZStackView/TZFuncAnimationDelegate.swift b/TZStackView/TZFuncAnimationDelegate.swift new file mode 100644 index 0000000..e963630 --- /dev/null +++ b/TZStackView/TZFuncAnimationDelegate.swift @@ -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 + } +} \ No newline at end of file From b45badcaccdc5f21d2491304df2cb01cca3b3058 Mon Sep 17 00:00:00 2001 From: CosynPa Date: Wed, 9 Mar 2016 22:35:18 +0800 Subject: [PATCH 2/3] Use a dummy animation object to deal with the completion callback, fixing #70 and #50 --- TZStackView/TZStackView.swift | 96 ++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/TZStackView/TZStackView.swift b/TZStackView/TZStackView.swift index 2fd77a8..3c77d7d 100755 --- a/TZStackView/TZStackView.swift +++ b/TZStackView/TZStackView.swift @@ -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 { @@ -51,8 +42,6 @@ public class TZStackView: UIView { private var spacerViews = [UIView]() - private var animationDidStopQueueEntries = [TZAnimationDidStopQueueEntry]() - private var registeredKvoSubviews = [UIView]() private var animatingToHiddenViews = [UIView]() @@ -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) } @@ -96,55 +85,70 @@ public class TZStackView: UIView { } public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { - 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) { From ad4fedc1f0039a0efca89f19860f5885de401cd1 Mon Sep 17 00:00:00 2001 From: CosynPa Date: Wed, 9 Mar 2016 22:36:34 +0800 Subject: [PATCH 3/3] Add tests --- TZStackView.xcodeproj/project.pbxproj | 4 + TZStackViewTests/HidingAnimationTests.swift | 290 ++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 TZStackViewTests/HidingAnimationTests.swift diff --git a/TZStackView.xcodeproj/project.pbxproj b/TZStackView.xcodeproj/project.pbxproj index cf969b2..5e0aab0 100644 --- a/TZStackView.xcodeproj/project.pbxproj +++ b/TZStackView.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 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, ); }; }; @@ -69,6 +70,7 @@ 5F50EF54F01A3A6938C6CEA1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 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 = ""; }; + 7E44D3931C906C3F00A3D266 /* HidingAnimationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HidingAnimationTests.swift; sourceTree = ""; }; 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 = ""; }; A45441C31B9B6D71002452BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -130,6 +132,7 @@ isa = PBXGroup; children = ( 5F50EDEB0947F99E67140FC6 /* TZStackViewTests.swift */, + 7E44D3931C906C3F00A3D266 /* HidingAnimationTests.swift */, DB5B70841B2A1963006043BD /* TestView.swift */, DB5B70861B2B8816006043BD /* TZStackViewTestCase.swift */, 5F50E05A91CC731E5AFD4E94 /* Supporting Files */, @@ -329,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; }; diff --git a/TZStackViewTests/HidingAnimationTests.swift b/TZStackViewTests/HidingAnimationTests.swift new file mode 100644 index 0000000..78d3f44 --- /dev/null +++ b/TZStackViewTests/HidingAnimationTests.swift @@ -0,0 +1,290 @@ +// +// HidingAnimationTests.swift +// TZStackView +// +// Created by CosynPa on 3/6/16. +// Copyright © 2016 Tom van Zummeren. All rights reserved. +// + +import Foundation +import UIKit +import XCTest +import TZStackView + +class HidingAnimationTests: TZStackViewTestCase { + var uiTestView: UIView! + var tzTestView: UIView! + + override func createTestViews() -> [UIView] { + var views = [UIView]() + for i in 0 ..< 5 { + views.append(TestView(index: i, size: CGSize(width: 100 * (i + 1), height: 100 * (i + 1)))) + } + return views + } + + override func setUp() { + super.setUp() + uiTestView = uiStackView.arrangedSubviews.last! + tzTestView = tzStackView.arrangedSubviews.last! + } + + // If you are not animating the hidden property, the hidden property should be set immediately + func testNonAnimatingHidden() { + let expectation = expectationWithDescription("delay") + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + self.uiTestView.hidden = true + self.tzTestView.hidden = true + + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + XCTAssert(self.uiTestView.layer.hidden) + XCTAssert(self.tzTestView.layer.hidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + // If you are not animating the hidden property, the hidden property should be set immediately + func testNonAnimatingHiddenWithOther() { + let expectation = expectationWithDescription("delay") + + uiTestView.backgroundColor = UIColor.clearColor() + tzTestView.backgroundColor = UIColor.clearColor() + UIView.animateWithDuration(2) { + self.uiTestView.backgroundColor = UIColor.greenColor() + self.tzTestView.backgroundColor = UIColor.greenColor() + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + self.uiTestView.hidden = true + self.tzTestView.hidden = true + + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + XCTAssert(self.uiTestView.layer.hidden) + XCTAssert(self.tzTestView.layer.hidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + func animationHiddenWithDelay(delay: NSTimeInterval) { + let expectation = expectationWithDescription("delay") + + let duration = 1.0 + + UIView.animateWithDuration(duration, delay: delay, options: [], + animations: { () -> Void in + self.uiTestView.hidden = true + self.tzTestView.hidden = true + + // Note uiTestView.hidden == true, tzTestView.hidden == false + + // The presentation should not be hidden. + XCTAssert(!self.uiTestView.layer.hidden) + XCTAssert(!self.tzTestView.layer.hidden) + + }, completion: { _ in + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + XCTAssert(self.uiTestView.layer.hidden) + XCTAssert(self.tzTestView.layer.hidden) + }) + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64((duration + delay + 0.2) * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + XCTAssert(self.uiTestView.layer.hidden) + XCTAssert(self.tzTestView.layer.hidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64((duration + delay + 0.4) * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + func testAnimatingHidden() { + animationHiddenWithDelay(0) + } + + func testAnimatingHiddenWithDelay() { + animationHiddenWithDelay(1) + } + + func testAnimatingHiddenWithOther() { + let expectation = expectationWithDescription("delay") + + UIView.animateWithDuration(1) { + self.uiTestView.hidden = true + self.tzTestView.hidden = true + } + + uiTestView.backgroundColor = UIColor.clearColor() + tzTestView.backgroundColor = UIColor.clearColor() + UIView.animateWithDuration(2) { + self.uiTestView.backgroundColor = UIColor.greenColor() + self.tzTestView.backgroundColor = UIColor.greenColor() + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + // The view should be hidden after the hiding animation completes even if there are still other animations + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + XCTAssert(self.uiTestView.layer.hidden) + XCTAssert(self.tzTestView.layer.hidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + // The completion callback of an animation should be called + func testHidingAnimationCallback() { + let expectation = expectationWithDescription("delay") + + var uiCompletionCalled = false + var tzCompletionCalled = false + + UIView.animateWithDuration(1, + animations: { + self.uiTestView.hidden = true + }, completion: { _ in + uiCompletionCalled = true + }) + + UIView.animateWithDuration(1, + animations: { + self.tzTestView.hidden = true + }, completion: { _ in + tzCompletionCalled = true + }) + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + XCTAssert(uiCompletionCalled) + XCTAssert(tzCompletionCalled) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.4 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + // The completion callback of an animation should be called when the animation is canceled + func testHidingAnimationCallbackCancel() { + let expectation = expectationWithDescription("delay") + + var uiCompletionCalled = false + var tzCompletionCalled = false + + UIView.animateWithDuration(1, + animations: { + self.uiTestView.hidden = true + }, completion: { finished in + uiCompletionCalled = true + XCTAssert(!finished) + }) + + UIView.animateWithDuration(1, + animations: { + self.tzTestView.hidden = true + }, completion: { finished in + tzCompletionCalled = true + XCTAssert(!finished) + }) + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + // This will cancel the animation + self.uiStackView.removeFromSuperview() + self.tzStackView.removeFromSuperview() + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.7 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + XCTAssert(uiCompletionCalled) + XCTAssert(tzCompletionCalled) + XCTAssert(self.uiTestView.hidden) + XCTAssert(self.tzTestView.hidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.4 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + // When set the hidden property in the middle of an animation, the hidden property should be updated eventually + func hidingAnimationSetAgainFirstHidden(firstHidden: Bool, withAnimation: Bool) { + let expectation = expectationWithDescription("delay") + + uiTestView.hidden = firstHidden + tzTestView.hidden = firstHidden + + UIView.animateWithDuration(2) { + self.uiTestView.hidden = !firstHidden + self.tzTestView.hidden = !firstHidden + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + if !withAnimation { + self.uiTestView.hidden = firstHidden + self.tzTestView.hidden = firstHidden + } else { + UIView.animateWithDuration(2) { + self.uiTestView.hidden = firstHidden + self.tzTestView.hidden = firstHidden + + // Animating, the presentation is not hidden + XCTAssert(!self.uiTestView.layer.hidden) + XCTAssert(!self.tzTestView.layer.hidden) + } + } + + // Note, here we don't expect the hidden property to be the right value even when without animation, + } + + let endTime = !withAnimation ? 2.2 : 3.2 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(endTime * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + XCTAssert(self.uiTestView.hidden == firstHidden) + XCTAssert(self.tzTestView.hidden == firstHidden) + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64((endTime + 0.2) * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { + expectation.fulfill() + } + + waitForExpectationsWithTimeout(100, handler: nil) + } + + func testHidingAnimationSetAgainFirstNotHidden() { + hidingAnimationSetAgainFirstHidden(false, withAnimation: false) + } + + func testHidingAnimationSetAgainFirstHidden() { + hidingAnimationSetAgainFirstHidden(true, withAnimation: false) + } + + func testHidingAnimationSetAgainFirstNotHiddenWithAnimation() { + hidingAnimationSetAgainFirstHidden(false, withAnimation: true) + } + + func testHidingAnimationSetAgainFirstHiddenWithAnimation() { + hidingAnimationSetAgainFirstHidden(true, withAnimation: true) + } +} \ No newline at end of file