├── .gitignore ├── LICENSE ├── MHWDirectoryWatcher.podspec ├── MHWDirectoryWatcher ├── MHWDirectoryWatcher.h └── MHWDirectoryWatcher.m ├── MHWDirectoryWatcherExample.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── MHWDirectoryWatcherExample ├── Default-568h@2x.png ├── Default.png ├── Default@2x.png ├── MHWDirectoryWatcherAppDelegate.h ├── MHWDirectoryWatcherAppDelegate.m ├── MHWDirectoryWatcherExample-Info.plist ├── MHWDirectoryWatcherExample-Prefix.pch ├── en.lproj │ └── InfoPlist.strings └── main.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode5 3 | # 4 | # NB: if you are storing "built" products, this WILL NOT WORK, 5 | # and you should use a different .gitignore (or none at all) 6 | # This file is for SOURCE projects, where there are many extra 7 | # files that we want to exclude 8 | # 9 | # For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 10 | # and https://gist.github.com/adamgit/3786883 11 | ######################### 12 | 13 | ##### 14 | # OS X temporary files that should never be committed 15 | 16 | .DS_Store 17 | *.swp 18 | profile 19 | 20 | 21 | #### 22 | # Xcode temporary files that should never be committed 23 | # 24 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 25 | 26 | *~.nib 27 | 28 | 29 | #### 30 | # Xcode build files - 31 | # 32 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 33 | 34 | DerivedData/ 35 | 36 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 37 | 38 | build/ 39 | 40 | 41 | ##### 42 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 43 | # 44 | # This is complicated: 45 | # 46 | # SOMETIMES you need to put this file in version control. 47 | # Apple designed it poorly - if you use "custom executables", they are 48 | # saved in this file. 49 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 50 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 51 | 52 | *.pbxuser 53 | *.mode1v3 54 | *.mode2v3 55 | *.perspectivev3 56 | # NB: also, whitelist the default ones, some projects need to use these 57 | !default.pbxuser 58 | !default.mode1v3 59 | !default.mode2v3 60 | !default.perspectivev3 61 | 62 | 63 | #### 64 | # Xcode 4 - semi-personal settings, often included in workspaces 65 | # 66 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them 67 | # 68 | 69 | xcuserdata 70 | 71 | #### 72 | # XCode 4 workspaces - more detailed 73 | # 74 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 75 | # 76 | # Workspace layout is quite spammy. For reference: 77 | # 78 | # (root)/ 79 | # (project-name).xcodeproj/ 80 | # project.pbxproj 81 | # project.xcworkspace/ 82 | # contents.xcworkspacedata 83 | # xcuserdata/ 84 | # (your name)/xcuserdatad/ 85 | # xcuserdata/ 86 | # (your name)/xcuserdatad/ 87 | # 88 | # 89 | # 90 | # Xcode 4 workspaces - SHARED 91 | # 92 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results 93 | # But if you're going to kill personal workspaces, at least keep the shared ones... 94 | # 95 | # 96 | !xcshareddata 97 | 98 | #### 99 | # XCode 4 build-schemes 100 | # 101 | # PRIVATE ones are stored inside xcuserdata 102 | !xcschemes 103 | 104 | #### 105 | # Xcode 4 - Deprecated classes 106 | # 107 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 108 | # 109 | # We're using source-control, so this is a "feature" that we do not want! 110 | 111 | *.moved-aside 112 | 113 | #### 114 | # Xcode 5 - Source Control files 115 | # 116 | # Xcode 5 introduced a new file type .xccheckout. This files contains VCS metadata 117 | # and should therefore not be checked into the VCS. 118 | 119 | *.xccheckout 120 | 121 | #### 122 | # Temporary IPAs 123 | *.ipa 124 | *.app.dSYM.zip 125 | 126 | #### 127 | # AppCode 128 | .idea/ 129 | 130 | #### 131 | # UNKNOWN: recommended by others, but I can't discover what these files are 132 | # 133 | # ...none. Everything is now explained.: 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2012 Martin Hwasser 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MHWDirectoryWatcher.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MHWDirectoryWatcher' 3 | s.version = '0.0.8' 4 | s.summary = 'MHWDirectoryWatcher is a lightweight and efficient class that uses GCD to monitor a given directory for changes.' 5 | s.homepage = 'https://github.com/hwaxxer/MHWDirectoryWatcher' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'Martin Hwasser' => 'martin.hwasser@gmail.com' } 8 | s.source = { :git => 'https://github.com/hwaxxer/MHWDirectoryWatcher.git', :tag => s.version.to_s } 9 | s.source_files = 'MHWDirectoryWatcher/*' 10 | s.requires_arc = true 11 | s.ios.deployment_target = '5.0' 12 | s.osx.deployment_target = '10.7' 13 | end 14 | -------------------------------------------------------------------------------- /MHWDirectoryWatcher/MHWDirectoryWatcher.h: -------------------------------------------------------------------------------- 1 | /* 2 | * MHWDirectoryWatcher.h 3 | * Created by Martin Hwasser on 12/19/11. 4 | */ 5 | 6 | #import 7 | 8 | @interface MHWDirectoryWatcher : NSObject 9 | 10 | // Returns an initialized MHWDirectoryWatcher and begins to watch the path, if specified 11 | + (MHWDirectoryWatcher *)directoryWatcherAtPath:(NSString *)watchedPath 12 | startImmediately:(BOOL)startImmediately 13 | callback:(void(^)(void))cb; 14 | 15 | // Equivalent to calling +directoryWatcherAtPath:startImmediately and passing 16 | // YES for startImmediately. 17 | + (MHWDirectoryWatcher *)directoryWatcherAtPath:(NSString *)watchPath 18 | callback:(void(^)(void))cb; 19 | 20 | // Returns YES if started watching, NO if already is watching 21 | - (BOOL)startWatching; 22 | 23 | // Returns YES if stopped watching, NO if not watching 24 | - (BOOL)stopWatching; 25 | 26 | // The path being watched 27 | @property (nonatomic, readonly, copy) NSString *watchedPath; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /MHWDirectoryWatcher/MHWDirectoryWatcher.m: -------------------------------------------------------------------------------- 1 | /* 2 | * MHWDirectoryWatcher.h 3 | * Created by Martin Hwasser on 12/19/11. 4 | */ 5 | 6 | #import "MHWDirectoryWatcher.h" 7 | 8 | #define kMHWDirectoryWatcherPollInterval 0.2 9 | #define kMHWDirectoryWatcherPollRetryCount 5 10 | 11 | typedef void (^MHWDirectoryWatcherCallback)(void); 12 | 13 | @interface MHWDirectoryWatcher () 14 | 15 | @property (nonatomic) dispatch_source_t source; 16 | @property (nonatomic) dispatch_queue_t queue; 17 | @property (nonatomic) NSInteger retriesLeft; 18 | @property (nonatomic, getter = isDirectoryChanging) BOOL directoryChanging; 19 | @property (nonatomic, readwrite, copy) NSString *watchedPath; 20 | @property (nonatomic, copy) MHWDirectoryWatcherCallback callback; 21 | 22 | @end 23 | 24 | @implementation MHWDirectoryWatcher 25 | 26 | - (void)dealloc 27 | { 28 | [self stopWatching]; 29 | _callback = nil; 30 | } 31 | 32 | - (id)initWithPath:(NSString *)path 33 | { 34 | self = [super init]; 35 | if (self) { 36 | _watchedPath = [path copy]; 37 | _queue = dispatch_queue_create("MHWDirectoryWatcherQueue", 0); 38 | } 39 | return self; 40 | } 41 | 42 | + (MHWDirectoryWatcher *)directoryWatcherAtPath:(NSString *)watchedPath 43 | startImmediately:(BOOL)startImmediately 44 | callback:(void(^)(void))cb 45 | { 46 | NSAssert(watchedPath != nil, @"The directory to watch must not be nil"); 47 | MHWDirectoryWatcher *directoryWatcher = [[MHWDirectoryWatcher alloc] initWithPath:watchedPath]; 48 | directoryWatcher.callback = cb; 49 | 50 | if (startImmediately) { 51 | if (![directoryWatcher startWatching]) { 52 | // Something went wrong, return nil 53 | return nil; 54 | } 55 | } 56 | return directoryWatcher; 57 | } 58 | 59 | + (MHWDirectoryWatcher *)directoryWatcherAtPath:(NSString *)watchPath 60 | callback:(void(^)(void))cb 61 | { 62 | return [MHWDirectoryWatcher directoryWatcherAtPath:watchPath 63 | startImmediately:YES 64 | callback:cb]; 65 | } 66 | #pragma mark - 67 | #pragma mark - Public methods 68 | 69 | - (BOOL)stopWatching 70 | { 71 | if (_source != NULL) { 72 | dispatch_source_cancel(_source); 73 | _source = NULL; 74 | return YES; 75 | } 76 | return NO; 77 | } 78 | 79 | - (BOOL)startWatching 80 | { 81 | // Already monitoring 82 | if (self.source != NULL) { 83 | return NO; 84 | } 85 | 86 | // Open an event-only file descriptor associated with the directory 87 | int fd = open([self.watchedPath fileSystemRepresentation], O_EVTONLY); 88 | if (fd < 0) { 89 | return NO; 90 | } 91 | 92 | void (^cleanup)(void) = ^{ 93 | close(fd); 94 | }; 95 | 96 | // Get a low priority queue 97 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 98 | 99 | // Monitor the directory for writes 100 | self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, // Monitors a file descriptor 101 | fd, // our file descriptor 102 | DISPATCH_VNODE_WRITE, // The file-system object data changed. 103 | queue); // the queue to dispatch on 104 | 105 | if (!_source) { 106 | cleanup(); 107 | return NO; 108 | } 109 | 110 | __weak __typeof__(self) _weakSelf = self; 111 | // Call directoryDidChange on event callback 112 | dispatch_source_set_event_handler(self.source, ^{ 113 | [_weakSelf directoryDidChange]; 114 | }); 115 | 116 | // Dispatch source destructor 117 | dispatch_source_set_cancel_handler(self.source, cleanup); 118 | 119 | // Sources are create in suspended state, so resume it 120 | dispatch_resume(self.source); 121 | 122 | // Everything was OK 123 | return YES; 124 | } 125 | 126 | 127 | #pragma mark - 128 | #pragma mark - Private methods 129 | 130 | - (NSArray *)directoryMetadata 131 | { 132 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 133 | NSArray *contents = [fileManager contentsOfDirectoryAtPath:self.watchedPath 134 | error:NULL]; 135 | 136 | NSMutableArray *directoryMetadata = [NSMutableArray array]; 137 | 138 | for (NSString *fileName in contents) { 139 | @autoreleasepool { 140 | NSString *filePath = [self.watchedPath stringByAppendingPathComponent:fileName]; 141 | NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath 142 | error:nil]; 143 | NSInteger fileSize = [[fileAttributes objectForKey:NSFileSize] intValue]; 144 | // The fileName and fileSize will be our hash key 145 | NSString *fileHash = [NSString stringWithFormat:@"%@%ld", fileName, (long)fileSize]; 146 | // Add it to our metadata list 147 | [directoryMetadata addObject:fileHash]; 148 | } 149 | } 150 | return directoryMetadata; 151 | } 152 | 153 | - (void)checkChangesAfterDelay:(NSTimeInterval)timeInterval 154 | { 155 | NSArray *directoryMetadata = [self directoryMetadata]; 156 | 157 | __weak __typeof__(self) _weakSelf = self; 158 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, timeInterval * NSEC_PER_SEC); 159 | dispatch_after(popTime, self.queue, ^(void){ 160 | [_weakSelf pollDirectoryForChangesWithMetadata:directoryMetadata]; 161 | }); 162 | } 163 | 164 | - (void)pollDirectoryForChangesWithMetadata:(NSArray *)oldDirectoryMetadata 165 | { 166 | NSArray *newDirectoryMetadata = [self directoryMetadata]; 167 | 168 | // Check if metadata has changed 169 | self.directoryChanging = ![newDirectoryMetadata isEqualToArray:oldDirectoryMetadata]; 170 | 171 | // Reset retries if it's still changing 172 | self.retriesLeft = self.isDirectoryChanging ? kMHWDirectoryWatcherPollRetryCount : self.retriesLeft; 173 | 174 | if (self.isDirectoryChanging || 0 < self.retriesLeft--) { 175 | // Either the directory is changing or 176 | // we should try again as more changes may occur 177 | [self checkChangesAfterDelay:kMHWDirectoryWatcherPollInterval]; 178 | } else { 179 | // Changes appear to be completed 180 | // Post a notification informing that the directory did change 181 | dispatch_async(dispatch_get_main_queue(), ^{ 182 | self.callback(); 183 | }); 184 | } 185 | } 186 | 187 | - (void)directoryDidChange 188 | { 189 | if (!self.isDirectoryChanging) { 190 | // Changes just occurred 191 | self.directoryChanging = YES; 192 | self.retriesLeft = kMHWDirectoryWatcherPollRetryCount; 193 | 194 | [self checkChangesAfterDelay:kMHWDirectoryWatcherPollInterval]; 195 | } 196 | } 197 | 198 | @end 199 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 73859A54179D9EA700B54906 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73859A53179D9EA700B54906 /* UIKit.framework */; }; 11 | 73859A56179D9EA700B54906 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73859A55179D9EA700B54906 /* Foundation.framework */; }; 12 | 73859A58179D9EA700B54906 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73859A57179D9EA700B54906 /* CoreGraphics.framework */; }; 13 | 73859A5E179D9EA700B54906 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 73859A5C179D9EA700B54906 /* InfoPlist.strings */; }; 14 | 73859A60179D9EA700B54906 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 73859A5F179D9EA700B54906 /* main.m */; }; 15 | 73859A64179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 73859A63179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.m */; }; 16 | 73859A66179D9EA700B54906 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 73859A65179D9EA700B54906 /* Default.png */; }; 17 | 73859A68179D9EA700B54906 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 73859A67179D9EA700B54906 /* Default@2x.png */; }; 18 | 73859A6A179D9EA700B54906 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 73859A69179D9EA700B54906 /* Default-568h@2x.png */; }; 19 | 73859A73179D9F2500B54906 /* MHWDirectoryWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 73859A72179D9F2500B54906 /* MHWDirectoryWatcher.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 73859A50179D9EA700B54906 /* MHWDirectoryWatcherExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MHWDirectoryWatcherExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 73859A53179D9EA700B54906 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 25 | 73859A55179D9EA700B54906 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 26 | 73859A57179D9EA700B54906 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 27 | 73859A5B179D9EA700B54906 /* MHWDirectoryWatcherExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "MHWDirectoryWatcherExample-Info.plist"; sourceTree = ""; }; 28 | 73859A5D179D9EA700B54906 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 29 | 73859A5F179D9EA700B54906 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 30 | 73859A61179D9EA700B54906 /* MHWDirectoryWatcherExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MHWDirectoryWatcherExample-Prefix.pch"; sourceTree = ""; }; 31 | 73859A62179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MHWDirectoryWatcherAppDelegate.h; sourceTree = ""; }; 32 | 73859A63179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MHWDirectoryWatcherAppDelegate.m; sourceTree = ""; }; 33 | 73859A65179D9EA700B54906 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 34 | 73859A67179D9EA700B54906 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 35 | 73859A69179D9EA700B54906 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 36 | 73859A71179D9F2500B54906 /* MHWDirectoryWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MHWDirectoryWatcher.h; sourceTree = ""; }; 37 | 73859A72179D9F2500B54906 /* MHWDirectoryWatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MHWDirectoryWatcher.m; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 73859A4D179D9EA700B54906 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 73859A54179D9EA700B54906 /* UIKit.framework in Frameworks */, 46 | 73859A56179D9EA700B54906 /* Foundation.framework in Frameworks */, 47 | 73859A58179D9EA700B54906 /* CoreGraphics.framework in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 73859A47179D9EA700B54906 = { 55 | isa = PBXGroup; 56 | children = ( 57 | 73859A70179D9F2500B54906 /* MHWDirectoryWatcher */, 58 | 73859A59179D9EA700B54906 /* MHWDirectoryWatcherExample */, 59 | 73859A52179D9EA700B54906 /* Frameworks */, 60 | 73859A51179D9EA700B54906 /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 73859A51179D9EA700B54906 /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 73859A50179D9EA700B54906 /* MHWDirectoryWatcherExample.app */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | 73859A52179D9EA700B54906 /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 73859A53179D9EA700B54906 /* UIKit.framework */, 76 | 73859A55179D9EA700B54906 /* Foundation.framework */, 77 | 73859A57179D9EA700B54906 /* CoreGraphics.framework */, 78 | ); 79 | name = Frameworks; 80 | sourceTree = ""; 81 | }; 82 | 73859A59179D9EA700B54906 /* MHWDirectoryWatcherExample */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 73859A62179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.h */, 86 | 73859A63179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.m */, 87 | 73859A5A179D9EA700B54906 /* Supporting Files */, 88 | ); 89 | path = MHWDirectoryWatcherExample; 90 | sourceTree = ""; 91 | }; 92 | 73859A5A179D9EA700B54906 /* Supporting Files */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 73859A5B179D9EA700B54906 /* MHWDirectoryWatcherExample-Info.plist */, 96 | 73859A5C179D9EA700B54906 /* InfoPlist.strings */, 97 | 73859A5F179D9EA700B54906 /* main.m */, 98 | 73859A61179D9EA700B54906 /* MHWDirectoryWatcherExample-Prefix.pch */, 99 | 73859A65179D9EA700B54906 /* Default.png */, 100 | 73859A67179D9EA700B54906 /* Default@2x.png */, 101 | 73859A69179D9EA700B54906 /* Default-568h@2x.png */, 102 | ); 103 | name = "Supporting Files"; 104 | sourceTree = ""; 105 | }; 106 | 73859A70179D9F2500B54906 /* MHWDirectoryWatcher */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 73859A71179D9F2500B54906 /* MHWDirectoryWatcher.h */, 110 | 73859A72179D9F2500B54906 /* MHWDirectoryWatcher.m */, 111 | ); 112 | path = MHWDirectoryWatcher; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 73859A4F179D9EA700B54906 /* MHWDirectoryWatcherExample */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 73859A6D179D9EA700B54906 /* Build configuration list for PBXNativeTarget "MHWDirectoryWatcherExample" */; 121 | buildPhases = ( 122 | 73859A4C179D9EA700B54906 /* Sources */, 123 | 73859A4D179D9EA700B54906 /* Frameworks */, 124 | 73859A4E179D9EA700B54906 /* Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | ); 130 | name = MHWDirectoryWatcherExample; 131 | productName = MHWDirectoryWatcherExample; 132 | productReference = 73859A50179D9EA700B54906 /* MHWDirectoryWatcherExample.app */; 133 | productType = "com.apple.product-type.application"; 134 | }; 135 | /* End PBXNativeTarget section */ 136 | 137 | /* Begin PBXProject section */ 138 | 73859A48179D9EA700B54906 /* Project object */ = { 139 | isa = PBXProject; 140 | attributes = { 141 | CLASSPREFIX = MHWDirectoryWatcher; 142 | LastUpgradeCheck = 0920; 143 | ORGANIZATIONNAME = "Martin Hwasser"; 144 | }; 145 | buildConfigurationList = 73859A4B179D9EA700B54906 /* Build configuration list for PBXProject "MHWDirectoryWatcherExample" */; 146 | compatibilityVersion = "Xcode 3.2"; 147 | developmentRegion = English; 148 | hasScannedForEncodings = 0; 149 | knownRegions = ( 150 | English, 151 | en, 152 | ); 153 | mainGroup = 73859A47179D9EA700B54906; 154 | productRefGroup = 73859A51179D9EA700B54906 /* Products */; 155 | projectDirPath = ""; 156 | projectRoot = ""; 157 | targets = ( 158 | 73859A4F179D9EA700B54906 /* MHWDirectoryWatcherExample */, 159 | ); 160 | }; 161 | /* End PBXProject section */ 162 | 163 | /* Begin PBXResourcesBuildPhase section */ 164 | 73859A4E179D9EA700B54906 /* Resources */ = { 165 | isa = PBXResourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 73859A5E179D9EA700B54906 /* InfoPlist.strings in Resources */, 169 | 73859A66179D9EA700B54906 /* Default.png in Resources */, 170 | 73859A68179D9EA700B54906 /* Default@2x.png in Resources */, 171 | 73859A6A179D9EA700B54906 /* Default-568h@2x.png in Resources */, 172 | ); 173 | runOnlyForDeploymentPostprocessing = 0; 174 | }; 175 | /* End PBXResourcesBuildPhase section */ 176 | 177 | /* Begin PBXSourcesBuildPhase section */ 178 | 73859A4C179D9EA700B54906 /* Sources */ = { 179 | isa = PBXSourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | 73859A60179D9EA700B54906 /* main.m in Sources */, 183 | 73859A64179D9EA700B54906 /* MHWDirectoryWatcherAppDelegate.m in Sources */, 184 | 73859A73179D9F2500B54906 /* MHWDirectoryWatcher.m in Sources */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | /* End PBXSourcesBuildPhase section */ 189 | 190 | /* Begin PBXVariantGroup section */ 191 | 73859A5C179D9EA700B54906 /* InfoPlist.strings */ = { 192 | isa = PBXVariantGroup; 193 | children = ( 194 | 73859A5D179D9EA700B54906 /* en */, 195 | ); 196 | name = InfoPlist.strings; 197 | sourceTree = ""; 198 | }; 199 | /* End PBXVariantGroup section */ 200 | 201 | /* Begin XCBuildConfiguration section */ 202 | 73859A6B179D9EA700B54906 /* Debug */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | ALWAYS_SEARCH_USER_PATHS = NO; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_OBJC_ARC = YES; 209 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_COMMA = YES; 212 | CLANG_WARN_CONSTANT_CONVERSION = YES; 213 | CLANG_WARN_EMPTY_BODY = YES; 214 | CLANG_WARN_ENUM_CONVERSION = YES; 215 | CLANG_WARN_INFINITE_RECURSION = YES; 216 | CLANG_WARN_INT_CONVERSION = YES; 217 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 225 | COPY_PHASE_STRIP = NO; 226 | ENABLE_STRICT_OBJC_MSGSEND = YES; 227 | ENABLE_TESTABILITY = YES; 228 | GCC_C_LANGUAGE_STANDARD = gnu99; 229 | GCC_DYNAMIC_NO_PIC = NO; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_OPTIMIZATION_LEVEL = 0; 232 | GCC_PREPROCESSOR_DEFINITIONS = ( 233 | "DEBUG=1", 234 | "$(inherited)", 235 | ); 236 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 239 | GCC_WARN_UNDECLARED_SELECTOR = YES; 240 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 241 | GCC_WARN_UNUSED_FUNCTION = YES; 242 | GCC_WARN_UNUSED_VARIABLE = YES; 243 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 244 | ONLY_ACTIVE_ARCH = YES; 245 | SDKROOT = iphoneos; 246 | TARGETED_DEVICE_FAMILY = "1,2"; 247 | }; 248 | name = Debug; 249 | }; 250 | 73859A6C179D9EA700B54906 /* Release */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 255 | CLANG_CXX_LIBRARY = "libc++"; 256 | CLANG_ENABLE_OBJC_ARC = YES; 257 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 258 | CLANG_WARN_BOOL_CONVERSION = YES; 259 | CLANG_WARN_COMMA = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_EMPTY_BODY = YES; 262 | CLANG_WARN_ENUM_CONVERSION = YES; 263 | CLANG_WARN_INFINITE_RECURSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 268 | CLANG_WARN_STRICT_PROTOTYPES = YES; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 273 | COPY_PHASE_STRIP = YES; 274 | ENABLE_STRICT_OBJC_MSGSEND = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 284 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 285 | SDKROOT = iphoneos; 286 | TARGETED_DEVICE_FAMILY = "1,2"; 287 | VALIDATE_PRODUCT = YES; 288 | }; 289 | name = Release; 290 | }; 291 | 73859A6E179D9EA700B54906 /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 295 | GCC_PREFIX_HEADER = "MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Prefix.pch"; 296 | INFOPLIST_FILE = "MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Info.plist"; 297 | PRODUCT_BUNDLE_IDENTIFIER = "com.hwaxxer.${PRODUCT_NAME:rfc1034identifier}"; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | WRAPPER_EXTENSION = app; 300 | }; 301 | name = Debug; 302 | }; 303 | 73859A6F179D9EA700B54906 /* Release */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 307 | GCC_PREFIX_HEADER = "MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Prefix.pch"; 308 | INFOPLIST_FILE = "MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Info.plist"; 309 | PRODUCT_BUNDLE_IDENTIFIER = "com.hwaxxer.${PRODUCT_NAME:rfc1034identifier}"; 310 | PRODUCT_NAME = "$(TARGET_NAME)"; 311 | WRAPPER_EXTENSION = app; 312 | }; 313 | name = Release; 314 | }; 315 | /* End XCBuildConfiguration section */ 316 | 317 | /* Begin XCConfigurationList section */ 318 | 73859A4B179D9EA700B54906 /* Build configuration list for PBXProject "MHWDirectoryWatcherExample" */ = { 319 | isa = XCConfigurationList; 320 | buildConfigurations = ( 321 | 73859A6B179D9EA700B54906 /* Debug */, 322 | 73859A6C179D9EA700B54906 /* Release */, 323 | ); 324 | defaultConfigurationIsVisible = 0; 325 | defaultConfigurationName = Release; 326 | }; 327 | 73859A6D179D9EA700B54906 /* Build configuration list for PBXNativeTarget "MHWDirectoryWatcherExample" */ = { 328 | isa = XCConfigurationList; 329 | buildConfigurations = ( 330 | 73859A6E179D9EA700B54906 /* Debug */, 331 | 73859A6F179D9EA700B54906 /* Release */, 332 | ); 333 | defaultConfigurationIsVisible = 0; 334 | defaultConfigurationName = Release; 335 | }; 336 | /* End XCConfigurationList section */ 337 | }; 338 | rootObject = 73859A48179D9EA700B54906 /* Project object */; 339 | } 340 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hwaxxer/MHWDirectoryWatcher/7953b6df27e7459ec14e25861cd09e52070c29b7/MHWDirectoryWatcherExample/Default-568h@2x.png -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hwaxxer/MHWDirectoryWatcher/7953b6df27e7459ec14e25861cd09e52070c29b7/MHWDirectoryWatcherExample/Default.png -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hwaxxer/MHWDirectoryWatcher/7953b6df27e7459ec14e25861cd09e52070c29b7/MHWDirectoryWatcherExample/Default@2x.png -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/MHWDirectoryWatcherAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // MHWDirectoryWatcherAppDelegate.h 3 | // MHWDirectoryWatcherExample 4 | // 5 | // Created by Martin Hwasser on 7/22/13. 6 | // Copyright (c) 2013 Martin Hwasser. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class MHWDirectoryWatcher; 12 | 13 | @interface MHWDirectoryWatcherAppDelegate : UIResponder 14 | 15 | @property (strong, nonatomic) UIWindow *window; 16 | @property (nonatomic, strong) MHWDirectoryWatcher *directoryWatcher; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/MHWDirectoryWatcherAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // MHWDirectoryWatcherAppDelegate.m 3 | // MHWDirectoryWatcherExample 4 | // 5 | // Created by Martin Hwasser on 7/22/13. 6 | // Copyright (c) 2013 Martin Hwasser. All rights reserved. 7 | // 8 | 9 | #import "MHWDirectoryWatcherAppDelegate.h" 10 | #import "MHWDirectoryWatcher.h" 11 | 12 | @implementation MHWDirectoryWatcherAppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 17 | // Override point for customization after application launch. 18 | self.window.backgroundColor = [UIColor whiteColor]; 19 | [self.window makeKeyAndVisible]; 20 | 21 | 22 | self.directoryWatcher = [MHWDirectoryWatcher directoryWatcherAtPath:[self pathToDocumentsDirectory] 23 | callback:^{ 24 | [self directoryDidChange]; 25 | }]; 26 | return YES; 27 | } 28 | 29 | - (void)directoryDidChange 30 | { 31 | NSLog(@"Files changed at: %@", self.directoryWatcher.watchedPath); 32 | self.window.backgroundColor = [self randomColor]; 33 | } 34 | 35 | - (void)moveFiles 36 | { 37 | static NSFileManager *fileManager = nil; 38 | static dispatch_once_t onceToken; 39 | dispatch_once(&onceToken, ^{ 40 | fileManager = [NSFileManager new]; 41 | }); 42 | 43 | NSString *newFilePath = [[self pathToDocumentsDirectory] stringByAppendingPathComponent:@"file"]; 44 | if ([fileManager fileExistsAtPath:newFilePath]) { 45 | [fileManager removeItemAtPath:newFilePath error:nil]; 46 | } else { 47 | [fileManager createFileAtPath:newFilePath contents:nil attributes:nil]; 48 | } 49 | } 50 | 51 | - (void)applicationWillResignActive:(UIApplication *)application 52 | { 53 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 54 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 55 | } 56 | 57 | - (void)applicationDidEnterBackground:(UIApplication *)application 58 | { 59 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 60 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 61 | [self.directoryWatcher stopWatching]; 62 | } 63 | 64 | - (void)applicationWillEnterForeground:(UIApplication *)application 65 | { 66 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 67 | } 68 | 69 | - (void)applicationDidBecomeActive:(UIApplication *)application 70 | { 71 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 72 | [self.directoryWatcher startWatching]; 73 | 74 | [NSTimer scheduledTimerWithTimeInterval:3.0 75 | target:self 76 | selector:@selector(moveFiles) 77 | userInfo:nil repeats:YES]; 78 | } 79 | 80 | - (void)applicationWillTerminate:(UIApplication *)application 81 | { 82 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 83 | } 84 | 85 | - (NSString *)pathToDocumentsDirectory 86 | { 87 | return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; 88 | } 89 | 90 | - (UIColor *)randomColor 91 | { 92 | CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 93 | CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white 94 | CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black 95 | return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; 96 | } 97 | 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/MHWDirectoryWatcherExample-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'MHWDirectoryWatcherExample' target in the 'MHWDirectoryWatcherExample' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_3_0 8 | #warning "This project uses features only available in iOS SDK 3.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /MHWDirectoryWatcherExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MHWDirectoryWatcherExample 4 | // 5 | // Created by Martin Hwasser on 7/22/13. 6 | // Copyright (c) 2013 Martin Hwasser. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MHWDirectoryWatcherAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([MHWDirectoryWatcherAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MHWDirectoryWatcher 2 | `MHWDirectoryWatcher` is a lightweight class that uses GCD to monitor a given path for changes. 3 | When any change to the directory occurs, `MHWDirectoryWatcher` starts polling the monitored path, making sure that file transfers are finished before posting notifications. 4 | 5 | ### Installing 6 | Copy `MHWDirectoryWatcher.h+m` into your project. 7 | 8 | (or use [CocoaPods](http://cocoapods.org)) 9 | 10 | ### Usage via blocks 11 | Get an instance of `MHWDirectoryWatcher` using the factory method `+directoryWatcherAtPath:callback:` and it will start monitoring the path immediately. Callback occurs after files have changed. 12 | 13 | Example: 14 | 15 | ```objective-c 16 | _dirWatcher = [MHWDirectoryWatcher directoryWatcherAtPath:kDocumentsFolder callback:^{ 17 | // Actions which should be performed when the files in the directory 18 | [self doSomethingNice]; 19 | }]; 20 | 21 | ``` 22 | 23 | Call `-stopWatching` / `-startWatching` to pause/resume. 24 | 25 | --- 26 | 27 | Used in [Kobo](https://itunes.apple.com/se/app/kobo-books/id301259483?l=en&mt=8) and [Readmill](https://itunes.apple.com/se/app/readmill-book-reader-for-epub/id438032664?l=en&mt=8) (RIP, acquired by Dropbox). 28 | 29 | If you like this repository and use it in your project, I'd love to hear about it! 30 | --------------------------------------------------------------------------------