├── Freezer.podspec ├── LICENSE ├── README.md └── freezer.swift /Freezer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Freezer" 3 | s.version = "1.0.0" 4 | s.summary = "Let your Swift tests travel through time" 5 | 6 | s.description = <<-DESC 7 | Freezer is a library that allows your swift tests to travel through time by mocking NSDate class 8 | DESC 9 | 10 | s.homepage = "https://github.com/Pr0Ger/Freezer" 11 | 12 | s.license = "MIT" 13 | 14 | s.author = { "Sergey Petrov" => "me@pr0ger.org" } 15 | s.social_media_url = "http://twitter.com/Pr0Ger" 16 | 17 | s.source = { :git => "https://github.com/Pr0Ger/Freezer.git", :tag => "#{s.version}" } 18 | 19 | s.ios.deployment_target = '8.0' 20 | s.osx.deployment_target = '10.9' 21 | s.tvos.deployment_target = '9.0' 22 | s.watchos.deployment_target = '2.0' 23 | 24 | s.source_files = "freezer.swift" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergey Petrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Freezer 2 | Freezer is a library that allows your Swift tests to travel through time by mocking `NSDate` class. 3 | 4 | ## Usage 5 | 6 | Once `Freezer.start()` has been invoked, all calls to `NSDate()` or `NSDate(timeIntervalSinceNow: secs)` will return the time that has been frozen. 7 | 8 | ### Helper function 9 | 10 | ```swift 11 | freeze(NSDate(timeIntervalSince1970: 946684800)) { 12 | print(NSDate()) // 2000-01-01 00:00:00 +0000 13 | } 14 | ``` 15 | 16 | ### Raw usage 17 | 18 | ```swift 19 | let freezer = Freezer(to: NSDate(timeIntervalSince1970: 946684800)) 20 | freezer.start() 21 | print(NSDate()) // 2000-01-01 00:00:00 +0000 22 | freezer.stop() 23 | ``` 24 | 25 | ### Time shifting 26 | 27 | Freezer will move you to a specified point in time, but then the time will keep ticking. 28 | 29 | ```swift 30 | timeshift(NSDate(timeIntervalSince1970: 946684800)) { 31 | print(NSDate()) // 2000-01-01 00:00:00 +0000 32 | sleep(2) 33 | print(NSDate()) // 2000-01-01 00:00:02 +0000 34 | } 35 | ``` 36 | 37 | ### Nested calls 38 | 39 | Freezer allows performing nested freezing/shifts 40 | 41 | ```swift 42 | freeze(NSDate(timeIntervalSince1970: 946684800)) { 43 | freeze(NSDate(timeIntervalSince1970: 946684000)) { 44 | freeze(NSDate(timeIntervalSince1970: 946684800)) { 45 | print(NSDate()) // 2000-01-01 00:00:00 +0000 46 | } 47 | print(NSDate()) // 1999-12-31 23:46:40 +0000 48 | } 49 | print(NSDate()) // 2000-01-01 00:00:00 +0000 50 | } 51 | ``` 52 | 53 | ## Installation 54 | 55 | ### CocoaPods 56 | 57 | Just add `pod 'Freezer', '~> 1.0'` to your test target in `Podfile`. 58 | 59 | ### Carthage 60 | 61 | There is no Xcode project, so Carthage will not build a framework for this library. You can still use it, just add `github "Pr0Ger/Freezer" ~> 1.0` to your `Cartfile` and then add `Carthage/Checkout/Freezer/freezer.swift` to your test target. 62 | 63 | ### Manual 64 | 65 | Just copy freezer.swift to your Xcode project and add it to your tests target. Most likely this library will not be updated, unless Apple breaks something by changing an internal implementation of `NSDate`, so this way is good too. 66 | 67 | ## License 68 | 69 | MIT 70 | 71 | -------------------------------------------------------------------------------- /freezer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sergey on 07/05/16. 3 | // 4 | 5 | import Foundation 6 | 7 | @available(iOS, deprecated=10.0) 8 | func _Selector(str: String) -> Selector { 9 | // Now Xcode can fuck off with his suggestion to use #selector 10 | return Selector(str) 11 | } 12 | 13 | enum StartingPoint: Equatable { 14 | case Fixed(NSDate) 15 | case Offset(NSTimeInterval) 16 | } 17 | 18 | func == (lhs: StartingPoint, rhs: StartingPoint) -> Bool { 19 | switch (lhs, rhs) { 20 | case (.Fixed(let lvalue), .Fixed(let rvalue)) where lvalue == rvalue: return true 21 | case (.Offset(let lvalue), .Offset(let rvalue)) where lvalue == rvalue: return true 22 | default: return false 23 | } 24 | } 25 | 26 | extension NSDate { 27 | func newInit() -> NSDate { 28 | return now() 29 | } 30 | 31 | func newInitWithTimeIntervalSinceNow(timeIntervalSinceNow secs: NSTimeInterval) -> NSDate { 32 | return NSDate(timeInterval: secs, sinceDate: now()) 33 | } 34 | 35 | convenience init(timeIntervalSinceRealNow secs: NSTimeInterval) { 36 | // After we swizzle initWithTimeIntervalSinceNow: this method is only way to obtain real date 37 | self.init(timeIntervalSinceNow: secs) 38 | } 39 | 40 | private func now() -> NSDate { 41 | let startingPoint = Freezer.startingPoints.last! 42 | switch startingPoint { 43 | case .Fixed(let date): return date 44 | case .Offset(let interval): return NSDate(timeIntervalSinceRealNow: interval) 45 | } 46 | } 47 | } 48 | 49 | public class Freezer { 50 | private static var oldNSDateInit: IMP! 51 | private static var oldNSDateInitWithTimeIntervalSinceNow: IMP! 52 | 53 | private static var startingPoints: [StartingPoint] = [] 54 | 55 | let startingPoint: StartingPoint 56 | 57 | private(set) var running: Bool = false 58 | 59 | init(to: NSDate) { 60 | self.startingPoint = .Fixed(to) 61 | } 62 | 63 | init(from: NSDate) { 64 | let now = NSDate(timeIntervalSinceRealNow: 0) 65 | self.startingPoint = .Offset(from.timeIntervalSince1970 - now.timeIntervalSince1970) 66 | } 67 | 68 | deinit { 69 | if running { 70 | stop() 71 | } 72 | } 73 | 74 | func start() { 75 | guard !running else { 76 | return 77 | } 78 | 79 | running = true 80 | 81 | if Freezer.startingPoints.count == 0 { 82 | Freezer.oldNSDateInit = replaceImplementation(_Selector("init"), newSelector: _Selector("newInit")) 83 | Freezer.oldNSDateInitWithTimeIntervalSinceNow = 84 | replaceImplementation(_Selector("initWithTimeIntervalSinceNow:"), 85 | newSelector: _Selector("newInitWithTimeIntervalSinceNow:")) 86 | 87 | let initWithRealNow = class_getInstanceMethod(NSClassFromString("__NSPlaceholderDate"), 88 | _Selector("initWithTimeIntervalSinceRealNow:")) 89 | method_setImplementation(initWithRealNow, Freezer.oldNSDateInitWithTimeIntervalSinceNow) 90 | } 91 | 92 | Freezer.startingPoints.append(startingPoint) 93 | } 94 | 95 | func stop() { 96 | guard running else { 97 | return 98 | } 99 | 100 | for (idx, point) in Freezer.startingPoints.enumerate().reverse() { 101 | if point == self.startingPoint { 102 | Freezer.startingPoints.removeAtIndex(idx) 103 | break 104 | } 105 | } 106 | 107 | if Freezer.startingPoints.count == 0 { 108 | restoreImplementation(_Selector("init"), oldImplementation: Freezer.oldNSDateInit) 109 | restoreImplementation(_Selector("initWithTimeIntervalSinceNow:"), oldImplementation: Freezer.oldNSDateInitWithTimeIntervalSinceNow) 110 | } 111 | 112 | running = false 113 | } 114 | 115 | private func replaceImplementation(oldSelector: Selector, newSelector: Selector) -> IMP { 116 | let oldMethod = class_getInstanceMethod(NSClassFromString("__NSPlaceholderDate"), oldSelector) 117 | let oldImplementation = method_getImplementation(oldMethod) 118 | 119 | let newMethod = class_getInstanceMethod(NSDate.self, newSelector) 120 | let newImplementation = method_getImplementation(newMethod) 121 | 122 | method_setImplementation(oldMethod, newImplementation) 123 | 124 | return oldImplementation 125 | } 126 | 127 | private func restoreImplementation(selector: Selector, oldImplementation: IMP) { 128 | let method = class_getInstanceMethod(NSClassFromString("__NSPlaceholderDate"), selector) 129 | method_setImplementation(method, oldImplementation) 130 | } 131 | } 132 | 133 | public func freeze(time: NSDate, @noescape block: () -> ()) { 134 | let freezer = Freezer(to: time) 135 | freezer.start() 136 | block() 137 | freezer.stop() 138 | } 139 | 140 | public func timeshift(from: NSDate, @noescape block: () -> ()) { 141 | let freezer = Freezer(from: from) 142 | freezer.start() 143 | block() 144 | freezer.stop() 145 | } 146 | --------------------------------------------------------------------------------