// // KeypathSearchableExtension.swift // lottie-swift // // Created by Brandon Withrow on 2/4/19. // import Foundation import QuartzCore extension KeypathSearchable { func animatorNodes(for keyPath: AnimationKeypath) -> [AnimatorNode]? { // Make sure there is a current key path. guard let currentKey = keyPath.currentKey else { return nil } // Now try popping the keypath for wildcard / child search guard let nextKeypath = keyPath.popKey(keypathName) else { // We may be on the final keypath. Check for match. if let node = self as? AnimatorNode, currentKey.equalsKeypath(keypathName) { // This is the final keypath and matches self. Return.s return [node] } /// Nope. Stop Search return nil } var results: [AnimatorNode] = [] if let node = self as? AnimatorNode, nextKeypath.currentKey == nil { // Keypath matched self and was the final keypath. results.append(node) } for childNode in childKeypaths { // Check if the child has any nodes matching the next keypath. if let foundNodes = childNode.animatorNodes(for: nextKeypath) { results.append(contentsOf: foundNodes) } // In this case the current key is fuzzy, and both child and self match the next keyname. Keep digging! if currentKey.keyPathType == .fuzzyWildcard, let nextKeypath = keyPath.nextKeypath, nextKeypath.equalsKeypath(childNode.keypathName), let foundNodes = childNode.animatorNodes(for: keyPath) { results.append(contentsOf: foundNodes) } } guard results.count > 0 else { return nil } return results } func nodeProperties(for keyPath: AnimationKeypath) -> [AnyNodeProperty]? { guard let nextKeypath = keyPath.popKey(keypathName) else { /// Nope. Stop Search return nil } /// Keypath matches in some way. Continue the search. var results: [AnyNodeProperty] = [] /// Check if we have a property keypath yet if let propertyKey = nextKeypath.propertyKey, let property = keypathProperties[propertyKey] { /// We found a property! results.append(property) } if nextKeypath.nextKeypath != nil { /// Now check child keypaths. for child in childKeypaths { if let childProperties = child.nodeProperties(for: nextKeypath) { results.append(contentsOf: childProperties) } } } guard results.count > 0 else { return nil } return results } func layer(for keyPath: AnimationKeypath) -> CALayer? { if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) { /// We found our layer! return keypathLayer } guard let nextKeypath = keyPath.popKey(keypathName) else { /// Nope. Stop Search return nil } /// Now check child keypaths. for child in childKeypaths { if let layer = child.layer(for: nextKeypath) { return layer } } return nil } func logKeypaths(for keyPath: AnimationKeypath?, logger: LottieLogger) { let newKeypath: AnimationKeypath if let previousKeypath = keyPath { newKeypath = previousKeypath.appendingKey(keypathName) } else { newKeypath = AnimationKeypath(keys: [keypathName]) } logger.info(newKeypath.fullPath) for key in keypathProperties.keys { logger.info(newKeypath.appendingKey(key).fullPath) } for child in childKeypaths { child.logKeypaths(for: newKeypath, logger: logger) } } } extension AnimationKeypath { var currentKey: String? { keys.first } var nextKeypath: String? { guard keys.count > 1 else { return nil } return keys[1] } var propertyKey: String? { if nextKeypath == nil { /// There are no more keypaths. This is a property key. return currentKey } if keys.count == 2, currentKey?.keyPathType == .fuzzyWildcard { /// The next keypath is the last and the current is a fuzzy key. return nextKeypath } return nil } var fullPath: String { keys.joined(separator: ".") } // Pops the top keypath from the stack if the keyname matches. func popKey(_ keyname: String) -> AnimationKeypath? { guard let currentKey = currentKey, currentKey.equalsKeypath(keyname), keys.count > 1 else { // Current key either doesnt match or we are on the last key. return nil } // Pop the keypath from the stack and return the new stack. let newKeys: [String] if currentKey.keyPathType == .fuzzyWildcard { /// Dont remove if current key is a fuzzy wildcard, and if the next keypath doesnt equal keypathname if let nextKeypath = nextKeypath, nextKeypath.equalsKeypath(keyname) { /// Remove next two keypaths. This keypath breaks the wildcard. var oldKeys = keys oldKeys.remove(at: 0) oldKeys.remove(at: 0) newKeys = oldKeys } else { newKeys = keys } } else { var oldKeys = keys oldKeys.remove(at: 0) newKeys = oldKeys } return AnimationKeypath(keys: newKeys) } func appendingKey(_ key: String) -> AnimationKeypath { var newKeys = keys newKeys.append(key) return AnimationKeypath(keys: newKeys) } } extension String { var keyPathType: KeyType { switch self { case "*": return .wildcard case "**": return .fuzzyWildcard default: return .specific } } func equalsKeypath(_ keyname: String) -> Bool { if keyPathType == .wildcard || keyPathType == .fuzzyWildcard { return true } if self == keyname { return true } if let index = firstIndex(of: "*") { // Wildcard search. let prefix = String(self.prefix(upTo: index)) let suffix = String(self.suffix(from: self.index(after: index))) if prefix.count > 0 { // Match prefix. if keyname.count < prefix.count { return false } let testPrefix = String(keyname.prefix(upTo: keyname.index(keyname.startIndex, offsetBy: prefix.count))) if testPrefix != prefix { // Prefix doesnt match return false } } if suffix.count > 0 { // Match suffix. if keyname.count < suffix.count { // Suffix doesnt match return false } let index = keyname.index(keyname.endIndex, offsetBy: -suffix.count) let testSuffix = String(keyname.suffix(from: index)) if testSuffix != suffix { return false } } return true } return false } } // MARK: - KeyType enum KeyType { case specific case wildcard case fuzzyWildcard }