@@ -176,4 +176,184 @@ class UIKitBehavioralTests: XCTestCase {
176176 " Expected \( keyPath. rawValue) not to generate any animations. " )
177177 }
178178 }
179+
180+ // MARK: .beginFromCurrentState option behavior
181+ //
182+ // The following tests indicate that UIKit treats .beginFromCurrentState differently depending
183+ // on the key path being animated. This difference is in line with whether or not a key path is
184+ // animated additively or not.
185+ //
186+ // > See testSomePropertiesImplicitlyAnimateAdditively and
187+ // > testSomePropertiesImplicitlyAnimateButNotAdditively for a list of which key paths are
188+ // > animated which way.
189+ //
190+ // Notably, ONLY non-additive key paths are affected by the beginFromCurrentState option. This
191+ // likely became the case starting in iOS 8 when additive animations were enabled by default.
192+ // Additive animations will always animate additively regardless of whether or not you provide
193+ // this flag.
194+
195+ func testDefaultsAnimatesOpacityNonAdditivelyFromItsModelLayerState( ) {
196+ UIView . animate ( withDuration: 0.1 ) {
197+ self . view. alpha = 0.5
198+ }
199+
200+ RunLoop . main. run ( until: . init( timeIntervalSinceNow: 0.01 ) )
201+
202+ let initialValue = self . view. layer. opacity
203+
204+ UIView . animate ( withDuration: 0.1 ) {
205+ self . view. alpha = 0.2
206+ }
207+
208+ XCTAssertNotNil ( view. layer. animationKeys ( ) ,
209+ " Expected an animation to be added, but none were found. " )
210+ guard let animationKeys = view. layer. animationKeys ( ) else {
211+ return
212+ }
213+ XCTAssertEqual ( animationKeys. count, 1 ,
214+ " Expected only one animation to be added, but the following were found: "
215+ + " \( animationKeys) . " )
216+ guard let key = animationKeys. first,
217+ let animation = view. layer. animation ( forKey: key) as? CABasicAnimation else {
218+ return
219+ }
220+
221+ XCTAssertFalse ( animation. isAdditive, " Expected the animation not to be additive, but it was. " )
222+
223+ XCTAssertTrue ( animation. fromValue is Float ,
224+ " The animation's from value was not a number type: "
225+ + String( describing: animation. fromValue) )
226+ guard let fromValue = animation. fromValue as? Float else {
227+ return
228+ }
229+
230+ XCTAssertEqualWithAccuracy ( fromValue, initialValue, accuracy: 0.0001 ,
231+ " Expected the animation to start from \( initialValue) , "
232+ + " but it did not. " )
233+ }
234+
235+ func testBeginFromCurrentStateAnimatesOpacityNonAdditivelyFromItsPresentationLayerState( ) {
236+ UIView . animate ( withDuration: 0.1 ) {
237+ self . view. alpha = 0.5
238+ }
239+
240+ RunLoop . main. run ( until: . init( timeIntervalSinceNow: 0.01 ) )
241+
242+ let initialValue = self . view. layer. presentation ( ) !. opacity
243+
244+ UIView . animate ( withDuration: 0.1 , delay: 0 , options: . beginFromCurrentState, animations: {
245+ self . view. alpha = 0.2
246+ } , completion: nil )
247+
248+ XCTAssertNotNil ( view. layer. animationKeys ( ) ,
249+ " Expected an animation to be added, but none were found. " )
250+ guard let animationKeys = view. layer. animationKeys ( ) else {
251+ return
252+ }
253+ XCTAssertEqual ( animationKeys. count, 1 ,
254+ " Expected only one animation to be added, but the following were found: "
255+ + " \( animationKeys) . " )
256+ guard let key = animationKeys. first,
257+ let animation = view. layer. animation ( forKey: key) as? CABasicAnimation else {
258+ return
259+ }
260+
261+ XCTAssertFalse ( animation. isAdditive, " Expected the animation not to be additive, but it was. " )
262+
263+ XCTAssertTrue ( animation. fromValue is Float ,
264+ " The animation's from value was not a number type: "
265+ + String( describing: animation. fromValue) )
266+ guard let fromValue = animation. fromValue as? Float else {
267+ return
268+ }
269+
270+ XCTAssertEqualWithAccuracy ( fromValue, initialValue, accuracy: 0.0001 ,
271+ " Expected the animation to start from \( initialValue) , "
272+ + " but it did not. " )
273+ }
274+
275+ func testDefaultsAnimatesPositionAdditivelyFromItsModelLayerState( ) {
276+ UIView . animate ( withDuration: 0.1 ) {
277+ self . view. layer. position = CGPoint ( x: 100 , y: self . view. layer. position. y)
278+ }
279+
280+ RunLoop . main. run ( until: . init( timeIntervalSinceNow: 0.01 ) )
281+
282+ let initialValue = self . view. layer. position
283+
284+ UIView . animate ( withDuration: 0.1 ) {
285+ self . view. layer. position = CGPoint ( x: 20 , y: self . view. layer. position. y)
286+ }
287+
288+ let displacement = initialValue. x - self . view. layer. position. x
289+
290+ XCTAssertNotNil ( view. layer. animationKeys ( ) ,
291+ " Expected an animation to be added, but none were found. " )
292+ guard let animationKeys = view. layer. animationKeys ( ) else {
293+ return
294+ }
295+ XCTAssertEqual ( animationKeys. count, 2 ,
296+ " Expected two animations to be added, but the following were found: "
297+ + " \( animationKeys) . " )
298+ guard let key = animationKeys. first ( where: { $0 != " position " } ) ,
299+ let animation = view. layer. animation ( forKey: key) as? CABasicAnimation else {
300+ return
301+ }
302+
303+ XCTAssertTrue ( animation. isAdditive, " Expected the animation to be additive, but it wasn't. " )
304+
305+ XCTAssertTrue ( animation. fromValue is CGPoint ,
306+ " The animation's from value was not a point type: "
307+ + String( describing: animation. fromValue) )
308+ guard let fromValue = animation. fromValue as? CGPoint else {
309+ return
310+ }
311+
312+ XCTAssertEqualWithAccuracy ( fromValue. x, displacement, accuracy: 0.0001 ,
313+ " Expected the animation to have a delta of \( displacement) , "
314+ + " but it did not. " )
315+ }
316+
317+ func testBeginFromCurrentStateAnimatesPositionAdditivelyFromItsModelLayerState( ) {
318+ UIView . animate ( withDuration: 0.1 ) {
319+ self . view. layer. position = CGPoint ( x: 100 , y: self . view. layer. position. y)
320+ }
321+
322+ RunLoop . main. run ( until: . init( timeIntervalSinceNow: 0.01 ) )
323+
324+ let initialValue = self . view. layer. position
325+
326+ UIView . animate ( withDuration: 0.1 , delay: 0 , options: . beginFromCurrentState, animations: {
327+ self . view. layer. position = CGPoint ( x: 20 , y: self . view. layer. position. y)
328+ } , completion: nil )
329+
330+ let displacement = initialValue. x - self . view. layer. position. x
331+
332+ XCTAssertNotNil ( view. layer. animationKeys ( ) ,
333+ " Expected an animation to be added, but none were found. " )
334+ guard let animationKeys = view. layer. animationKeys ( ) else {
335+ return
336+ }
337+ XCTAssertEqual ( animationKeys. count, 2 ,
338+ " Expected two animations to be added, but the following were found: "
339+ + " \( animationKeys) . " )
340+ guard let key = animationKeys. first ( where: { $0 != " position " } ) ,
341+ let animation = view. layer. animation ( forKey: key) as? CABasicAnimation else {
342+ return
343+ }
344+
345+ XCTAssertTrue ( animation. isAdditive, " Expected the animation to be additive, but it wasn't. " )
346+
347+ XCTAssertTrue ( animation. fromValue is CGPoint ,
348+ " The animation's from value was not a point type: "
349+ + String( describing: animation. fromValue) )
350+ guard let fromValue = animation. fromValue as? CGPoint else {
351+ return
352+ }
353+
354+ XCTAssertEqualWithAccuracy ( fromValue. x, displacement, accuracy: 0.0001 ,
355+ " Expected the animation to have a delta of \( displacement) , "
356+ + " but it did not. " )
357+ }
358+
179359}
0 commit comments