diff --git a/AnimationRestoration.xcodeproj/project.pbxproj b/AnimationRestoration.xcodeproj/project.pbxproj index 0553458..afecf5b 100644 --- a/AnimationRestoration.xcodeproj/project.pbxproj +++ b/AnimationRestoration.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2EA1DA391E96E26B00255843 /* AnimationPreservingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA1DA381E96E26B00255843 /* AnimationPreservingView.swift */; }; + 2EA1DA3B1E96E2A700255843 /* ObjectAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA1DA3A1E96E2A700255843 /* ObjectAssociation.swift */; }; 2ECCB76F1E96C01F00CD12C6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ECCB76E1E96C01F00CD12C6 /* AppDelegate.swift */; }; 2ECCB7711E96C01F00CD12C6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ECCB7701E96C01F00CD12C6 /* ViewController.swift */; }; 2ECCB7741E96C01F00CD12C6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2ECCB7721E96C01F00CD12C6 /* Main.storyboard */; }; @@ -15,6 +17,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 2EA1DA381E96E26B00255843 /* AnimationPreservingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationPreservingView.swift; sourceTree = "<group>"; }; + 2EA1DA3A1E96E2A700255843 /* ObjectAssociation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectAssociation.swift; sourceTree = "<group>"; }; 2ECCB76B1E96C01F00CD12C6 /* AnimationRestoration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnimationRestoration.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2ECCB76E1E96C01F00CD12C6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 2ECCB7701E96C01F00CD12C6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; }; @@ -54,6 +58,8 @@ 2ECCB76D1E96C01F00CD12C6 /* AnimationRestoration */ = { isa = PBXGroup; children = ( + 2EA1DA3A1E96E2A700255843 /* ObjectAssociation.swift */, + 2EA1DA381E96E26B00255843 /* AnimationPreservingView.swift */, 2ECCB76E1E96C01F00CD12C6 /* AppDelegate.swift */, 2ECCB7701E96C01F00CD12C6 /* ViewController.swift */, 2ECCB7721E96C01F00CD12C6 /* Main.storyboard */, @@ -136,7 +142,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2EA1DA391E96E26B00255843 /* AnimationPreservingView.swift in Sources */, 2ECCB7711E96C01F00CD12C6 /* ViewController.swift in Sources */, + 2EA1DA3B1E96E2A700255843 /* ObjectAssociation.swift in Sources */, 2ECCB76F1E96C01F00CD12C6 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -301,6 +309,7 @@ 2ECCB77F1E96C01F00CD12C6 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/AnimationRestoration/AnimationPreservingView.swift b/AnimationRestoration/AnimationPreservingView.swift new file mode 100644 index 0000000..d80f6d4 --- /dev/null +++ b/AnimationRestoration/AnimationPreservingView.swift @@ -0,0 +1,141 @@ +// +// AnimationPreservingView.swift +// AnimationPreservingView +// +// Created by Wojciech Nagrodzki on 08/03/2017. +// Copyright © 2017 Wojciech Nagrodzki. All rights reserved. +// + +import UIKit + + +/// The `AnimationPreservingView` class keeps it's layer tree animations safe from being removed. +/// There are two cases when `CAAnimation` can be removed from `CALayer` automatically: +/// - when application goes to background +/// - when view backed by the layer is removed from window +class AnimationPreservingView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + registerForNotifications() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + registerForNotifications() + } + + override func willMove(toWindow newWindow: UIWindow?) { + super.willMove(toWindow: newWindow) + if newWindow == nil { layer.storeAnimations() } + } + + override func didMoveToWindow() { + super.didMoveToWindow() + if window != nil { layer.restoreAnimations() } + } +} + + +extension AnimationPreservingView { + + fileprivate func registerForNotifications() { + + NotificationCenter.default.addObserver(self, + selector: #selector(AnimationPreservingView.applicationWillResignActive), + name: .UIApplicationWillResignActive, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(AnimationPreservingView.applicationDidBecomeActive), + name: .UIApplicationDidBecomeActive, + object: nil) + } + + @objc private func applicationWillResignActive() { + + guard window != nil else { return } + layer.storeAnimations() + } + + @objc private func applicationDidBecomeActive() { + + guard window != nil else { return } + layer.restoreAnimations() + } +} + + +extension CALayer { + + private static let association = ObjectAssociation<NSDictionary>() + + private var animationsStorage: [String: CAAnimation] { + + get { return CALayer.association[self] as? [String : CAAnimation] ?? [:] } + set { CALayer.association[self] = newValue as NSDictionary } + } + + /// Returns a dictionary of copies of animations currently attached to the layer along with their's keys. + private var animationsForKeys: [String: CAAnimation] { + + guard let keys = animationKeys() else { return [:] } + return keys.reduce([:], { + var result = $0 + let key = $1 + result[key] = (animation(forKey: key)!.copy() as! CAAnimation) + return result + }) + } + + /// Pauses the layer tree and stores it's animations. + func storeAnimations() { + + pause() + depositAnimations() + } + + /// Resumes the layer tree and restores it's animations. + func restoreAnimations() { + + withdrawAnimations() + resume() + } + + private func depositAnimations() { + + animationsStorage = animationsForKeys + sublayers?.forEach { $0.depositAnimations() } + } + + private func withdrawAnimations() { + + sublayers?.forEach { $0.withdrawAnimations() } + animationsStorage.forEach { add($0.value, forKey: $0.key) } + animationsStorage = [:] + } +} + + +extension CALayer { + + /// Pauses animations in layer tree. + /// - note: [Technical Q&A QA1673](https://developer.apple.com/library/ios/qa/qa1673/_index.html#//apple_ref/doc/uid/DTS40010053) + fileprivate func pause() { + + let pausedTime = convertTime(CACurrentMediaTime(), from: nil) + speed = 0.0; + timeOffset = pausedTime; + } + + /// Resumes animations in layer tree. + /// - note: [Technical Q&A QA1673](https://developer.apple.com/library/ios/qa/qa1673/_index.html#//apple_ref/doc/uid/DTS40010053) + fileprivate func resume() { + + let pausedTime = timeOffset; + speed = 1.0; + timeOffset = 0.0; + beginTime = 0.0; + let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime; + beginTime = timeSincePause; + } +} diff --git a/AnimationRestoration/ObjectAssociation.swift b/AnimationRestoration/ObjectAssociation.swift new file mode 100644 index 0000000..4c0a7e5 --- /dev/null +++ b/AnimationRestoration/ObjectAssociation.swift @@ -0,0 +1,55 @@ +// +// MIT License +// +// Copyright (c) 2017 Trifork Kraków Office +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Wraps Objective-C runtime object associations. Assumes one instance per association key. +/// +/// Example usage when simulating stored property with a computed one: +/// +/// extension SomeType { +/// private static let association = ObjectAssociation<NSObject>() +/// var simulatedProperty: NSObject? { +/// get { return SomeType.association[self] } +/// set { SomeType.association[self] = newValue } +/// } +/// } +public final class ObjectAssociation<T: AnyObject> { + + private let policy: objc_AssociationPolicy + + /// - Parameter policy: An association policy that will be used when linking objects. + public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) { + + self.policy = policy + } + + /// Accesses associated object. + /// - Parameter index: An object whose associated object is to be accessed. + public subscript(index: AnyObject) -> T? { + + get { return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? } + set { objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy) } + } +}