├── .gitignore ├── Mac ├── InfoPlist.strings ├── PARStore-Prefix.pch ├── main.m ├── PARAppDelegate.h ├── PARAppDelegate.m ├── Credits.rtf └── PARStore-Info.plist ├── iOS ├── Default.png ├── en.lproj │ └── InfoPlist.strings ├── Default@2x.png ├── Default-568h@2x.png ├── PARAppDelegate.h ├── iOS-PARStore-Prefix.pch ├── main.m ├── iOS-PARStore-Info.plist └── PARAppDelegate.m ├── Tests ├── en.lproj │ └── InfoPlist.strings ├── PARStore-Prefix.pch ├── PARTestCase.h ├── PARStoreExample.h ├── PARStoreTests-Info.plist ├── PARTestCase.m ├── PARStoreExample.m ├── PARDispatchQueueTests.m └── PARSQLiteTests.m ├── iOS-Tests ├── en.lproj │ └── InfoPlist.strings ├── iOS_PARStoreTests.h ├── iOS_PARStoreTests.m └── iOS-PARStoreTests-Info.plist ├── PARStore Viewer ├── PARStore Viewer-Bridging-Header.h ├── PARStore Viewer │ ├── AppDelegate.swift │ ├── PARStore+ContentDump.swift │ ├── TextViewController.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── PARStoreDocument.swift │ ├── Info.plist │ ├── DocumentWindowController.swift │ └── HistoryViewController.swift └── PARStore Viewer.xcodeproj │ └── project.pbxproj ├── Core ├── NSError+Factory.h ├── PARNotificationSemaphore.h ├── NSError+Factory.m ├── PARNotificationSemaphore.m ├── PARDispatchQueue.h ├── PARStore.h └── PARDispatchQueue.m ├── README.markdown ├── LICENSE-BSD.txt ├── PARStore.xcodeproj ├── xcshareddata │ └── xcschemes │ │ ├── PARStore-IOS.xcscheme │ │ └── PARStore-Mac.xcscheme └── project.pbxproj └── PARStore 2.0.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | project.xcworkspace/ 3 | xcuserdata/ 4 | -------------------------------------------------------------------------------- /Mac/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /iOS/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cparnot/PARStore/HEAD/iOS/Default.png -------------------------------------------------------------------------------- /iOS/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /iOS/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cparnot/PARStore/HEAD/iOS/Default@2x.png -------------------------------------------------------------------------------- /iOS-Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /iOS/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cparnot/PARStore/HEAD/iOS/Default-568h@2x.png -------------------------------------------------------------------------------- /Mac/PARStore-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'PARStore' target in the 'PARStore' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /Tests/PARStore-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'PARStore' target in the 'PARStore' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "PARDispatchQueue.h" 6 | #import "PARStore.h" 7 | #import "NSError+Factory.h" 8 | -------------------------------------------------------------------------------- /Mac/main.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | #import 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | return NSApplicationMain(argc, (const char **)argv); 10 | } 11 | -------------------------------------------------------------------------------- /Tests/PARTestCase.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | 6 | #import 7 | 8 | @interface PARTestCase : XCTestCase 9 | 10 | // utilities 11 | - (NSURL *)urlWithUniqueTmpDirectory; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Mac/PARAppDelegate.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | #import 6 | 7 | @interface PARAppDelegate : NSObject 8 | 9 | @property (assign) IBOutlet NSWindow *window; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /iOS-Tests/iOS_PARStoreTests.h: -------------------------------------------------------------------------------- 1 | // 2 | // iOS_PARStoreTests.h 3 | // iOS-PARStoreTests 4 | // 5 | // Created by Charles Parnot on 3/2/13. 6 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface iOS_PARStoreTests : SenTestCase 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /iOS/PARAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // PARAppDelegate.h 3 | // iOS-PARStore 4 | // 5 | // Created by Charles Parnot on 3/2/13. 6 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface PARAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Mac/PARAppDelegate.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | #import "PARAppDelegate.h" 6 | 7 | @implementation PARAppDelegate 8 | 9 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 10 | { 11 | // Insert code here to initialize your application 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /iOS/iOS-PARStore-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'iOS-PARStore' target in the 'iOS-PARStore' 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 | -------------------------------------------------------------------------------- /iOS/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // iOS-PARStore 4 | // 5 | // Created by Charles Parnot on 3/2/13. 6 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "PARAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([PARAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Core/NSError+Factory.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/13/13. 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | @import Foundation; 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface NSError (Factory) 10 | + (NSError *)errorWithObject:(id)object code:(NSInteger)code localizedDescription:(nullable NSString *)description underlyingError:(nullable NSError *)underlyingError; 11 | @end 12 | 13 | NS_ASSUME_NONNULL_END -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | PARStore 2 | 3 | Authors: Charles Parnot and Joris Kluivers 4 | 5 | Contact: charles.parnot@gmail.com 6 | 7 | 8 | Description 9 | ----------- 10 | 11 | PARStore is a generic key/value store in a Dropbox-friendly file format. 12 | 13 | 14 | License 15 | ------- 16 | 17 | The PARStore and PARDispatchQueue classes and the example code are released under the modified BSD License. Please read the text of the license included with the project for more details. 18 | 19 | 20 | 21 | Changelog 22 | --------- 23 | 24 | Mar 2013 25 | 26 | * First release 27 | -------------------------------------------------------------------------------- /Mac/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PARStoreExample.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | 6 | #import "PARStore.h" 7 | 8 | @interface PARStoreExample : PARStore 9 | 10 | @property (copy) NSString *first; 11 | @property (copy) NSString *last; 12 | @property (copy) NSString *title; 13 | @property (copy) NSString *summary; 14 | 15 | 16 | - (NSArray *)relevantKeysForSync; 17 | 18 | @property BOOL shouldThrottleNotifications; 19 | 20 | - (NSArray *)sortedLogRepresentationsFromDeviceIdentifier:(NSString *)deviceIdentifier; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /iOS-Tests/iOS_PARStoreTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // iOS_PARStoreTests.m 3 | // iOS-PARStoreTests 4 | // 5 | // Created by Charles Parnot on 3/2/13. 6 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 7 | // 8 | 9 | #import "iOS_PARStoreTests.h" 10 | 11 | @implementation iOS_PARStoreTests 12 | 13 | - (void)setUp 14 | { 15 | [super setUp]; 16 | 17 | // Set-up code here. 18 | } 19 | 20 | - (void)tearDown 21 | { 22 | // Tear-down code here. 23 | 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample 28 | { 29 | STFail(@"Unit tests are not implemented yet in iOS-PARStoreTests"); 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Core/PARNotificationSemaphore.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Author: Charles Parnot 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | #import 6 | 7 | 8 | // The APIs were audited. None of the method return values, method parameters or properties are nullable. 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | @interface PARNotificationSemaphore : NSObject 12 | 13 | + (PARNotificationSemaphore *)semaphoreForNotificationName:(NSString *)name object:(id)obj; 14 | - (BOOL)waitUntilNotificationWithTimeout:(NSTimeInterval)timeout; 15 | 16 | @property (readonly) BOOL notificationWasPosted; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/PARStore+ContentDump.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PARStore+ContentDump.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension PARStore { 12 | 13 | var contentDump: String { 14 | typealias StoreEntry = (key: String, value: Any) 15 | var entries = [StoreEntry]() 16 | allEntries().forEach { 17 | entries.append(($0 as! String, $1)) 18 | } 19 | entries.sort { $0.key < $1.key } 20 | let descriptions = entries.map({"\($0.key) : \($0.value)"}) 21 | return descriptions.joined(separator: "\n") 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/PARStoreTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS-Tests/iOS-PARStoreTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.parnot.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Core/NSError+Factory.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/13/13. 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | 6 | #import "NSError+Factory.h" 7 | 8 | @implementation NSError (Factory) 9 | 10 | + (NSError *)errorWithObject:(id)object code:(NSInteger)code localizedDescription:(NSString *)description underlyingError:(NSError *)underlyingError 11 | { 12 | NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; 13 | if (description) 14 | userInfo[NSLocalizedDescriptionKey] = description; 15 | if (underlyingError) 16 | userInfo[NSUnderlyingErrorKey] = underlyingError; 17 | return [NSError errorWithDomain:NSStringFromClass([object class]) code:code userInfo:userInfo]; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/TextViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewController.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class TextViewController: NSViewController { 12 | 13 | @IBOutlet weak var textView: NSTextView? 14 | 15 | var string: String { 16 | get { 17 | return textView?.string ?? "" 18 | } 19 | set { 20 | textView?.string = newValue 21 | let font = NSFont(name: "courier", size: 11.0)! 22 | textView?.setFont(font, range: NSRange(location: 0, length: newValue.characters.count)) 23 | } 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | textView?.enclosingScrollView?.borderType = .noBorder 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Tests/PARTestCase.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | 6 | #import "PARTestCase.h" 7 | 8 | @implementation PARTestCase 9 | 10 | #pragma mark - Utilities 11 | 12 | - (NSURL *)urlWithUniqueTmpDirectory 13 | { 14 | NSString *parentDir = [[NSBundle mainBundle] bundleIdentifier]; 15 | NSString *uniqueDir = [[[NSDate date] description] stringByAppendingString:[[NSUUID UUID] UUIDString]]; 16 | NSString *path = [[[@"~/Xcode-Tests" stringByAppendingPathComponent:parentDir] stringByAppendingPathComponent:uniqueDir] stringByStandardizingPath]; 17 | NSURL *url = [NSURL fileURLWithPath:path]; 18 | NSError *error = nil; 19 | BOOL success = [[NSFileManager defaultManager] createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error]; 20 | XCTAssertTrue(success, @"Could not create temporary directory:\npath: %@\nerror: %@", path, error); 21 | return url; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Mac/PARStore-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 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 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2013 Charles Parnot. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE-BSD.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 2 | 3 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 4 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 5 | * The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 6 | 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 8 | -------------------------------------------------------------------------------- /Core/PARNotificationSemaphore.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Author: Charles Parnot 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | 6 | #import "PARNotificationSemaphore.h" 7 | 8 | @interface PARNotificationSemaphore() 9 | @property (strong) dispatch_semaphore_t dispatchSemaphore; 10 | @property (strong) NSOperationQueue *operationQueue; 11 | @property (readwrite) BOOL _notificationWasPosted; 12 | @end 13 | 14 | @implementation PARNotificationSemaphore 15 | 16 | + (PARNotificationSemaphore *)semaphoreForNotificationName:(NSString *)name object:(id)obj 17 | { 18 | PARNotificationSemaphore *notificationSemaphore = [[PARNotificationSemaphore alloc] init]; 19 | notificationSemaphore.dispatchSemaphore = dispatch_semaphore_create(0); 20 | notificationSemaphore.operationQueue = [[NSOperationQueue alloc] init]; 21 | [[NSNotificationCenter defaultCenter] addObserverForName:name object:obj queue:notificationSemaphore.operationQueue usingBlock:^(NSNotification *note) 22 | { 23 | notificationSemaphore._notificationWasPosted = YES; 24 | dispatch_semaphore_signal(notificationSemaphore.dispatchSemaphore); 25 | }]; 26 | return notificationSemaphore; 27 | } 28 | 29 | - (BOOL)waitUntilNotificationWithTimeout:(NSTimeInterval)timeout; 30 | { 31 | long waitResult = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC)); 32 | return (waitResult == 0); 33 | } 34 | 35 | - (BOOL)notificationWasPosted 36 | { 37 | return self._notificationWasPosted; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /iOS/iOS-PARStore-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 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/PARStoreDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PARStoreDocument.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class PARStoreDocument: NSDocument { 12 | 13 | var historyViewController: HistoryViewController? 14 | 15 | var store: PARStore? 16 | 17 | override func makeWindowControllers() { 18 | let windowController: DocumentWindowController = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "Document Window Controller") as! DocumentWindowController 19 | addWindowController(windowController) 20 | historyViewController = windowController.historyViewController; 21 | historyViewController?.store = store 22 | windowController.refresh() 23 | } 24 | 25 | override func data(ofType typeName: String) throws -> Data { 26 | fatalError("Method not implemented: \(#selector(data(ofType:)))") 27 | } 28 | 29 | override func read(from url: URL, ofType typeName: String) throws { 30 | var deviceIdentifier = NSUUID().uuidString 31 | let deviceSubpaths = try FileManager.default.contentsOfDirectory(atPath: url.appendingPathComponent("devices").path) 32 | for subpath in deviceSubpaths { 33 | guard !subpath.hasPrefix(".") else { continue } 34 | deviceIdentifier = subpath 35 | } 36 | store = PARStore(url: url, deviceIdentifier: deviceIdentifier) 37 | historyViewController?.store = store 38 | } 39 | 40 | override var isEntireFileLoaded: Bool { 41 | return false 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | parstore 13 | 14 | CFBundleTypeIconFile 15 | 16 | CFBundleTypeName 17 | PARStore 18 | CFBundleTypeOSTypes 19 | 20 | ???? 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | LSTypeIsPackage 25 | 0 26 | NSDocumentClass 27 | $(PRODUCT_MODULE_NAME).PARStoreDocument 28 | 29 | 30 | CFBundleExecutable 31 | $(EXECUTABLE_NAME) 32 | CFBundleIconFile 33 | 34 | CFBundleIdentifier 35 | $(PRODUCT_BUNDLE_IDENTIFIER) 36 | CFBundleInfoDictionaryVersion 37 | 6.0 38 | CFBundleName 39 | $(PRODUCT_NAME) 40 | CFBundlePackageType 41 | APPL 42 | CFBundleShortVersionString 43 | 1.0 44 | CFBundleVersion 45 | 1 46 | LSMinimumSystemVersion 47 | $(MACOSX_DEPLOYMENT_TARGET) 48 | NSHumanReadableCopyright 49 | Copyright © 2017 Charles Parnot. All rights reserved. 50 | NSMainStoryboardFile 51 | Main 52 | NSPrincipalClass 53 | NSApplication 54 | 55 | 56 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/DocumentWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentWindowController.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DocumentWindowController: NSWindowController { 12 | 13 | var rootSplitViewController: NSSplitViewController { 14 | return contentViewController?.childViewControllers[0] as! NSSplitViewController 15 | } 16 | 17 | var textSplitViewController: NSSplitViewController { 18 | return rootSplitViewController.splitViewItems[1].viewController as! NSSplitViewController 19 | } 20 | 21 | var historyViewController: HistoryViewController { 22 | return rootSplitViewController.splitViewItems[0].viewController as! HistoryViewController 23 | } 24 | 25 | var textViewController1: TextViewController { 26 | return textSplitViewController.splitViewItems[0].viewController as! TextViewController 27 | } 28 | 29 | var textViewController2: TextViewController { 30 | return textSplitViewController.splitViewItems[1].viewController as! TextViewController 31 | } 32 | 33 | @IBAction func historyViewControllerSelectionDidChange(_ sender: Any) { 34 | refresh() 35 | } 36 | 37 | @IBAction func reload(_ sender: Any) { 38 | historyViewController.refresh() 39 | refresh() 40 | } 41 | 42 | func refresh() { 43 | // change and snapshot 44 | let change = historyViewController.changeForSelectedTimestamp?.description ?? "" 45 | let snapshot = historyViewController.storeForSelectedTimestamp 46 | let state = snapshot.contentDump 47 | // update views 48 | textViewController1.string = change 49 | textViewController2.string = state 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /iOS/PARAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // PARAppDelegate.m 3 | // iOS-PARStore 4 | // 5 | // Created by Charles Parnot on 3/2/13. 6 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 7 | // 8 | 9 | #import "PARAppDelegate.h" 10 | 11 | @implementation PARAppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 14 | { 15 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 16 | // Override point for customization after application launch. 17 | self.window.backgroundColor = [UIColor whiteColor]; 18 | [self.window makeKeyAndVisible]; 19 | return YES; 20 | } 21 | 22 | - (void)applicationWillResignActive:(UIApplication *)application 23 | { 24 | // 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. 25 | // 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. 26 | } 27 | 28 | - (void)applicationDidEnterBackground:(UIApplication *)application 29 | { 30 | // 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. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | - (void)applicationWillEnterForeground:(UIApplication *)application 35 | { 36 | // 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. 37 | } 38 | 39 | - (void)applicationDidBecomeActive:(UIApplication *)application 40 | { 41 | // 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. 42 | } 43 | 44 | - (void)applicationWillTerminate:(UIApplication *)application 45 | { 46 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Tests/PARStoreExample.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 3/2/13. 3 | // Copyright (c) 2013 Charles Parnot. All rights reserved. 4 | 5 | 6 | #import "PARStoreExample.h" 7 | #import 8 | 9 | @interface PARStore (PARStorePrivate) 10 | @property (retain) PARDispatchQueue *notificationQueue; 11 | - (void)postNotificationWithName:(NSString *)notificationName; 12 | - (NSArray *)_sortedLogRepresentationsFromDeviceIdentifier:(NSString *)deviceIdentifier; 13 | @end 14 | 15 | @implementation PARStoreExample 16 | 17 | - (NSArray *)sortedLogRepresentationsFromDeviceIdentifier:(NSString *)deviceIdentifier 18 | { 19 | return [super _sortedLogRepresentationsFromDeviceIdentifier:deviceIdentifier]; 20 | } 21 | 22 | 23 | + (NSArray *)relevantKeysForSync 24 | { 25 | static dispatch_once_t onceToken; 26 | static NSArray *keys = nil; 27 | dispatch_once(&onceToken, ^ 28 | { 29 | keys = @[@"first", @"last", @"title", @"summary"]; 30 | }); 31 | return keys; 32 | } 33 | 34 | - (NSArray *)relevantKeysForSync 35 | { 36 | return [[self class] relevantKeysForSync]; 37 | } 38 | 39 | // accessors are created dynamically based on the list of keys returned by the method `relevantKeysForSync` 40 | 41 | @dynamic first, last, title, summary; 42 | 43 | // inspired by http://stackoverflow.com/questions/3560364/writing-my-own-dynamic-properties-in-cocoa 44 | 45 | + (BOOL)resolveInstanceMethod:(SEL)aSEL 46 | { 47 | // method name --> property 48 | NSString *property = NSStringFromSelector(aSEL); 49 | BOOL setter = ([property length] > 4 && [property hasPrefix:@"set"] && [property hasSuffix:@":"]); 50 | if (setter) 51 | { 52 | NSString *firstLetter = [[property substringWithRange:NSMakeRange(3, 1)] lowercaseString]; 53 | property = [firstLetter stringByAppendingString:[property substringWithRange:NSMakeRange(4, [property length] - 5)]]; 54 | } 55 | 56 | // valid metadata property? 57 | if (![[self relevantKeysForSync] containsObject:property]) 58 | return [super resolveInstanceMethod:aSEL]; 59 | 60 | // setter or getter 61 | if (setter) 62 | class_addMethod([self class], aSEL, (IMP) metadataSetter, "v@:@"); 63 | else 64 | class_addMethod([self class], aSEL, (IMP) metadataGetter, "@@:"); 65 | 66 | return YES; 67 | } 68 | 69 | id metadataGetter(id self, SEL _cmd) 70 | { 71 | NSString *property = NSStringFromSelector(_cmd); 72 | return [self propertyListValueForKey:property]; 73 | } 74 | 75 | void metadataSetter(id self, SEL _cmd, id newValue) 76 | { 77 | // method name --> property 78 | NSString *property = NSStringFromSelector(_cmd); 79 | NSString *firstLetter = [[property substringWithRange:NSMakeRange(3, 1)] lowercaseString]; 80 | property = [firstLetter stringByAppendingString:[property substringWithRange:NSMakeRange(4, [property length] - 5)]]; 81 | 82 | // set metadata value 83 | [self setPropertyListValue:newValue forKey:property]; 84 | } 85 | 86 | #pragma mark - Throttling notifications 87 | 88 | // to test that notifications are all sent when `waitUntilFinished` returns, we introduce here an extra delay to make sure the test fails without the proper queuing 89 | - (void)postNotificationWithName:(NSString *)notificationName 90 | { 91 | [self.notificationQueue dispatchAsynchronously:^ 92 | { 93 | if (self.shouldThrottleNotifications) 94 | [NSThread sleepForTimeInterval:0.3]; 95 | }]; 96 | [super postNotificationWithName:notificationName]; 97 | } 98 | 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /Core/PARDispatchQueue.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Author: Charles Parnot 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | #import 6 | 7 | 8 | #ifndef FDDISPATCHQUEUE_HEADER 9 | #define FDDISPATCHQUEUE_HEADER 10 | 11 | typedef void (^PARDispatchBlock)(void); 12 | 13 | // Timer Behaviors 14 | // PARTimerBehaviorCoalesce: subsequent calls can only reduce the time until firing, not extend 15 | // PARTimerBehaviorDelay: subsequent calls replace the existing time, potentially extending it 16 | // PARTimerBehaviorThrottle: subsequent calls can only fire after the elapsed time, potentially immediately 17 | typedef NS_ENUM(NSInteger, PARTimerBehavior) 18 | { 19 | PARTimerBehaviorCoalesce, 20 | PARTimerBehaviorDelay, 21 | PARTimerBehaviorThrottle, 22 | }; 23 | 24 | 25 | // Synchronous Dispatch Behaviors = what to do when dispatching synchronously a block and we are already within the queue 26 | // PARDeadlockBehaviorExecute: do not add the block to the queue, execute inline (default) 27 | // PARDeadlockBehaviorSkip: do not add the block to the queue, drop it silently 28 | // PARDeadlockBehaviorLog: do not add the block to the queue, log to console 29 | // PARDeadlockBehaviorAssert: do not add the block to the queue, raise an exception 30 | // PARDeadlockBehaviorBlock: add the block to the queue, and be damned 31 | typedef NS_ENUM(NSInteger, PARDeadlockBehavior) 32 | { 33 | PARDeadlockBehaviorExecute, 34 | PARDeadlockBehaviorSkip, 35 | PARDeadlockBehaviorLog, 36 | PARDeadlockBehaviorAssert, 37 | PARDeadlockBehaviorBlock 38 | }; 39 | 40 | 41 | // The APIs were audited. None of the method return values, method parameters or properties are nullable. 42 | NS_ASSUME_NONNULL_BEGIN 43 | 44 | 45 | @interface PARDispatchQueue : NSObject 46 | 47 | 48 | /// @name Creating Queues 49 | + (PARDispatchQueue *)globalDispatchQueue; 50 | + (PARDispatchQueue *)mainDispatchQueue; 51 | + (PARDispatchQueue *)dispatchQueueWithLabel:(NSString *)label; 52 | + (PARDispatchQueue *)dispatchQueueWithLabel:(NSString *)label behavior:(PARDeadlockBehavior)behavior; 53 | 54 | // queue created lazily, then shared and guaranteed to be always the same 55 | // this is useful as an alternative to `globalDispatchQueue` to dispatch barrier blocks 56 | + (PARDispatchQueue *)sharedConcurrentQueue; 57 | 58 | 59 | /// @name Properties 60 | @property (readonly, copy) NSString *label; 61 | @property (readonly) PARDeadlockBehavior deadlockBehavior; 62 | 63 | 64 | /// @name Utilities 65 | + (NSString *)labelByPrependingBundleIdentifierToString:(NSString *)suffix; 66 | 67 | 68 | /// @name Dispatching Blocks 69 | 70 | - (void)dispatchSynchronously:(PARDispatchBlock)block; 71 | - (void)dispatchAsynchronously:(PARDispatchBlock)block; 72 | - (void)dispatchBarrierSynchronously:(PARDispatchBlock)block; 73 | - (void)dispatchBarrierAsynchronously:(PARDispatchBlock)block; 74 | 75 | // applicable only for serial queues, with one caveat for the main queue: all blocks in the stack should be dispatched using PARDispatchQueue `dispatchXXX:` calls 76 | - (BOOL)isCurrentQueue; 77 | - (BOOL)isInCurrentQueueStack; 78 | 79 | 80 | /// @name Adding and Updating Timers 81 | - (void)scheduleTimerWithName:(NSString *)name timeInterval:(NSTimeInterval)delay behavior:(PARTimerBehavior)behavior block:(PARDispatchBlock)block; 82 | - (void)cancelTimerWithName:(NSString *)name; 83 | - (void)cancelAllTimers; 84 | - (NSUInteger)timerCount; // the returned value cannot be fully trusted, of course 85 | 86 | @end 87 | 88 | 89 | @interface PARBlockOperation : NSObject 90 | + (PARBlockOperation *)dispatchedOperationWithQueue:(PARDispatchQueue *)queue block:(PARDispatchBlock)block; 91 | - (void)waitUntilFinished; 92 | @end 93 | 94 | 95 | NS_ASSUME_NONNULL_END 96 | 97 | #endif 98 | -------------------------------------------------------------------------------- /PARStore.xcodeproj/xcshareddata/xcschemes/PARStore-IOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /PARStore.xcodeproj/xcshareddata/xcschemes/PARStore-Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer/HistoryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryViewController.swift 3 | // PARStore Viewer 4 | // 5 | // Created by Charles Parnot on 10/13/17. 6 | // Copyright © 2017 Charles Parnot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class HistoryViewController: NSViewController { 12 | 13 | // MARK: Properties 14 | 15 | @IBOutlet weak var tableView: NSTableView! 16 | 17 | typealias ChangeRep = (device: String, change: PARChange) 18 | fileprivate var changeReps: [ChangeRep] = [] 19 | 20 | var store: PARStore? { 21 | didSet { 22 | refresh() 23 | } 24 | } 25 | 26 | var changeForSelectedTimestamp: PARChange? { 27 | let selectedRow = tableView.selectedRow 28 | guard selectedRow >= 0 else { 29 | return nil 30 | } 31 | return changeReps[selectedRow].change 32 | } 33 | 34 | var storeForSelectedTimestamp: PARStore { 35 | let selectedRow = tableView.selectedRow 36 | guard selectedRow >= 0 else { 37 | store?.loadNow() 38 | let snapshotStore = PARStore.inMemory() 39 | snapshotStore.setEntries(from: (store ?? PARStore()).allEntries()) 40 | store?.tearDownNow() 41 | return snapshotStore 42 | } 43 | let snapshotStore = PARStore.inMemory() 44 | for change in changeReps[0...selectedRow] { 45 | snapshotStore.setPropertyListValue(change.change.propertyList, forKey: change.change.key) 46 | } 47 | return snapshotStore 48 | } 49 | 50 | 51 | // MARK: Life Cycle 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | tableView?.enclosingScrollView?.borderType = .noBorder 56 | } 57 | 58 | func refresh() { 59 | guard let store = self.store else { return } 60 | changeReps = HistoryViewController.changeReps(from: store) 61 | tableView.reloadData() 62 | } 63 | 64 | } 65 | 66 | extension HistoryViewController: NSTableViewDataSource, NSTableViewDelegate { 67 | 68 | func numberOfRows(in tableView: NSTableView) -> Int { 69 | return changeReps.count 70 | } 71 | 72 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 73 | 74 | guard let columnIdentifier = tableColumn?.identifier else { return nil } 75 | 76 | let change = changeReps[row] 77 | let stringValue: String 78 | 79 | // timestamp column 80 | if columnIdentifier == "Timestamp" { 81 | let microseconds = Double(change.change.timestamp.int64Value) 82 | let seconds: Double = microseconds / (1000.0 * 1000.0) 83 | let date = Date(timeIntervalSinceReferenceDate: seconds) 84 | stringValue = date.description 85 | } 86 | 87 | // key column 88 | else if columnIdentifier == "Key" { 89 | // layout also gets the number of paragraphs 90 | if change.change.key == "layout", let layout = change.change.propertyList as? [String] { 91 | stringValue = "layout (\(layout.count))" 92 | } 93 | else { 94 | stringValue = change.change.key 95 | } 96 | } 97 | 98 | // device column 99 | else if columnIdentifier == "Device" { 100 | stringValue = change.device 101 | } 102 | 103 | // error 104 | else { 105 | fatalError("invalid column identifier: \(columnIdentifier)") 106 | } 107 | 108 | // final cell view 109 | let cellView = tableView.make(withIdentifier: columnIdentifier, owner: nil) as? NSTableCellView 110 | cellView?.textField?.stringValue = stringValue 111 | cellView?.imageView?.image = nil 112 | return cellView 113 | } 114 | 115 | func tableViewSelectionDidChange(_ notification: Notification) { 116 | nextResponder?.try(toPerform: #selector(HistoryViewControllerActions.historyViewControllerSelectionDidChange(_:)), with: self) 117 | } 118 | 119 | } 120 | 121 | fileprivate extension HistoryViewController { 122 | 123 | fileprivate class func changeReps(from parStore: PARStore) -> [ChangeRep] { 124 | 125 | // device identifiers --> device names 126 | var namesFromIdentifiers: [String : String] = [:] 127 | if let infoDirectory = parStore.storeURL?.appendingPathComponent("blobs").appendingPathComponent("info") { 128 | do { 129 | let files = try FileManager.default.contentsOfDirectory(atPath: infoDirectory.path) 130 | for fileName in files { 131 | let fileURL = infoDirectory.appendingPathComponent(fileName) 132 | let identifier = fileURL.deletingPathExtension().lastPathComponent 133 | if let content = NSDictionary(contentsOf: fileURL) as? [String: Any], let name = content["deviceName"] as? String { 134 | namesFromIdentifiers[identifier] = name 135 | } 136 | } 137 | } catch { 138 | NSLog("could not access info file for store \(String(describing: parStore.storeURL)) because of error: \(error)") 139 | } 140 | } 141 | 142 | // changes --> change reps 143 | let identifiers = (parStore.foreignDeviceIdentifiers + [parStore.deviceIdentifier]) as! [String] 144 | var changeReps: [ChangeRep] = [] 145 | for deviceIdentifier in identifiers { 146 | let deviceName = namesFromIdentifiers[deviceIdentifier] ?? deviceIdentifier 147 | let moreStoreChanges = parStore.fetchChanges(sinceTimestamp: PARStore.timestampForDistantPast(), forDeviceIdentifier: deviceIdentifier) 148 | changeReps += moreStoreChanges.map { return (device: deviceName, change: $0) } 149 | } 150 | changeReps.sort { $0.change.timestamp.int64Value < $1.change.timestamp.int64Value } 151 | return changeReps 152 | } 153 | 154 | } 155 | 156 | /// Action sent up the responder chain by HistoryViewController when its selection changes. 157 | @objc protocol HistoryViewControllerActions { 158 | 159 | @objc optional func historyViewControllerSelectionDidChange(_ sender: Any) 160 | 161 | } 162 | -------------------------------------------------------------------------------- /PARStore 2.0.markdown: -------------------------------------------------------------------------------- 1 | ## PARStore 2.0 2 | 3 | Outline: 4 | 5 | 1. Get rid of CoreData --> just SQLite (+ FMDB or similar). 6 | 2. Do not keep the database open, except maybe the r/w for a short amount of time to optimize for writing bursts. 7 | 3. Always read the entire database when opening, no need for `relevantKeysForSync` (this is already in place as of commit b79be6ab2e8); this was an unnecessary premature optimization. 8 | 4. Swift-only, or at least, swift-aware (PARStore 1 is already compatible with swift, see commit series ending with 62375f9352ee9fc1b5b2609157e576a19abe2f58); the PARDispatchQueue dependency may also be updated to swift. 9 | 5. Simplify the caching: memory layer is still a dictionary (with the latest values for each key), but we also need to cache some of the database information, and we might as well just cache the relevant rows instead of all the complicated setup of PARStore 1. Most of that complexity is historical: as I built more functionality into PARStore, I added some way to keep track of more information as part of the memory cache, and ended up storing pretty much the same information as what's in the database queue, but in a different structure. 10 | 11 | The problem with (1) is that it will likely break backward-compatibility (none of the other points do). Maybe we can be careful to maintain some kind of CoreData-compatible structure using the right SQLite setup, or else officially break backward-compatibility. At least, we should be able to maintain backward-compatibility for files created with PARStore 1, and manipulated by PARStore 2. We could also implement both and have a compatibility flag. Given the simplified cache setup outlined in (5), it would not be very hard. In any case, we want to be forward-compatible and be able to open stores created with PARStore 1, so some of this will have to be implemented. 12 | 13 | The idea with (5) is simply to keep a proper cache of the relevant database rows, and pass those back and forth between memory and database. The logs cache being part of the memory queue means we can do all the translation between the logs and the KV dictionary within the memory queue. The database queue is relegated to a simple role of pushing values into the local database, and pulling values out of the foreign databases, keeping track of where it started last. This would remove all the state management used to keep track of the different device latest updates. 14 | 15 | The different elements are represented in the diagram below: 16 | 17 | 1. KV dictionary. This is the 'truth' exposed to the clients. Clients can get and set KV pairs from any thread. Internally, this sets/gets value on the KV dictionary and access is serialized by the memory queue. 18 | 2. Local database. Access is serialized via the database queue. This is the only database the store can write to. It is also never read except with the initial loading (see below). 19 | 3. Foreign databases. Access is serialized via the database queue. These databases are read-only. The queue remembers the last row read for each of the foreign dbs, so when it comes time to read data from those databases (because they were apparently udpated), only the most recent stuff is read. 20 | 4. Logs cache. The idea is to keep a cache of the rows needed to build the KV dictionary. Its access is serialized as part of the memory queue, so it can be manipulated and queried to be consistent with the KV dictionary wihtout having to care about the databases. The translation between log rows and the KV dictionary is thus completely done within the memory queue, instead of the current messy interplay between the memory and database queue during sync. 21 | 5. Communication between the queues. It's all asynchronous (except for initial loading, see below) and done by passing subsets of log rows, using value semantics (immutable snapshots). 22 | 23 | The diagram: 24 | 25 | 26 | memory queue 27 | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ 28 | KV dictionary 29 | │ ┌─┬────────┬──────────────────┐ │ 30 | client │ │Key1 │Value1 │ 31 | get KV◀─┼────────│ ├────────┼──────────────────┤ │ 32 | ┌────▶│ │Key2 │Value2 │ 33 | │ │ │ ├────────┼──────────────────┤ │ 34 | │ │ │Key3 │Value3 │ 35 | │ │ ┌─▶└─┴────────┴──────────────────┘ │ 36 | │ │ 37 | │ │ │ │ 38 | client │ │ Logs cache (A = local device) 39 | set KV──┼──┤ │ ┌──────┬──────┬──────┬──────┐ │ database queue 40 | │ │ │DEVICE│TIME │KEY │VALUE │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ 41 | │ │ │ └──────┴──────┴──────┴──────┘ │ local db 42 | │ │ ┌─┬──────┬──────┬──────┬──────┬─┐ │ ┌──────┐ │ 43 | │ │ │ │ │A │ │ │ │ │ │ ├──────┤ 44 | └────▶│ ├──────┼──────┼──────┼──────┤ │─────┼─▶├──────┤ │ 45 | │ │ │ │A │ │ │ │ │ │ └──────┘ 46 | │ └─┴──────┴──────┴──────┴──────┴─┘ │ │ 47 | │ │ ┌─┬──────┬──────┬──────┬──────┬─┐ │ foreign dbs 48 | │ │ │B │ │ │ │ │ │ ┌──────┐ │ 49 | │ │ │ ├──────┼──────┼──────┼──────┤ │ │┌────├──────┤ 50 | │ │ │B │ │ │ │ │ │ │ ├──────┤ │ 51 | │ └──│ ├──────┼──────┼──────┼──────┤ │◀─┼┤ └──────┘ 52 | │ │B │ │ │ │ │ │ │ ┌──────┐ │ 53 | │ │ ├──────┼──────┼──────┼──────┤ │ ││ ├──────┤ 54 | │ │C │ │ │ │ │ └─┼──├──────┤ │ 55 | │ └─┴──────┴──────┴──────┴──────┴─┘ │ └──────┘ 56 | ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ─ ─ ┘ 57 | 58 | [Created with Monodraw] 59 | 60 | Now for the different events and how they are handled in the above: 61 | 62 | 1. Initial loading. This is a special step that only needs to happens once, as the very first thing to do, and that requires a synchronous call from the memory queue into the database queue. The database queues reads all the databases and gather all the rows for all the possible key+device configurations (only keeping the most recent timestamps for each), and sends that back to the memory queue, which populates the log cache, then the KV dictionary. 63 | 2. Client sets a new KV pair. The memory queue updates the corresponding KV entry in the dictionary. It also updates the row in the cache, and send a copy of that row asynchronously to the database queue for saving. 64 | 3. Foreign database gets updated ("sync event"). The database reads all the foreign database up to the previous point (fs events cannot tell which database was changed, so they need to be all read), aggregates the results into a bunch of log rows, and sends a copy asynchronously to the memory queue. The memory queue updates its log cache. Since both queues are serial, updates will always come in the correct order even if there are multiple sync events triggered. So the memory queue can overwrite the corresponding rows if relevant (only keeping one row per device/key combination). From the new cache, we can update the KV dictionary. 65 | 66 | Except for the initial loading, the flow for the local database is always in the same direction, and the flow for foreign databases is the other way around: 67 | 68 | 69 | memory queue database queue 70 | ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ 71 | │ 72 | │ KV dict ──▶ logs cache ────┼──▶ local db │ 73 | │ 74 | │ │ │ 75 | KV dict ◀── logs cache ◀─┼───── foreign db 76 | │ │ │ 77 | ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ─ ─ ─ ─ ─ ─ ─ ─ 78 | 79 | [Created with Monodraw] 80 | 81 | In the above, we still have the option to manage the database open/close as we like, see point (2) in the outline: either keep everything opened all the time; or more likely, keep the local database open for a little while after the last save (in case more is in the way), and close the foreign databases right after use (since sync events should be rare anyway). -------------------------------------------------------------------------------- /Core/PARStore.h: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Authors: Charles Parnot and Joris Kluivers 3 | // Licensed under the terms of the BSD License, see license terms in 'LICENSE-BSD.txt' 4 | 5 | #import "PARDispatchQueue.h" 6 | 7 | 8 | /// Key-value store for local storage of app data, with the following characteristics: 9 | /// - persistent storage in a file package 10 | /// - transparent syncing between multiple devices sharing the file via Dropbox, iCloud, or other file-based syncing system 11 | /// - includes full history 12 | 13 | /// Note on memory management: before releasing a store object, it is recommended to call `saveNow` or `waitUntilFinished`, but not necessary. Any change will schedule a save operation that will still be performed and the object will still be retained until that happens. It is not necessary to explicitly `close` a store. 14 | 15 | // TODO: add documentation to methods, also include whether the method will hit the db or not 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @class PARChange; 20 | 21 | /// @name Notifications 22 | /// Notifications are posted asynchronously. You cannot expect the store to be in the state that it was after the last operation that triggered the notification. The 'Change' and 'Sync' notifications includes a user info dictionary with two entries @"values" and @"timestamps"; each entry contain a dictionary where the keys correspond to the keys changed by the sync, and the values corresponding property list values and timestamps, respectively. In the case of 'Sync' notifications, these are the same dictionaries as the one passed to the method `applySyncChangeWithValues:timestamps:`. 23 | 24 | extern NSString *PARStoreDidLoadNotification; 25 | extern NSString *PARStoreDidTearDownNotification; 26 | extern NSString *PARStoreDidDeleteNotification; 27 | extern NSString *PARStoreDidChangeNotification; 28 | extern NSString *PARStoreDidSyncNotification; 29 | 30 | @interface PARStore : NSObject 31 | 32 | /// @name Creating and Loading 33 | + (instancetype)storeWithURL:(nullable NSURL *)url deviceIdentifier:(NSString *)identifier; 34 | - (instancetype)initWithURL:(nullable NSURL *)url deviceIdentifier:(NSString *)identifier; 35 | + (instancetype)inMemoryStore; 36 | - (void)load; 37 | - (void)closeDatabase; 38 | - (void)tearDown; 39 | 40 | /// @name Getting Store Information 41 | @property (readonly, copy, nullable) NSURL *storeURL; 42 | @property (readonly, copy) NSString *deviceIdentifier; 43 | @property (readonly, copy) NSArray *foreignDeviceIdentifiers; 44 | @property (readonly) BOOL loaded; 45 | @property (readonly) BOOL deleted; 46 | @property (readonly) BOOL inMemory; 47 | @property (readonly) BOOL inMemoryCacheEnabled; 48 | @property (readonly) BOOL fileCoordinationEnabled; 49 | 50 | /// @name Memory Cache 51 | - (void)disableInMemoryCache; 52 | 53 | /// @name File Coordination and Presentation 54 | - (void)disableFileCoordination; 55 | 56 | /// @name Adding and Accessing Values 57 | - (nullable id)propertyListValueForKey:(NSString *)key; 58 | - (void)setPropertyListValue:(nullable id)plist forKey:(NSString *)key; 59 | - (NSArray *)allKeys; 60 | - (NSDictionary *)allEntries; 61 | - (void)setEntriesFromDictionary:(NSDictionary *)dictionary NS_SWIFT_NAME(setEntries(from:)); 62 | - (void)setEntriesFromDictionary:(NSDictionary *)dictionary timestampApplied:(NSNumber * __autoreleasing _Nonnull * _Nullable)returnTimestamp NS_SWIFT_NAME(setEntries(from:timestampApplied:)); 63 | 64 | - (void)runTransaction:(PARDispatchBlock)block; 65 | 66 | /// @name Adding and Accessing Blobs 67 | - (BOOL)writeBlobData:(NSData *)data toPath:(NSString *)path error:(NSError **)error; 68 | - (BOOL)writeBlobFromPath:(NSString *)sourcePath toPath:(NSString *)path error:(NSError **)error; 69 | - (nullable NSData *)blobDataAtPath:(NSString *)path error:(NSError **)error; 70 | - (BOOL)deleteBlobAtPath:(NSString *)path error:(NSError **)error; 71 | - (nullable NSString *)absolutePathForBlobPath:(NSString *)path; 72 | - (NSArray *)absolutePathsForBlobsPrefixedBy:(NSString *)prefix NS_SWIFT_NAME(absolutePaths(forBlobsPrefixedBy:)); 73 | - (void)enumerateBlobs:(void(^)(NSString *path))block; 74 | 75 | /// @name Syncing 76 | - (void)sync; 77 | 78 | // These methods should not be called from within a transaction, or they will fail. 79 | - (NSArray *)fetchAllKeys; 80 | - (nullable id)fetchPropertyListValueForKey:(NSString *)key; 81 | - (nullable id)fetchPropertyListValueForKey:(NSString *)key timestamp:(nullable NSNumber *)timestamp; 82 | // for subclassing 83 | - (void)applySyncChangeWithValues:(NSDictionary *)values timestamps:(NSDictionary *)timestamps NS_REQUIRES_SUPER; 84 | 85 | /// @name Merging 86 | - (void)mergeStore:(PARStore *)store unsafeDeviceIdentifiers:(NSArray *)activeDeviceIdentifiers completionHandler:(nullable void(^)(NSError*))completionHandler; 87 | 88 | /// @name Getting Timestamps 89 | + (NSNumber *)timestampNow; 90 | + (NSNumber *)timestampForDistantPast; 91 | + (NSNumber *)timestampForDistantFuture; 92 | 93 | - (NSDictionary *)mostRecentTimestampsByKey; 94 | - (nullable NSNumber *)mostRecentTimestampForKey:(NSString *)key; 95 | // These methods should not be called from within a transaction, or they will fail. 96 | - (NSDictionary *)mostRecentTimestampsByDeviceIdentifier; 97 | - (nullable NSNumber *)mostRecentTimestampForDeviceIdentifier:(nullable NSString *)deviceIdentifier; 98 | 99 | /// @name Synchronous Method Calls 100 | // Synchronous calls can potentially result in longer wait, and should be avoided in the main thread. These should not be called from within a transaction, or they will fail. 101 | // In addition, syncing and saving should normally be triggered automatically and asynchronously. 102 | - (void)loadNow; 103 | - (void)syncNow; 104 | - (void)saveNow; 105 | - (void)closeDatabaseNow; 106 | - (void)tearDownNow; 107 | - (void)waitUntilFinished; 108 | 109 | /// @name History 110 | // This method returns an array of PARChange instances. It should not be called from within a transaction, or it will fail. 111 | - (NSArray *)fetchChangesSinceTimestamp:(nullable NSNumber *)timestamp; 112 | 113 | /// This method returns an array of PARChange instances for the device identifier passed in. 114 | /// It should not be called from within a transaction, or it will fail. 115 | /// Pass in nil for the device identifier to get results for all devices. 116 | - (NSArray *)fetchChangesSinceTimestamp:(nullable NSNumber *)timestamp forDeviceIdentifier:(nullable NSString *)deviceIdentifier; 117 | 118 | /// This method returns an array of PARChange instances for the device identifier passed in, between (and including) the timestamps passed. 119 | /// It should not be called from within a transaction, or it will fail. 120 | /// Pass in nil for the device identifier to get results for all devices. 121 | /// Pass nil for either timestamp to have an open range. 122 | - (NSArray *)fetchChangesFromTimestamp:(nullable NSNumber *)firstTimestamp toTimestamp:(nullable NSNumber *)lastTimestamp forDeviceIdentifier:(nullable NSString *)deviceIdentifier; 123 | 124 | /// Fetches the most recent predecessor of each change passed in, for the device passed in. If the device passed in is `nil`, 125 | /// it gives back the predecessor from any device. 126 | /// The returned dictionary contains the predecessors by `key` atribute. 127 | /// If a key is missing from the dictionary, no predecessor was found for that key. 128 | - (NSDictionary *)fetchMostRecentPredecessorsOfChanges:(NSArray *)changes forDeviceIdentifier:(nullable NSString *)deviceIdentifier; 129 | 130 | /// Fetches the most recent successor of each change passed in, for the device passed in. If the device passed in is `nil`, 131 | /// it gives back the successor from any device. 132 | /// The returned dictionary contains the successors by `key` atribute. 133 | /// If a key is missing from the dictionary, no successor was found for that key (ie the change itself is most recent). 134 | - (NSDictionary *)fetchMostRecentSuccessorsOfChanges:(NSArray *)changes forDeviceIdentifier:(nullable NSString *)deviceIdentifier; 135 | 136 | /// Returns an array representing the most recent set of changes matching a given key prefix. A single device can be passed in, or nil, 137 | /// to search across all devices. 138 | - (NSArray *)fetchMostRecentChangesMatchingKeyPrefix:(NSString *)prefix forDeviceIdentifier:(nullable NSString *)fetchDeviceIdentifier; 139 | 140 | // TODO: error handling 141 | 142 | @end 143 | 144 | 145 | // Accesss for backends that need to import data from other devices via cloud services. 146 | @interface PARStore (RemoteUpdates) 147 | 148 | /// Inserts the changes passed for the device given. 149 | /// If `appendOnly` is false, it will insert the new changes regardless of the existing stored changes, 150 | /// though it will avoid inserting duplicates. 151 | /// If `appendOnly` is true, it will only insert a change that occurs on or after the most recent change in the store. 152 | /// Returns `NO` on failure, and the `error` is set on failure. 153 | - (BOOL)insertChanges:(NSArray *)changes forDeviceIdentifier:(NSString *)deviceIdentifier appendOnly:(BOOL)appendOnly error:(NSError * __autoreleasing *)error; 154 | 155 | @end 156 | 157 | 158 | @interface PARChange : NSObject 159 | + (PARChange *)changeWithTimestamp:(NSNumber *)timestamp parentTimestamp:(nullable NSNumber *)parentTimestamp key:(NSString *)key propertyList:(nullable id)propertyList; 160 | + (PARChange *)changeWithPropertyDictionary:(NSDictionary *)propertyDictionary; 161 | @property (readonly, copy) NSNumber *timestamp; 162 | @property (readonly, copy, nullable) NSNumber *parentTimestamp; 163 | @property (readonly, copy) NSString *key; 164 | @property (readonly, copy, nullable) id propertyList; 165 | @property (readonly, copy) NSDictionary *propertyDictionary; 166 | - (BOOL)isEqual:(nullable id)object; 167 | @end 168 | 169 | 170 | NS_ASSUME_NONNULL_END 171 | 172 | /** Subclassing notes: 173 | 174 | - `-applySyncChangeWithValues:timestamps` --> Default implementation updates the internal representation of the store to include the change. Subclasses can override as follows: 175 | 176 | 1. inspect the change to detect conflicts 177 | 2. call `[super applySyncChangeWithValues:values timestamps:timestamps]` 178 | 3. further change the store if necessary to resolve conflicts 179 | 180 | Note: the state of the store is guaranteed to be consistent during this call, by serializing access to the store. This also means the implementation may block the main thread if user input happens while the method is running. It is thus recommended to keep this implementation as fast as possible. 181 | */ 182 | -------------------------------------------------------------------------------- /Tests/PARDispatchQueueTests.m: -------------------------------------------------------------------------------- 1 | // PARDispatchQueueTests.m 2 | // Created by Charles Parnot on 11/2/12. 3 | // Copyright (c) 2012 Charles Parnot. All rights reserved. 4 | 5 | #import 6 | #import "PARDispatchQueue.h" 7 | 8 | @interface PARDispatchQueueTests : XCTestCase 9 | 10 | @end 11 | 12 | 13 | @implementation PARDispatchQueueTests 14 | 15 | 16 | #pragma mark - Current Queue 17 | 18 | - (void)testIsCurrentQueue 19 | { 20 | __block BOOL isCurrentQueue = NO; 21 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 22 | [queue dispatchSynchronously:^{ isCurrentQueue = [queue isCurrentQueue]; }]; 23 | XCTAssertTrue(isCurrentQueue, @"isCurrentQueue should be true when called from inside a block dispatched to that queue"); 24 | } 25 | 26 | - (void)testIsNotCurrentQueue 27 | { 28 | __block BOOL isCurrentQueue = NO; 29 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 30 | [queue dispatchSynchronously:^{ isCurrentQueue = [queue isCurrentQueue]; }]; 31 | XCTAssertFalse([queue isCurrentQueue], @"isCurrentQueue should be false when called from outside the queue"); 32 | } 33 | 34 | - (void)testIsNotCurrentQueueEvenIfActive 35 | { 36 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 37 | [queue dispatchAsynchronously:^{ [NSThread sleepForTimeInterval:10.0]; }]; 38 | XCTAssertFalse([queue isCurrentQueue], @"isCurrentQueue should be false when called from outside the queue, even if the queue is otherwise active"); 39 | } 40 | 41 | - (void)testIsCurrentQueueWithinBlock 42 | { 43 | __block BOOL isCurrentQueue = NO; 44 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 45 | [queue dispatchSynchronously:^{ 46 | PARDispatchBlock block = ^{ isCurrentQueue = [queue isCurrentQueue]; }; 47 | block(); 48 | }]; 49 | XCTAssertTrue(isCurrentQueue, @"isCurrentQueue should be true when called from inside a block in a block dispatched to that queue"); 50 | } 51 | 52 | - (void)testIsCurrentQueueWithinNativeQueue 53 | { 54 | __block BOOL isCurrentQueue = NO; 55 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 56 | [queue1 dispatchSynchronously:^ 57 | { 58 | dispatch_queue_t queue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); 59 | dispatch_sync(queue2, ^{ isCurrentQueue = [queue1 isCurrentQueue]; }); 60 | }]; 61 | XCTAssertTrue(!isCurrentQueue, @"isCurrentQueue should (unfortunately) be false when called from inside a block dispatched to that queue, because that call is itself called in another queue down the queue stack that is not using the PARDispatchQueue APIs"); 62 | } 63 | 64 | - (void)testIsInCurrentQueueStack 65 | { 66 | __block BOOL isCurrentQueue = NO; 67 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 68 | PARDispatchQueue *queue2 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 69 | 70 | // both synchronous 71 | [queue1 dispatchSynchronously:^ 72 | { 73 | [queue2 dispatchSynchronously:^{ isCurrentQueue = [queue1 isInCurrentQueueStack]; }]; 74 | }]; 75 | 76 | XCTAssertTrue(isCurrentQueue, @"isCurrentQueue should be true when called from inside a block dispatched to that queue, even if that call is itself called in another queue down the queue hierarchy"); 77 | 78 | } 79 | 80 | - (void)testIsCurrentQueueWithinDispatchQueueAsync 81 | { 82 | __block BOOL isCurrentQueue = NO; 83 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 84 | PARDispatchQueue *queue2 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 85 | 86 | // both synchronous 87 | PARBlockOperation *operation = [PARBlockOperation dispatchedOperationWithQueue:queue1 block:^ 88 | { 89 | [queue2 dispatchSynchronously:^{ isCurrentQueue = [queue2 isCurrentQueue]; }]; 90 | }]; 91 | [operation waitUntilFinished]; 92 | XCTAssertTrue(isCurrentQueue, @"isCurrentQueue should be true when called from inside a block dispatched to that queue, even if that call is itself called in another queue down the queue hierarchy"); 93 | } 94 | 95 | - (void)testIsNotCurrentQueueWithinDifferentQueue 96 | { 97 | __block BOOL isCurrentQueue = NO; 98 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 99 | PARDispatchQueue *queue2 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 100 | 101 | // both synchronous 102 | [queue1 dispatchSynchronously:^ 103 | { 104 | isCurrentQueue = [queue2 isCurrentQueue]; 105 | }]; 106 | 107 | XCTAssertFalse(isCurrentQueue, @"isCurrentQueue should be false when called from within another queue"); 108 | 109 | } 110 | 111 | 112 | #pragma mark - Timers 113 | 114 | // timers can't be guaranteed to fire at the time set, but unless the computer is under heavy load, it will work well enough for the DELAY_TIME to be over while waiting for at least SLEEP_TIME 115 | #define DELAY_TIME 0.010 116 | #define SLEEP_TIME 0.012 117 | 118 | - (void)testTimerSchedule 119 | { 120 | __block BOOL done = NO; 121 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 122 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ done = YES; }]; 123 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 124 | XCTAssertTrue(done, @"timer should have fired after and should have set the 'done' flag to YES"); 125 | } 126 | 127 | - (void)testTimerCancel 128 | { 129 | __block BOOL done = NO; 130 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 131 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ done = YES; }]; 132 | [queue1 cancelTimerWithName:@"flag"]; 133 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 134 | XCTAssertFalse(done, @"timer was canceled and should have not set the 'done' flag to YES"); 135 | } 136 | 137 | // replacing the timer with a new delay should execute the second block, not the firt one 138 | - (void)testTimerReplaceAndDelay 139 | { 140 | __block NSUInteger flag = 0; 141 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 142 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 1; }]; 143 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 2; }]; 144 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 145 | NSUInteger expected = 2; 146 | XCTAssertEqual(flag, expected, @"timer should have set the flag value to %@, but it is %@", @(expected), @(flag)); 147 | } 148 | 149 | // replacing the timer with a new delay should execute the second block, but only after the new delay 150 | - (void)testTimerReplaceAndDelayLonger 151 | { 152 | __block NSUInteger flag = 0; 153 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 154 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 1; }]; 155 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME+SLEEP_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 2; }]; 156 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 157 | NSUInteger expected = 2; 158 | XCTAssertFalse(flag == expected, @"timer should have set the flag value to %@ yet, but it is %@", @(expected), @(flag)); 159 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 160 | XCTAssertEqual(flag, expected, @"timer should have now set the flag value to %@, but it is %@", @(expected), @(flag)); 161 | } 162 | 163 | // replacing the timer by coalescing a second timer should execute the second block, not the first one 164 | - (void)testTimerReplaceAndCoalesce 165 | { 166 | __block NSUInteger flag = 0; 167 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 168 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 1; }]; 169 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorCoalesce block:^{ flag = 2; }]; 170 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 171 | NSUInteger expected = 2; 172 | XCTAssertEqual(flag, expected, @"timer should have set the flag value to %@, but it is %@", @(expected), @(flag)); 173 | } 174 | 175 | // coalescing the second timer should not reset the timer delay, but still execute the second block at the target time set by the first timer 176 | - (void)testTimerReplaceAndCoalesceLonger 177 | { 178 | __block NSUInteger flag = 0; 179 | PARDispatchQueue *queue1 = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 180 | [queue1 scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorDelay block:^{ flag = 1; }]; 181 | [queue1 scheduleTimerWithName:@"flag" timeInterval:SLEEP_TIME+SLEEP_TIME behavior:PARTimerBehaviorCoalesce block:^{ flag = 2; }]; 182 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 183 | NSUInteger expected = 2; 184 | XCTAssertEqual(flag, expected, @"timer should have set the flag value to %@, but it is %@", @(expected), @(flag)); 185 | } 186 | 187 | // coalescing the second timer should reset the timer delay because it is shorter, and execute the second block at the target time set by the second timer 188 | - (void)testTimerReplaceAndCoalesceShorter 189 | { 190 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 191 | 192 | // schedule timer with 2 different blocks, where the second one should dictate the timing and the flag value 193 | __block NSDate *blockDate = nil; 194 | __block NSUInteger flag = 0; 195 | [queue scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME behavior:PARTimerBehaviorCoalesce block:^ 196 | { 197 | blockDate = [NSDate date]; 198 | flag = 1; 199 | }]; 200 | NSDate *startDate = [NSDate date]; 201 | [queue scheduleTimerWithName:@"flag" timeInterval:DELAY_TIME / 2.0 behavior:PARTimerBehaviorCoalesce block:^ 202 | { 203 | blockDate = [NSDate date]; 204 | flag = 2; 205 | }]; 206 | [NSThread sleepForTimeInterval:SLEEP_TIME]; 207 | 208 | // check the flag value 209 | NSUInteger expectedFlag = 2; 210 | XCTAssertEqual(flag, expectedFlag, @"timer should have set the flag value to %@, but it is %@", @(expectedFlag), @(flag)); 211 | 212 | // check the timing 213 | NSTimeInterval expectedDelay = DELAY_TIME / 2.0; 214 | NSTimeInterval actualDelay = [blockDate timeIntervalSinceDate:startDate]; 215 | XCTAssertEqualWithAccuracy(expectedDelay, actualDelay, 0.001, @"expected delay of execution %@ but it was %@", @(expectedDelay), @(actualDelay)); 216 | } 217 | 218 | // when **scheduling** a timer while the queue is busy, it should not affect the execution delay (assuming the queue is not busy when it's time to **execute** the scheduled block) 219 | - (void)testBusyQueueNotAffectingDelay 220 | { 221 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 222 | 223 | // keep the queue busy 224 | [queue dispatchAsynchronously:^{ [NSThread sleepForTimeInterval:DELAY_TIME / 2.0]; }]; 225 | 226 | // schedule the timer while the queue is busy 227 | NSTimeInterval expectedDelay = DELAY_TIME; 228 | __block NSTimeInterval actualDelay = 0; 229 | NSDate *startDate = [NSDate date]; 230 | [queue scheduleTimerWithName:@"flag" timeInterval:expectedDelay behavior:PARTimerBehaviorDelay block:^ 231 | { 232 | actualDelay = [[NSDate date] timeIntervalSinceDate:startDate]; 233 | }]; 234 | 235 | // by the time the timer fires, the queue should be ready to execute the timer block 236 | // we add more sleep time to account for the case where the test actually fails, and the timer is further delayed by the first block sent to the queue above 237 | [NSThread sleepForTimeInterval:2.0 * SLEEP_TIME]; 238 | 239 | XCTAssertEqualWithAccuracy(expectedDelay, actualDelay, 0.001, @"expected delay of execution %@ but it was %@", @(expectedDelay), @(actualDelay)); 240 | } 241 | 242 | - (void)testNow 243 | { 244 | // dispatch queue just used to have accessed to the `_now` method 245 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 246 | 247 | NSTimeInterval expectedElapsed = 1.0; 248 | NSTimeInterval t1 = [[NSDate date] timeIntervalSinceReferenceDate]; 249 | NSTimeInterval u1 = [[queue valueForKey:@"_now"] doubleValue]; 250 | [NSThread sleepForTimeInterval:expectedElapsed]; 251 | NSTimeInterval t2 = [[NSDate date] timeIntervalSinceReferenceDate]; 252 | NSTimeInterval u2 = [[queue valueForKey:@"_now"] doubleValue]; 253 | 254 | XCTAssertEqualWithAccuracy(t2 - t1, expectedElapsed, 0.01, @"expected elpased time: %@", @(expectedElapsed)); 255 | XCTAssertEqualWithAccuracy(u2 - u1, expectedElapsed, 0.01, @"expected elpased time: %@", @(expectedElapsed)); 256 | XCTAssertEqualWithAccuracy(t2 - t1, u2 - u1, 0.01, @"expected elpased time: %@", @(expectedElapsed)); 257 | } 258 | 259 | - (void)testTimerWithThrottleBehavior 260 | { 261 | PARDispatchQueue *queue = [PARDispatchQueue dispatchQueueWithLabel:NSStringFromSelector(_cmd)]; 262 | 263 | __block NSDate *start = [NSDate date]; 264 | __block NSDate *date1 = nil; 265 | __block NSDate *date2 = nil; 266 | NSTimeInterval throttleDelay = 0.2; 267 | 268 | // first timer should be fired very quickly 269 | dispatch_semaphore_t sema = dispatch_semaphore_create(0); 270 | [queue scheduleTimerWithName:@"throttle" timeInterval:throttleDelay behavior:PARTimerBehaviorThrottle block:^{ 271 | date1 = [NSDate date]; 272 | NSLog(@"date1: %@", date1); 273 | dispatch_semaphore_signal(sema); 274 | }]; 275 | long waitResult = dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC)); 276 | XCTAssertTrue(waitResult == 0, @"Timeout while waiting for timer 1 to fire"); 277 | 278 | // second timer should only fire after throttle time 279 | sema = dispatch_semaphore_create(0); 280 | [queue scheduleTimerWithName:@"throttle" timeInterval:throttleDelay behavior:PARTimerBehaviorThrottle block:^{ 281 | date2 = [NSDate date]; 282 | NSLog(@"date2: %@", date2); 283 | dispatch_semaphore_signal(sema); 284 | }]; 285 | waitResult = dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, throttleDelay * 2.0 * NSEC_PER_SEC)); 286 | XCTAssertTrue(waitResult == 0, @"Timeout while waiting for timer 2 to fire"); 287 | 288 | NSTimeInterval interval1 = [date1 timeIntervalSinceDate:start]; 289 | NSTimeInterval interval2 = [date2 timeIntervalSinceDate:date1]; 290 | 291 | XCTAssertNotNil(date1); 292 | XCTAssertNotNil(date2); 293 | XCTAssertEqualWithAccuracy(interval1, 0.0, 0.01); 294 | XCTAssertEqualWithAccuracy(interval2, throttleDelay, 0.01); 295 | } 296 | 297 | 298 | #pragma mark - Global Queue 299 | 300 | // with a concurrent queue like the global dispatch queue, PARDispatchQueue should not keep track of the queue stack to try to avoid deadlocks, or it will be very confused 301 | - (void)testGlobalQueueMultipleDispatchesIgnoreQueueStack 302 | { 303 | [[PARDispatchQueue globalDispatchQueue] dispatchAsynchronously:^{[NSThread sleepForTimeInterval:0.02];}]; 304 | [[PARDispatchQueue globalDispatchQueue] dispatchAsynchronously:^{[NSThread sleepForTimeInterval:0.02];}]; 305 | [[PARDispatchQueue globalDispatchQueue] dispatchAsynchronously:^{[NSThread sleepForTimeInterval:0.02];}]; 306 | [[PARDispatchQueue globalDispatchQueue] dispatchAsynchronously:^{[NSThread sleepForTimeInterval:0.02];}]; 307 | [[PARDispatchQueue globalDispatchQueue] dispatchAsynchronously:^{[NSThread sleepForTimeInterval:0.02];}]; 308 | [NSThread sleepForTimeInterval:0.1]; 309 | } 310 | 311 | 312 | 313 | @end 314 | -------------------------------------------------------------------------------- /PARStore Viewer/PARStore Viewer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 566F166A1F90BADF007EA8F9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F16691F90BADF007EA8F9 /* AppDelegate.swift */; }; 11 | 566F166E1F90BADF007EA8F9 /* PARStoreDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F166D1F90BADF007EA8F9 /* PARStoreDocument.swift */; }; 12 | 566F16701F90BADF007EA8F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 566F166F1F90BADF007EA8F9 /* Assets.xcassets */; }; 13 | 566F16731F90BADF007EA8F9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 566F16711F90BADF007EA8F9 /* Main.storyboard */; }; 14 | 566F16821F90BB5B007EA8F9 /* PARDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 566F167D1F90BB5B007EA8F9 /* PARDispatchQueue.m */; }; 15 | 566F16831F90BB5B007EA8F9 /* PARStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 566F167F1F90BB5B007EA8F9 /* PARStore.m */; }; 16 | 566F16841F90BB5B007EA8F9 /* NSError+Factory.m in Sources */ = {isa = PBXBuildFile; fileRef = 566F16811F90BB5B007EA8F9 /* NSError+Factory.m */; }; 17 | 566F16881F90BD0E007EA8F9 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F16871F90BD0E007EA8F9 /* HistoryViewController.swift */; }; 18 | 566F168A1F90BE9C007EA8F9 /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F16891F90BE9C007EA8F9 /* TextViewController.swift */; }; 19 | 566F168C1F90C03A007EA8F9 /* DocumentWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F168B1F90C03A007EA8F9 /* DocumentWindowController.swift */; }; 20 | 566F168E1F90C08A007EA8F9 /* PARStore+ContentDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566F168D1F90C08A007EA8F9 /* PARStore+ContentDump.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 566F16661F90BADF007EA8F9 /* PARStore Viewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PARStore Viewer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 566F16691F90BADF007EA8F9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 566F166D1F90BADF007EA8F9 /* PARStoreDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PARStoreDocument.swift; sourceTree = ""; }; 27 | 566F166F1F90BADF007EA8F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 566F16721F90BADF007EA8F9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 566F16741F90BADF007EA8F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | 566F167B1F90BB5A007EA8F9 /* PARStore Viewer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PARStore Viewer-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; 31 | 566F167C1F90BB5B007EA8F9 /* PARDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PARDispatchQueue.h; path = ../Core/PARDispatchQueue.h; sourceTree = ""; }; 32 | 566F167D1F90BB5B007EA8F9 /* PARDispatchQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PARDispatchQueue.m; path = ../Core/PARDispatchQueue.m; sourceTree = ""; }; 33 | 566F167E1F90BB5B007EA8F9 /* PARStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PARStore.h; path = ../Core/PARStore.h; sourceTree = ""; }; 34 | 566F167F1F90BB5B007EA8F9 /* PARStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PARStore.m; path = ../Core/PARStore.m; sourceTree = ""; }; 35 | 566F16801F90BB5B007EA8F9 /* NSError+Factory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSError+Factory.h"; path = "../Core/NSError+Factory.h"; sourceTree = ""; }; 36 | 566F16811F90BB5B007EA8F9 /* NSError+Factory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSError+Factory.m"; path = "../Core/NSError+Factory.m"; sourceTree = ""; }; 37 | 566F16871F90BD0E007EA8F9 /* HistoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; 38 | 566F16891F90BE9C007EA8F9 /* TextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; 39 | 566F168B1F90C03A007EA8F9 /* DocumentWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentWindowController.swift; sourceTree = ""; }; 40 | 566F168D1F90C08A007EA8F9 /* PARStore+ContentDump.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PARStore+ContentDump.swift"; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 566F16631F90BADF007EA8F9 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 566F165D1F90BADF007EA8F9 = { 55 | isa = PBXGroup; 56 | children = ( 57 | 566F16681F90BADF007EA8F9 /* PARStore Viewer */, 58 | 566F167A1F90BB47007EA8F9 /* PARStore */, 59 | 566F16671F90BADF007EA8F9 /* Products */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 566F16671F90BADF007EA8F9 /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 566F16661F90BADF007EA8F9 /* PARStore Viewer.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 566F16681F90BADF007EA8F9 /* PARStore Viewer */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 566F167B1F90BB5A007EA8F9 /* PARStore Viewer-Bridging-Header.h */, 75 | 566F16691F90BADF007EA8F9 /* AppDelegate.swift */, 76 | 566F166D1F90BADF007EA8F9 /* PARStoreDocument.swift */, 77 | 566F168B1F90C03A007EA8F9 /* DocumentWindowController.swift */, 78 | 566F16871F90BD0E007EA8F9 /* HistoryViewController.swift */, 79 | 566F16891F90BE9C007EA8F9 /* TextViewController.swift */, 80 | 566F168D1F90C08A007EA8F9 /* PARStore+ContentDump.swift */, 81 | 566F166F1F90BADF007EA8F9 /* Assets.xcassets */, 82 | 566F16711F90BADF007EA8F9 /* Main.storyboard */, 83 | 566F16741F90BADF007EA8F9 /* Info.plist */, 84 | ); 85 | path = "PARStore Viewer"; 86 | sourceTree = ""; 87 | }; 88 | 566F167A1F90BB47007EA8F9 /* PARStore */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 566F167C1F90BB5B007EA8F9 /* PARDispatchQueue.h */, 92 | 566F167D1F90BB5B007EA8F9 /* PARDispatchQueue.m */, 93 | 566F167E1F90BB5B007EA8F9 /* PARStore.h */, 94 | 566F167F1F90BB5B007EA8F9 /* PARStore.m */, 95 | 566F16801F90BB5B007EA8F9 /* NSError+Factory.h */, 96 | 566F16811F90BB5B007EA8F9 /* NSError+Factory.m */, 97 | ); 98 | name = PARStore; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 566F16651F90BADF007EA8F9 /* PARStore Viewer */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 566F16771F90BADF007EA8F9 /* Build configuration list for PBXNativeTarget "PARStore Viewer" */; 107 | buildPhases = ( 108 | 566F16621F90BADF007EA8F9 /* Sources */, 109 | 566F16631F90BADF007EA8F9 /* Frameworks */, 110 | 566F16641F90BADF007EA8F9 /* Resources */, 111 | ); 112 | buildRules = ( 113 | ); 114 | dependencies = ( 115 | ); 116 | name = "PARStore Viewer"; 117 | productName = "PARStore Viewer"; 118 | productReference = 566F16661F90BADF007EA8F9 /* PARStore Viewer.app */; 119 | productType = "com.apple.product-type.application"; 120 | }; 121 | /* End PBXNativeTarget section */ 122 | 123 | /* Begin PBXProject section */ 124 | 566F165E1F90BADF007EA8F9 /* Project object */ = { 125 | isa = PBXProject; 126 | attributes = { 127 | LastSwiftUpdateCheck = 0830; 128 | LastUpgradeCheck = 0830; 129 | ORGANIZATIONNAME = "Charles Parnot"; 130 | TargetAttributes = { 131 | 566F16651F90BADF007EA8F9 = { 132 | CreatedOnToolsVersion = 8.3.3; 133 | DevelopmentTeam = C85V3SP3F7; 134 | LastSwiftMigration = 0830; 135 | ProvisioningStyle = Automatic; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 566F16611F90BADF007EA8F9 /* Build configuration list for PBXProject "PARStore Viewer" */; 140 | compatibilityVersion = "Xcode 3.2"; 141 | developmentRegion = English; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 566F165D1F90BADF007EA8F9; 148 | productRefGroup = 566F16671F90BADF007EA8F9 /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 566F16651F90BADF007EA8F9 /* PARStore Viewer */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 566F16641F90BADF007EA8F9 /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 566F16701F90BADF007EA8F9 /* Assets.xcassets in Resources */, 163 | 566F16731F90BADF007EA8F9 /* Main.storyboard in Resources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXResourcesBuildPhase section */ 168 | 169 | /* Begin PBXSourcesBuildPhase section */ 170 | 566F16621F90BADF007EA8F9 /* Sources */ = { 171 | isa = PBXSourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | 566F16841F90BB5B007EA8F9 /* NSError+Factory.m in Sources */, 175 | 566F16821F90BB5B007EA8F9 /* PARDispatchQueue.m in Sources */, 176 | 566F166A1F90BADF007EA8F9 /* AppDelegate.swift in Sources */, 177 | 566F166E1F90BADF007EA8F9 /* PARStoreDocument.swift in Sources */, 178 | 566F16881F90BD0E007EA8F9 /* HistoryViewController.swift in Sources */, 179 | 566F16831F90BB5B007EA8F9 /* PARStore.m in Sources */, 180 | 566F168E1F90C08A007EA8F9 /* PARStore+ContentDump.swift in Sources */, 181 | 566F168A1F90BE9C007EA8F9 /* TextViewController.swift in Sources */, 182 | 566F168C1F90C03A007EA8F9 /* DocumentWindowController.swift in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 566F16711F90BADF007EA8F9 /* Main.storyboard */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 566F16721F90BADF007EA8F9 /* Base */, 193 | ); 194 | name = Main.storyboard; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXVariantGroup section */ 198 | 199 | /* Begin XCBuildConfiguration section */ 200 | 566F16751F90BADF007EA8F9 /* Debug */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | CLANG_ANALYZER_NONNULL = YES; 205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 220 | CLANG_WARN_UNREACHABLE_CODE = YES; 221 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 222 | CODE_SIGN_IDENTITY = "-"; 223 | COPY_PHASE_STRIP = NO; 224 | DEBUG_INFORMATION_FORMAT = dwarf; 225 | ENABLE_STRICT_OBJC_MSGSEND = YES; 226 | ENABLE_TESTABILITY = YES; 227 | GCC_C_LANGUAGE_STANDARD = gnu99; 228 | GCC_DYNAMIC_NO_PIC = NO; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_OPTIMIZATION_LEVEL = 0; 231 | GCC_PREPROCESSOR_DEFINITIONS = ( 232 | "DEBUG=1", 233 | "$(inherited)", 234 | ); 235 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 236 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 237 | GCC_WARN_UNDECLARED_SELECTOR = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 239 | GCC_WARN_UNUSED_FUNCTION = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | MACOSX_DEPLOYMENT_TARGET = 10.12; 242 | MTL_ENABLE_DEBUG_INFO = YES; 243 | ONLY_ACTIVE_ARCH = YES; 244 | SDKROOT = macosx; 245 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 246 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 247 | }; 248 | name = Debug; 249 | }; 250 | 566F16761F90BADF007EA8F9 /* Release */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_ANALYZER_NONNULL = YES; 255 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 264 | CLANG_WARN_EMPTY_BODY = YES; 265 | CLANG_WARN_ENUM_CONVERSION = YES; 266 | CLANG_WARN_INFINITE_RECURSION = YES; 267 | CLANG_WARN_INT_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | CODE_SIGN_IDENTITY = "-"; 273 | COPY_PHASE_STRIP = NO; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | ENABLE_NS_ASSERTIONS = NO; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu99; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | MACOSX_DEPLOYMENT_TARGET = 10.12; 286 | MTL_ENABLE_DEBUG_INFO = NO; 287 | SDKROOT = macosx; 288 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 289 | }; 290 | name = Release; 291 | }; 292 | 566F16781F90BADF007EA8F9 /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | CLANG_ENABLE_MODULES = YES; 297 | COMBINE_HIDPI_IMAGES = YES; 298 | DEVELOPMENT_TEAM = C85V3SP3F7; 299 | INFOPLIST_FILE = "PARStore Viewer/Info.plist"; 300 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 301 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.PARStore-Viewer"; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_OBJC_BRIDGING_HEADER = "PARStore Viewer-Bridging-Header.h"; 304 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 305 | SWIFT_VERSION = 3.0; 306 | }; 307 | name = Debug; 308 | }; 309 | 566F16791F90BADF007EA8F9 /* Release */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | CLANG_ENABLE_MODULES = YES; 314 | COMBINE_HIDPI_IMAGES = YES; 315 | DEVELOPMENT_TEAM = C85V3SP3F7; 316 | INFOPLIST_FILE = "PARStore Viewer/Info.plist"; 317 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 318 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.PARStore-Viewer"; 319 | PRODUCT_NAME = "$(TARGET_NAME)"; 320 | SWIFT_OBJC_BRIDGING_HEADER = "PARStore Viewer-Bridging-Header.h"; 321 | SWIFT_VERSION = 3.0; 322 | }; 323 | name = Release; 324 | }; 325 | /* End XCBuildConfiguration section */ 326 | 327 | /* Begin XCConfigurationList section */ 328 | 566F16611F90BADF007EA8F9 /* Build configuration list for PBXProject "PARStore Viewer" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | 566F16751F90BADF007EA8F9 /* Debug */, 332 | 566F16761F90BADF007EA8F9 /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | 566F16771F90BADF007EA8F9 /* Build configuration list for PBXNativeTarget "PARStore Viewer" */ = { 338 | isa = XCConfigurationList; 339 | buildConfigurations = ( 340 | 566F16781F90BADF007EA8F9 /* Debug */, 341 | 566F16791F90BADF007EA8F9 /* Release */, 342 | ); 343 | defaultConfigurationIsVisible = 0; 344 | }; 345 | /* End XCConfigurationList section */ 346 | }; 347 | rootObject = 566F165E1F90BADF007EA8F9 /* Project object */; 348 | } 349 | -------------------------------------------------------------------------------- /Core/PARDispatchQueue.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Author: Charles Parnot 3 | // Licensed under the terms of the BSD License, as specified in the file 'LICENSE-BSD.txt' included with this distribution 4 | 5 | 6 | #import "PARDispatchQueue.h" 7 | #import 8 | 9 | // keys and context used for the `dispatch_xxx_specific` APIs and to keep track of the stack of queues 10 | static int PARQueueStackKey = 1; 11 | static int PARIsCurrentKey = 1; 12 | 13 | 14 | // private properties 15 | @interface PARDispatchQueue() 16 | @property (nonatomic, strong) dispatch_queue_t queue; 17 | @property (copy) NSString *_label; 18 | @property (strong) NSMutableDictionary *timers; 19 | @property (nonatomic) PARDeadlockBehavior _deadlockBehavior; 20 | @property BOOL concurrent; 21 | @property NSUInteger timerCountPrivate; 22 | @end 23 | 24 | 25 | @implementation PARDispatchQueue 26 | 27 | + (PARDispatchQueue *)dispatchQueueWithGCDQueue:(dispatch_queue_t)gcdQueue behavior:(PARDeadlockBehavior)behavior 28 | { 29 | PARDispatchQueue *newQueue = [[self alloc] init]; 30 | newQueue.queue = gcdQueue; 31 | newQueue._deadlockBehavior = behavior; 32 | dispatch_queue_set_specific(gcdQueue, &PARIsCurrentKey, (__bridge void *)(newQueue), NULL); 33 | return newQueue; 34 | } 35 | 36 | + (PARDispatchQueue *)dispatchQueueWithLabel:(NSString *)label behavior:(PARDeadlockBehavior)behavior 37 | { 38 | PARDispatchQueue *newQueue = [self dispatchQueueWithGCDQueue:dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL) behavior:behavior]; 39 | newQueue._label = label ?: @"PARDispatchQueue"; 40 | return newQueue; 41 | } 42 | 43 | + (PARDispatchQueue *)dispatchQueueWithLabel:(NSString *)label 44 | { 45 | return [self dispatchQueueWithLabel:label behavior:PARDeadlockBehaviorExecute]; 46 | } 47 | 48 | + (NSString *)labelByPrependingBundleIdentifierToString:(NSString *)suffix 49 | { 50 | return [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingFormat:@".%@", suffix]; 51 | } 52 | 53 | - (NSString *)description 54 | { 55 | return [NSString stringWithFormat:@"<%@:%p>[%@]", NSStringFromClass([self class]), self, self.label]; 56 | } 57 | 58 | 59 | #pragma mark - Special Queues 60 | 61 | // using singletons for these queues so that `isCurrentQueue` works as intended 62 | 63 | + (PARDispatchQueue *)globalDispatchQueue 64 | { 65 | static dispatch_once_t pred = 0; 66 | static PARDispatchQueue *globalDispatchQueue = nil; 67 | dispatch_once(&pred, ^ 68 | { 69 | globalDispatchQueue = [self dispatchQueueWithGCDQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) behavior:PARDeadlockBehaviorBlock]; 70 | const char *label = dispatch_queue_get_label(globalDispatchQueue.queue); 71 | if (label == NULL) 72 | label = "unlabeled"; 73 | globalDispatchQueue._label = [NSString stringWithUTF8String:label]; 74 | globalDispatchQueue.concurrent = YES; 75 | }); 76 | return globalDispatchQueue; 77 | } 78 | 79 | // DISPATCH_QUEUE_PRIORITY_HIGH 80 | // DISPATCH_QUEUE_PRIORITY_LOW 81 | 82 | static PARDispatchQueue *PARMainDispatchQueue = nil; 83 | + (PARDispatchQueue *)mainDispatchQueue 84 | { 85 | static dispatch_once_t pred = 0; 86 | dispatch_once(&pred, ^ 87 | { 88 | PARMainDispatchQueue = [self dispatchQueueWithGCDQueue:dispatch_get_main_queue() behavior:PARDeadlockBehaviorExecute]; 89 | const char *label = dispatch_queue_get_label(PARMainDispatchQueue.queue); 90 | if (label == NULL) 91 | label = "unlabeled"; 92 | PARMainDispatchQueue._label = [NSString stringWithUTF8String:label]; 93 | }); 94 | return PARMainDispatchQueue; 95 | } 96 | 97 | 98 | static PARDispatchQueue *PARSharedConcurrentQueue = nil; 99 | + (PARDispatchQueue *)sharedConcurrentQueue 100 | { 101 | static dispatch_once_t pred = 0; 102 | dispatch_once(&pred, ^ 103 | { 104 | NSString *label = @"PARSharedConcurrentQueue"; 105 | PARSharedConcurrentQueue = [self dispatchQueueWithGCDQueue:dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT) behavior:PARDeadlockBehaviorBlock]; 106 | PARSharedConcurrentQueue._label = label; 107 | PARSharedConcurrentQueue.concurrent = YES; 108 | }); 109 | return PARSharedConcurrentQueue; 110 | } 111 | 112 | 113 | #pragma mark - Accessors 114 | 115 | - (NSString *)label 116 | { 117 | return self._label; 118 | } 119 | 120 | - (PARDeadlockBehavior)deadlockBehavior 121 | { 122 | return self._deadlockBehavior; 123 | } 124 | 125 | 126 | #pragma mark - Dispatch 127 | 128 | - (void)dispatchSynchronously:(PARDispatchBlock)block 129 | { 130 | PARDeadlockBehavior behavior = self.deadlockBehavior; 131 | 132 | // dispatch_sync will only deadlock if that's the desired behavior 133 | if (behavior == PARDeadlockBehaviorBlock || ![self isInCurrentQueueStack]) 134 | { 135 | // prepare the new stack before we are inside the queue, so it can be set on the queue 136 | NSMutableArray *queueStack = (__bridge NSMutableArray *)(dispatch_get_specific(&PARQueueStackKey)); 137 | BOOL newStack = NO; 138 | if (!queueStack) 139 | { 140 | queueStack = [NSMutableArray array]; 141 | newStack = YES; 142 | } 143 | 144 | // dispatch_queue_set_specific should be serialized within the queue, so it's consistent from one block execution to the next 145 | dispatch_sync(self.queue, ^ 146 | { 147 | if (!self.concurrent) 148 | [queueStack addObject:self]; 149 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, (__bridge void *)queueStack, NULL); 150 | block(); 151 | NSAssert([queueStack lastObject] == self, @"The queue stack set after execution of a block should have the parent queue as the last object: %@\n Iinstead, it has the following stack: %@", self, queueStack); 152 | if (!self.concurrent) 153 | [queueStack removeLastObject]; 154 | NSAssert(!newStack || [queueStack count] == 0, @"The queue stack should be empty after execution of a block dispatched synchronously that was started without a queue stack yet: %@", self); 155 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, NULL, NULL); 156 | }); 157 | } 158 | 159 | else 160 | { 161 | if (behavior == PARDeadlockBehaviorExecute) 162 | block(); 163 | else if (behavior == PARDeadlockBehaviorLog) 164 | NSLog(@"Synchronous dispatch can not be executed on the queue with label '%@' because it is already part of the current dispatch queue hierarchy", self.label); 165 | else if (behavior == PARDeadlockBehaviorAssert) 166 | NSAssert(0, @"Synchronous dispatch can not be executed on the queue with label '%@' because it is already part of the current dispatch queue hierarchy", self.label); 167 | } 168 | } 169 | 170 | // asynchronous dispatch can only start a new queue stack 171 | - (void)dispatchAsynchronously:(PARDispatchBlock)block 172 | { 173 | if (self.concurrent) 174 | dispatch_async(self.queue, block); 175 | else 176 | dispatch_async(self.queue, ^ 177 | { 178 | NSAssert(dispatch_get_specific(&PARQueueStackKey) == NULL, @"There should be no queue stack set before execution of a block dispatched asynchronously with queue: %@", self); 179 | NSMutableArray *queueStack = [NSMutableArray arrayWithObject:self]; 180 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, (__bridge void *)queueStack, NULL); 181 | block(); 182 | NSAssert([queueStack lastObject] == self, @"The queue stack set after execution of a block should have the parent queue as the last object: %@\n Iinstead, it has the following stack: %@", self, queueStack); 183 | [queueStack removeLastObject]; 184 | NSAssert([queueStack count] == 0, @"The queue stack should be empty after execution of a block dispatched asynchronously with queue: %@", self); 185 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, NULL, NULL); 186 | }); 187 | } 188 | 189 | - (void)dispatchBarrierSynchronously:(PARDispatchBlock)block 190 | { 191 | dispatch_barrier_sync(self.queue, block); 192 | } 193 | 194 | - (void)dispatchBarrierAsynchronously:(PARDispatchBlock)block 195 | { 196 | dispatch_barrier_async(self.queue, block); 197 | } 198 | 199 | // see: https://devforums.apple.com/message/710745 for why using dispatch_get_current_queue() is not a good way to check the current queue, and why it's deprecated in iOS 6.0 200 | - (BOOL)isCurrentQueue 201 | { 202 | return (dispatch_get_specific(&PARIsCurrentKey) == (__bridge void *)(self)); 203 | } 204 | 205 | - (BOOL)isInCurrentQueueStack 206 | { 207 | // main queue is easier and safer to assert 208 | if (self == PARMainDispatchQueue) 209 | return [NSThread isMainThread]; 210 | 211 | NSArray *queueStack = (__bridge NSArray *)(dispatch_get_specific(&PARQueueStackKey)); 212 | return [queueStack containsObject:self]; 213 | } 214 | 215 | #pragma mark - Timers 216 | 217 | - (NSTimeInterval)_now 218 | { 219 | static mach_timebase_info_data_t info; 220 | static dispatch_once_t pred; 221 | dispatch_once(&pred, ^{ mach_timebase_info(&info); }); 222 | 223 | NSTimeInterval t = mach_absolute_time(); 224 | t *= info.numer; 225 | t /= info.denom; 226 | return t / NSEC_PER_SEC; 227 | } 228 | 229 | - (BOOL)_scheduleTimerWithName:(NSString *)name referenceTime:(NSTimeInterval)time timeInterval:(NSTimeInterval)delay behavior:(PARTimerBehavior)behavior block:(PARDispatchBlock)block 230 | { 231 | if (!self.timers) 232 | self.timers = [NSMutableDictionary dictionary]; 233 | 234 | NSDictionary *timerInfo = self.timers[name]; 235 | NSNumber *dateValue = timerInfo[@"DateValue"]; 236 | NSValue *timerValue = timerInfo[@"TimerValue"]; 237 | if (timerValue == nil && behavior != PARTimerBehaviorThrottle) 238 | dateValue = nil; 239 | 240 | // get the underlying dispatch timer 241 | dispatch_source_t dispatchTimer = NULL; 242 | if (timerValue) 243 | dispatchTimer = (dispatch_source_t)[timerValue pointerValue]; 244 | else 245 | dispatchTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue); 246 | if (!dispatchTimer) 247 | return NO; 248 | 249 | // adjust firing time as needed: 250 | NSTimeInterval now = [self _now]; 251 | NSTimeInterval newFireTime; 252 | 253 | // - wait behavior: fire now or last date + delay 254 | if (behavior == PARTimerBehaviorThrottle) 255 | { 256 | // timer already set: already throttled 257 | if (timerValue != nil) 258 | { 259 | newFireTime = dateValue.doubleValue; 260 | } 261 | 262 | else 263 | { 264 | // never fired or already past the throttle delay --> fire now 265 | newFireTime = dateValue.doubleValue + delay; 266 | if (dateValue == nil || newFireTime <= now) 267 | { 268 | newFireTime = now; 269 | // it seems we should not use a time interval of zero, to avoid warning: "DEPRECATED USE in libdispatch client: Setting timer interval to 0 requests a 1ns timer, did you mean FOREVER (a one-shot timer)?" 270 | dispatch_source_set_timer(dispatchTimer, dispatch_time(DISPATCH_TIME_NOW, 1), 0, 0); 271 | } 272 | 273 | // fired before --> apply throttle 274 | else 275 | { 276 | NSTimeInterval adjustedDelay = newFireTime - now; 277 | dispatch_source_set_timer(dispatchTimer, dispatch_time(DISPATCH_TIME_NOW, adjustedDelay * NSEC_PER_SEC), 0, 0); 278 | } 279 | } 280 | } 281 | 282 | // - coalesce behavior: only take into account earlier-than-currently-set firing 283 | // - delay behavior: firing always at now + delay 284 | else 285 | { 286 | NSTimeInterval adjustedDelay = delay - (now - time); 287 | if (adjustedDelay < 0.0) 288 | adjustedDelay = 0.0; 289 | NSTimeInterval fireTime = [dateValue doubleValue]; 290 | newFireTime = now + adjustedDelay; 291 | if (dateValue == nil || behavior == PARTimerBehaviorDelay || newFireTime < fireTime) 292 | { 293 | dispatch_source_set_timer(dispatchTimer, dispatch_time(DISPATCH_TIME_NOW, adjustedDelay * NSEC_PER_SEC), 0, 0); 294 | } 295 | } 296 | 297 | // set the new event handler 298 | if (self.concurrent) 299 | dispatch_source_set_event_handler(dispatchTimer, ^ 300 | { 301 | block(); 302 | [self _cancelTimerWithName:name]; 303 | }); 304 | else 305 | dispatch_source_set_event_handler(dispatchTimer, ^ 306 | { 307 | NSAssert(dispatch_get_specific(&PARQueueStackKey) == NULL, @"There should be no queue stack set before execution of a block dispatched asynchronously by timer '%@' with queue: %@", name, self); 308 | NSMutableArray *queueStack = [NSMutableArray arrayWithObject:self]; 309 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, (__bridge void *)queueStack, NULL); 310 | block(); 311 | NSAssert([queueStack lastObject] == self, @"The queue stack set after execution of a block should have the parent queue as the last object: %@\n Iinstead, it has the following stack: %@", self, queueStack); 312 | [queueStack removeLastObject]; 313 | NSAssert([queueStack count] == 0, @"The queue stack should be empty after execution of a block dispatched asynchronously by timer '%@' with queue: %@", name, self); 314 | dispatch_queue_set_specific(self.queue, &PARQueueStackKey, NULL, NULL); 315 | [self _cancelTimerWithName:name]; 316 | }); 317 | 318 | // new timers need to be retained and activated 319 | if (!timerValue) 320 | { 321 | timerValue = [NSValue valueWithPointer:(__bridge_retained const void *)dispatchTimer]; 322 | dispatch_resume(dispatchTimer); 323 | } 324 | 325 | // update timer info 326 | self.timers[name] = @{ @"DateValue" : @(newFireTime), @"TimerValue" : timerValue }; 327 | [self _updateTimerCountPrivate]; 328 | 329 | return YES; 330 | } 331 | 332 | - (void)_cancelTimerWithName:(NSString *)name 333 | { 334 | NSDictionary *timerInfo = self.timers[name]; 335 | NSValue *timerValue = timerInfo[@"TimerValue"]; 336 | if (!timerValue) 337 | return; 338 | NSNumber *dateValue = timerInfo[@"DateValue"]; 339 | 340 | // because we are using NSValue, we need to do the memory management ourselves 341 | dispatch_source_t dispatchTimer = (__bridge_transfer dispatch_source_t)[timerValue pointerValue]; 342 | dispatch_source_cancel(dispatchTimer); 343 | // **from here on, the timerValue pointer can't be used anymore!** 344 | 345 | // for 'throttle' behavior, we need to keep track of last firing date, but still remove the timer value itself 346 | self.timers[name] = @{ @"DateValue" : dateValue }; 347 | [self _updateTimerCountPrivate]; 348 | } 349 | 350 | - (void)_updateTimerCountPrivate 351 | { 352 | NSUInteger count = 0; 353 | for (NSDictionary *timerDescription in self.timers.allValues) 354 | { 355 | if (timerDescription[@"TimerValue"] != nil) 356 | { 357 | count ++; 358 | } 359 | } 360 | self.timerCountPrivate = count; 361 | } 362 | 363 | - (void)scheduleTimerWithName:(NSString *)name timeInterval:(NSTimeInterval)delay behavior:(PARTimerBehavior)behavior block:(PARDispatchBlock)block 364 | { 365 | NSTimeInterval time = [self _now]; 366 | [self dispatchAsynchronously:^ 367 | { 368 | [self _scheduleTimerWithName:name referenceTime:time timeInterval:delay behavior:behavior block:block]; 369 | }]; 370 | } 371 | 372 | - (void)cancelTimerWithName:(NSString *)name 373 | { 374 | [self dispatchAsynchronously:^ 375 | { 376 | [self _cancelTimerWithName:name]; 377 | }]; 378 | } 379 | 380 | - (void)cancelAllTimers 381 | { 382 | [self dispatchAsynchronously:^ 383 | { 384 | for (NSString *name in self.timers.allKeys) 385 | [self _cancelTimerWithName:name]; 386 | self.timerCountPrivate = self.timers.count; 387 | }]; 388 | } 389 | 390 | // the whole point of having a property timerCountPrivate` separate from the `timers` dictionary, is to not require a synchronous call into the queue, while still having an atomic accessor 391 | // the returned value may well be outdated by the time it is used (except if used **inside** the queue), but this should be obvious to the client 392 | - (NSUInteger)timerCount 393 | { 394 | return _timerCountPrivate; 395 | } 396 | 397 | @end 398 | 399 | 400 | @interface PARBlockOperation () 401 | @property (nonatomic, strong) PARDispatchQueue *queue; 402 | @property BOOL done; 403 | @end 404 | 405 | @implementation PARBlockOperation 406 | 407 | + (PARBlockOperation *)dispatchedOperationWithQueue:(PARDispatchQueue *)queue block:(PARDispatchBlock)block; 408 | { 409 | PARBlockOperation *operation = [[PARBlockOperation alloc] init]; 410 | 411 | // use a private queue to guarantee FIFO order: the block will be executed before the one used in the `waitUntilFinished` method 412 | operation.queue = [PARDispatchQueue dispatchQueueWithLabel:[queue.label stringByAppendingString:@".block_operation"]]; 413 | [operation.queue dispatchAsynchronously:^{ operation.done = NO; [queue dispatchSynchronously:block]; operation.done = YES; }]; 414 | 415 | return operation; 416 | } 417 | 418 | - (void)waitUntilFinished 419 | { 420 | __block BOOL reallyDone = NO; 421 | [self.queue dispatchSynchronously:^{ reallyDone = self.done; /* noop, just waiting */ }]; 422 | NSAssert(reallyDone, @"BAD"); 423 | } 424 | 425 | 426 | @end 427 | 428 | -------------------------------------------------------------------------------- /Tests/PARSQLiteTests.m: -------------------------------------------------------------------------------- 1 | // PARStore 2 | // Created by Charles Parnot on 7/10/14. 3 | // Copyright (c) 2014 Charles Parnot. All rights reserved. 4 | 5 | #import "PARTestCase.h" 6 | #import 7 | 8 | @interface PARSQLiteTests : PARTestCase 9 | 10 | @end 11 | 12 | @implementation PARSQLiteTests 13 | 14 | - (void)setUp 15 | { 16 | [super setUp]; 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | - (void)tearDown 21 | { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | [super tearDown]; 24 | } 25 | 26 | 27 | #pragma mark - Testing Journal Modes 28 | 29 | - (void)testJournalModeWal 30 | { 31 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 32 | NSString *databaseName = @"test"; 33 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 34 | 35 | // create stack 36 | NSManagedObjectContext *moc = [self managedObjectContext]; 37 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"WAL"]; 38 | 39 | // add content 40 | NSUInteger numberOfObjectsToAdd = 100; 41 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 42 | { 43 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 44 | } 45 | 46 | // assert files 47 | NSString *wal = [self walPathWithName:databaseName directory:directory]; 48 | NSString *shm = [self shmPathWithName:databaseName directory:directory]; 49 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 50 | [self assertFileExists:databasePath]; 51 | [self assertFileExists:wal]; 52 | [self assertFileExists:shm]; 53 | [self assertFileIsEmpty:wal]; 54 | [self assertFileDoesNotExist:journal]; 55 | 56 | // save 57 | NSError *saveError = nil; 58 | BOOL saveSuccess = [moc save:&saveError]; 59 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 60 | 61 | // assert files 62 | [self assertFileExists:databasePath]; 63 | [self assertFileExists:wal]; 64 | [self assertFileExists:shm]; 65 | [self assertFileIsNotEmpty:wal]; 66 | [self assertFileDoesNotExist:journal]; 67 | 68 | // closing the database connection 69 | moc = nil; 70 | 71 | // assert files 72 | // the wal file is empty after checkpointing: http://www.sqlite.org/wal.html#ckpt 73 | [self assertFileExists:databasePath]; 74 | [self assertFileExists:wal]; 75 | [self assertFileExists:shm]; 76 | [self assertFileIsEmpty:wal]; 77 | [self assertFileDoesNotExist:journal]; 78 | } 79 | 80 | - (void)testJournalModeTruncate 81 | { 82 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 83 | NSString *databaseName = @"test"; 84 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 85 | 86 | // create stack 87 | NSManagedObjectContext *moc = [self managedObjectContext]; 88 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"TRUNCATE"]; 89 | 90 | // add content 91 | NSUInteger numberOfObjectsToAdd = 100; 92 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 93 | { 94 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 95 | } 96 | 97 | // assert files 98 | NSString *wal = [self walPathWithName:databaseName directory:directory]; 99 | NSString *shm = [self shmPathWithName:databaseName directory:directory]; 100 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 101 | [self assertFileExists:databasePath]; 102 | [self assertFileDoesNotExist:journal]; 103 | [self assertFileDoesNotExist:wal]; 104 | [self assertFileDoesNotExist:shm]; 105 | 106 | // save 107 | NSError *saveError = nil; 108 | BOOL saveSuccess = [moc save:&saveError]; 109 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 110 | 111 | // assert files 112 | [self assertFileExists:databasePath]; 113 | [self assertFileExists:journal]; 114 | [self assertFileIsEmpty:journal]; 115 | [self assertFileDoesNotExist:wal]; 116 | [self assertFileDoesNotExist:shm]; 117 | 118 | // closing the database connection 119 | moc = nil; 120 | 121 | // assert files 122 | [self assertFileExists:databasePath]; 123 | [self assertFileExists:journal]; 124 | [self assertFileIsEmpty:journal]; 125 | [self assertFileDoesNotExist:wal]; 126 | [self assertFileDoesNotExist:shm]; 127 | } 128 | 129 | - (void)testJournalModeDelete 130 | { 131 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 132 | NSString *databaseName = @"test"; 133 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 134 | 135 | // create stack 136 | NSManagedObjectContext *moc = [self managedObjectContext]; 137 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"TRUNCATE"]; 138 | 139 | // add content 140 | NSUInteger numberOfObjectsToAdd = 100; 141 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 142 | { 143 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 144 | } 145 | 146 | // assert files 147 | NSString *wal = [self walPathWithName:databaseName directory:directory]; 148 | NSString *shm = [self shmPathWithName:databaseName directory:directory]; 149 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 150 | [self assertFileExists:databasePath]; 151 | [self assertFileDoesNotExist:journal]; 152 | [self assertFileDoesNotExist:wal]; 153 | [self assertFileDoesNotExist:shm]; 154 | 155 | // save 156 | NSError *saveError = nil; 157 | BOOL saveSuccess = [moc save:&saveError]; 158 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 159 | 160 | // assert files 161 | [self assertFileExists:databasePath]; 162 | [self assertFileExists:journal]; 163 | [self assertFileIsEmpty:journal]; 164 | [self assertFileDoesNotExist:wal]; 165 | [self assertFileDoesNotExist:shm]; 166 | 167 | // closing the database connection 168 | moc = nil; 169 | 170 | // assert files 171 | [self assertFileExists:databasePath]; 172 | [self assertFileExists:journal]; 173 | [self assertFileIsEmpty:journal]; 174 | [self assertFileDoesNotExist:wal]; 175 | [self assertFileDoesNotExist:shm]; 176 | } 177 | 178 | - (void)testJournalModePersist 179 | { 180 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 181 | NSString *databaseName = @"test"; 182 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 183 | 184 | // create stack 185 | NSManagedObjectContext *moc = [self managedObjectContext]; 186 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"PERSIST"]; 187 | 188 | // add content 189 | NSUInteger numberOfObjectsToAdd = 100; 190 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 191 | { 192 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 193 | } 194 | 195 | // assert files 196 | NSString *wal = [self walPathWithName:databaseName directory:directory]; 197 | NSString *shm = [self shmPathWithName:databaseName directory:directory]; 198 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 199 | [self assertFileExists:databasePath]; 200 | [self assertFileDoesNotExist:journal]; 201 | [self assertFileDoesNotExist:wal]; 202 | [self assertFileDoesNotExist:shm]; 203 | 204 | // save 205 | NSError *saveError = nil; 206 | BOOL saveSuccess = [moc save:&saveError]; 207 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 208 | 209 | // assert files 210 | [self assertFileExists:databasePath]; 211 | [self assertFileExists:journal]; 212 | [self assertFileIsNotEmpty:journal]; 213 | [self assertFileDoesNotExist:wal]; 214 | [self assertFileDoesNotExist:shm]; 215 | 216 | // closing the database connection 217 | moc = nil; 218 | 219 | // assert files 220 | [self assertFileExists:databasePath]; 221 | [self assertFileExists:journal]; 222 | [self assertFileIsNotEmpty:journal]; 223 | [self assertFileDoesNotExist:wal]; 224 | [self assertFileDoesNotExist:shm]; 225 | } 226 | 227 | 228 | #pragma mark - Testing Journal Mode Changes 229 | 230 | - (void)testWalToTruncate 231 | { 232 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 233 | NSString *databaseName = @"test"; 234 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 235 | 236 | // create stack in WAL mode 237 | NSManagedObjectContext *moc = [self managedObjectContext]; 238 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"WAL"]; 239 | 240 | // add content 241 | NSUInteger numberOfObjectsToAdd = 100; 242 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 243 | { 244 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 245 | } 246 | 247 | // save 248 | NSError *saveError = nil; 249 | BOOL saveSuccess = [moc save:&saveError]; 250 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 251 | 252 | // snapshot of the content 253 | NSArray *snapshotWal = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 254 | 255 | // close the database completely 256 | moc = nil; 257 | 258 | // assert files 259 | NSString *wal = [self walPathWithName:databaseName directory:directory]; 260 | NSString *shm = [self shmPathWithName:databaseName directory:directory]; 261 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 262 | [self assertFileExists:databasePath]; 263 | [self assertFileExists:wal]; 264 | [self assertFileExists:shm]; 265 | [self assertFileIsEmpty:wal]; 266 | [self assertFileDoesNotExist:journal]; 267 | 268 | 269 | // reopen stack in TRUNCATE mode 270 | moc = [self managedObjectContext]; 271 | [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"TRUNCATE"]; 272 | 273 | // snapshot of the content 274 | NSArray *snapshotTruncate = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 275 | 276 | // close the database completely 277 | moc = nil; 278 | 279 | // assert files 280 | // for some reason, the shm file is left behind 281 | [self assertFileExists:databasePath]; 282 | [self assertFileExists:journal]; 283 | [self assertFileIsEmpty:journal]; 284 | [self assertFileDoesNotExist:wal]; 285 | [self assertFileExists:shm]; 286 | 287 | // compare snapshots 288 | XCTAssert(snapshotWal.count == snapshotTruncate.count, @"snapshots have different number of elements: %@ and %@", @(snapshotWal.count), @(snapshotTruncate.count)); 289 | NSEnumerator *e1 = snapshotWal.objectEnumerator; 290 | NSEnumerator *e2 = snapshotTruncate.objectEnumerator; 291 | NSDictionary *rep1 = nil; 292 | NSDictionary *rep2 = nil; 293 | while ((rep1 = e1.nextObject) && (rep2 = e2.nextObject)) 294 | { 295 | NSString *foo1 = rep1[@"foo"]; 296 | NSString *foo2 = rep2[@"foo"]; 297 | NSString *bar1 = rep1[@"bar"]; 298 | NSString *bar2 = rep2[@"bar"]; 299 | XCTAssertEqualObjects(foo1, foo2, @"foo property should be the same"); 300 | XCTAssertEqualObjects(bar1, bar2, @"bar property should be the same"); 301 | } 302 | 303 | 304 | // remove the shm file from disk 305 | NSError *removeError; 306 | BOOL removeSuccess = [[NSFileManager defaultManager] removeItemAtPath:shm error:&removeError]; 307 | XCTAssertTrue(removeSuccess, @"error removing file at path '%@': %@", shm, removeError); 308 | 309 | // open stack in TRUNCATE mode, again 310 | moc = [self managedObjectContext]; 311 | [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"TRUNCATE"]; 312 | 313 | // snapshot of the content 314 | snapshotTruncate = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 315 | 316 | // close the database completely 317 | moc = nil; 318 | 319 | // assert files 320 | [self assertFileExists:databasePath]; 321 | [self assertFileExists:journal]; 322 | [self assertFileIsEmpty:journal]; 323 | [self assertFileDoesNotExist:wal]; 324 | [self assertFileDoesNotExist:shm]; 325 | 326 | // compare snapshots 327 | XCTAssert(snapshotWal.count == snapshotTruncate.count, @"snapshots have different number of elements: %@ and %@", @(snapshotWal.count), @(snapshotTruncate.count)); 328 | e1 = snapshotWal.objectEnumerator; 329 | e2 = snapshotTruncate.objectEnumerator; 330 | rep1 = nil; 331 | rep2 = nil; 332 | while ((rep1 = e1.nextObject) && (rep2 = e2.nextObject)) 333 | { 334 | NSString *foo1 = rep1[@"foo"]; 335 | NSString *foo2 = rep2[@"foo"]; 336 | NSString *bar1 = rep1[@"bar"]; 337 | NSString *bar2 = rep2[@"bar"]; 338 | XCTAssertEqualObjects(foo1, foo2, @"foo property should be the same"); 339 | XCTAssertEqualObjects(bar1, bar2, @"bar property should be the same"); 340 | } 341 | } 342 | 343 | 344 | #pragma mark - SQLite Tests 345 | 346 | - (void)testSqliteHotJournalCapture 347 | { 348 | // paths 349 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 350 | NSString *databaseName = @"test"; 351 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 352 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 353 | NSString *journalCopy = [self journalCopyPathWithName:databaseName directory:directory]; 354 | 355 | // create database using Core Data 356 | NSManagedObjectContext *moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:YES]; 357 | moc = nil; 358 | 359 | // assert files 360 | [self assertFileExists:databasePath]; 361 | [self assertFileExists:journal]; 362 | [self assertFileDoesNotExist:journalCopy]; 363 | [self assertFileIsEmpty:journal]; 364 | 365 | // capture sqlite database in "hot" state 366 | [self captureHotJournalWithDatabaseName:databaseName directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 367 | 368 | // assert files 369 | [self assertFileExists:databasePath]; 370 | [self assertFileExists:journal]; 371 | [self assertFileExists:journalCopy]; 372 | [self assertFileIsEmpty:journal]; 373 | [self assertFileIsNotEmpty:journalCopy]; 374 | } 375 | 376 | - (void)testSqliteToCoreData 377 | { 378 | // database path 379 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 380 | NSString *databaseName = @"test"; 381 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 382 | 383 | // create database using Core Data 384 | NSManagedObjectContext *moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:YES]; 385 | NSArray *snapshot1 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 386 | moc = nil; 387 | 388 | // capture sqlite database in "hot" state 389 | [self captureHotJournalWithDatabaseName:databaseName directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 390 | 391 | // there should be a new row in Core Data 392 | moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:NO]; 393 | NSArray *snapshot2 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 394 | moc = nil; 395 | 396 | // compare snapshots 397 | snapshot1 = [snapshot1 arrayByAddingObject:@{@"foo" : @"ZZZ", @"bar" : @"ZZZ"}]; 398 | XCTAssert(snapshot1.count == snapshot2.count, @"snapshots have different number of elements: %@ and %@", @(snapshot1.count), @(snapshot2.count)); 399 | NSEnumerator *e1 = snapshot1.objectEnumerator; 400 | NSEnumerator *e2 = snapshot2.objectEnumerator; 401 | NSDictionary *rep1 = nil; 402 | NSDictionary *rep2 = nil; 403 | while ((rep1 = e1.nextObject) && (rep2 = e2.nextObject)) 404 | { 405 | NSString *foo1 = rep1[@"foo"]; 406 | NSString *foo2 = rep2[@"foo"]; 407 | NSString *bar1 = rep1[@"bar"]; 408 | NSString *bar2 = rep2[@"bar"]; 409 | XCTAssertEqualObjects(foo1, foo2, @"foo property should be the same"); 410 | XCTAssertEqualObjects(bar1, bar2, @"bar property should be the same"); 411 | } 412 | } 413 | 414 | - (void)testSqliteHotJournal 415 | { 416 | // database path 417 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 418 | NSString *databaseName = @"test"; 419 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 420 | NSString *databaseCopy = [self databaseCopyPathWithName:databaseName directory:directory]; 421 | NSString *journalCopy = [self journalCopyPathWithName:databaseName directory:directory]; 422 | 423 | // create database using Core Data 424 | NSManagedObjectContext *moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:YES]; 425 | NSArray *snapshot1 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 426 | moc = nil; 427 | 428 | // capture sqlite database in "hot" state 429 | [self captureHotJournalWithDatabaseName:databaseName directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 430 | 431 | // open the "hot" database with Core Data 432 | moc = [self managedObjectContextForDatabaseAtPath:databaseCopy populate:NO]; 433 | NSArray *snapshot2 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 434 | moc = nil; 435 | 436 | // Merely opening the database, without changing the content, like we just did, apparently does **not** reset the journal database. But the SQLite docs indicate that if the journal is "hot", it should be reset (see: http://www.sqlite.org/lockingv3.html#hot_journals). It is thus possible that the above does not generate a journal considered "hot" 437 | [self assertFileIsNotEmpty:journalCopy]; 438 | 439 | // the "hot" database should have the same content as the initial database, since the new content was not committed 440 | XCTAssert(snapshot1.count == snapshot2.count, @"snapshots have different number of elements: %@ and %@", @(snapshot1.count), @(snapshot2.count)); 441 | NSEnumerator *e1 = snapshot1.objectEnumerator; 442 | NSEnumerator *e2 = snapshot2.objectEnumerator; 443 | NSDictionary *rep1 = nil; 444 | NSDictionary *rep2 = nil; 445 | while ((rep1 = e1.nextObject) && (rep2 = e2.nextObject)) 446 | { 447 | NSString *foo1 = rep1[@"foo"]; 448 | NSString *foo2 = rep2[@"foo"]; 449 | NSString *bar1 = rep1[@"bar"]; 450 | NSString *bar2 = rep2[@"bar"]; 451 | XCTAssertEqualObjects(foo1, foo2, @"foo property should be the same"); 452 | XCTAssertEqualObjects(bar1, bar2, @"bar property should be the same"); 453 | } 454 | 455 | // opening the "hot" database **and** modifying content will reset the journal file 456 | [self captureHotJournalWithDatabaseName:[self databaseCopyNameWithName:databaseName] directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 457 | [self assertFileExists:databaseCopy]; 458 | [self assertFileExists:journalCopy]; 459 | [self assertFileIsEmpty:journalCopy]; 460 | } 461 | 462 | - (void)testSqliteSwapHotJournal 463 | { 464 | // database path 465 | NSString *directory = [[self urlWithUniqueTmpDirectory] path]; 466 | NSString *databaseName = @"test"; 467 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 468 | NSString *databaseCopy = [self databaseCopyPathWithName:databaseName directory:directory]; 469 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 470 | NSString *journalCopy = [self journalCopyPathWithName:databaseName directory:directory]; 471 | 472 | // create database using Core Data 473 | NSManagedObjectContext *moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:YES]; 474 | NSArray *snapshot1 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 475 | moc = nil; 476 | 477 | // capture sqlite database in "hot" state 478 | [self captureHotJournalWithDatabaseName:databaseName directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 479 | 480 | // copy the hot journal to apply it to the non-hot database --> "heated" database 481 | [[NSFileManager defaultManager] removeItemAtPath:journal error:NULL]; 482 | [[NSFileManager defaultManager] copyItemAtPath:journalCopy toPath:journal error:NULL]; 483 | [self assertFileExists:journal]; 484 | [self assertFileIsNotEmpty:journal]; 485 | 486 | // open the "heated" database with Core Data 487 | moc = [self managedObjectContextForDatabaseAtPath:databasePath populate:NO]; 488 | NSArray *snapshot2 = [self allManagedObjectRepresentationsForManagedObjectContext:moc]; 489 | moc = nil; 490 | 491 | // Merely opening the database, without changing the content, like we just did, apparently does **not** reset the journal database. But the SQLite docs indicate that if the journal is "hot", it should be reset (see: http://www.sqlite.org/lockingv3.html#hot_journals). It is thus possible that the above does not generate a journal considered "hot" 492 | [self assertFileIsNotEmpty:journal]; 493 | 494 | // adding the "hot" journal should not have change the content of the database, since it was not committed 495 | snapshot1 = [snapshot1 arrayByAddingObject:@{@"foo" : @"ZZZ", @"bar" : @"ZZZ"}]; 496 | XCTAssert(snapshot1.count == snapshot2.count, @"snapshots have different number of elements: %@ and %@", @(snapshot1.count), @(snapshot2.count)); 497 | NSEnumerator *e1 = snapshot1.objectEnumerator; 498 | NSEnumerator *e2 = snapshot2.objectEnumerator; 499 | NSDictionary *rep1 = nil; 500 | NSDictionary *rep2 = nil; 501 | while ((rep1 = e1.nextObject) && (rep2 = e2.nextObject)) 502 | { 503 | NSString *foo1 = rep1[@"foo"]; 504 | NSString *foo2 = rep2[@"foo"]; 505 | NSString *bar1 = rep1[@"bar"]; 506 | NSString *bar2 = rep2[@"bar"]; 507 | XCTAssertEqualObjects(foo1, foo2, @"foo property should be the same"); 508 | XCTAssertEqualObjects(bar1, bar2, @"bar property should be the same"); 509 | } 510 | 511 | // opening the "heated" database **and** modifying content will reset the journal file 512 | [self captureHotJournalWithDatabaseName:[self databaseCopyNameWithName:databaseName] directory:directory addFooValue:@"ZZZ" barValue:@"ZZZ"]; 513 | [self assertFileExists:databaseCopy]; 514 | [self assertFileExists:journalCopy]; 515 | [self assertFileIsEmpty:journalCopy]; 516 | } 517 | 518 | - (NSManagedObjectContext *)managedObjectContextForDatabaseAtPath:(NSString *)databasePath populate:(BOOL)populate 519 | { 520 | // create Core Data stack 521 | NSManagedObjectContext *moc = [self managedObjectContext]; 522 | NSPersistentStore *store1 = [self addPersistentStoreWithCoordinator:moc.persistentStoreCoordinator storePath:databasePath readOnly:NO journalMode:@"TRUNCATE"]; 523 | 524 | // add content 525 | NSUInteger numberOfObjectsToAdd = populate ? 100 : 0; 526 | for (NSUInteger i = 0; i < numberOfObjectsToAdd; i++) 527 | { 528 | [self addTestManagedObjectToPersistentStore:store1 managedObjectContext:moc]; 529 | } 530 | 531 | // save and tear down Core Data 532 | NSError *saveError = nil; 533 | BOOL saveSuccess = [moc save:&saveError]; 534 | XCTAssertTrue(saveSuccess, @"error saving: %@", saveError); 535 | 536 | return moc; 537 | } 538 | 539 | // - open database using sqlite 540 | // - bring it to a state where a journal file is created (in the middle of a transaction) 541 | // - capture the db and journal file by copying the file 542 | // - tear things down 543 | - (void)captureHotJournalWithDatabaseName:(NSString *)databaseName directory:(NSString *)directory addFooValue:(NSString *)fooValue barValue:(NSString *)barValue 544 | { 545 | NSString *databasePath = [self databasePathWithName:databaseName directory:directory]; 546 | NSString *databaseCopy = [self databaseCopyPathWithName:databaseName directory:directory]; 547 | NSString *journal = [self journalPathWithName:databaseName directory:directory]; 548 | NSString *journalCopy = [self journalCopyPathWithName:databaseName directory:directory]; 549 | 550 | // open sqlite 551 | sqlite3 *sqlitedb = [self openSqliteDatabaseAtPath:databasePath]; 552 | [self executeStatement:@"PRAGMA journal_mode = TRUNCATE" sqliteDatabase:sqlitedb]; 553 | 554 | // set up a transaction 555 | [self executeStatement:@"BEGIN IMMEDIATE TRANSACTION" sqliteDatabase:sqlitedb]; 556 | 557 | // add a row 558 | NSString *entityName = [self managedObjectModel].entitiesByName.allKeys.firstObject; 559 | NSString *tableName = [@"Z" stringByAppendingString:entityName]; 560 | NSString *insertStatement = [NSString stringWithFormat:@"INSERT INTO %@ (Z_ENT, Z_OPT, ZFOO, ZBAR) VALUES (1, 1, '%@', '%@')", tableName, fooValue ?: @"value1", barValue ?: @"value2"]; 561 | [self executeStatement:insertStatement sqliteDatabase:sqlitedb]; 562 | 563 | // make a copy of the journal file, which is not empty at this point 564 | [self assertFileIsNotEmpty:journal]; 565 | [[NSFileManager defaultManager] copyItemAtPath:journal toPath:journalCopy error:NULL]; 566 | [[NSFileManager defaultManager] copyItemAtPath:databasePath toPath:databaseCopy error:NULL]; 567 | [self assertFileExists:journalCopy]; 568 | [self assertFileExists:databaseCopy]; 569 | 570 | // end transaction 571 | [self executeStatement:@"COMMIT TRANSACTION" sqliteDatabase:sqlitedb]; 572 | 573 | // close sqlite 574 | [self closeSqliteDatabase:sqlitedb]; 575 | } 576 | 577 | 578 | #pragma mark - SQLite Utilities 579 | 580 | - (sqlite3 *)openSqliteDatabaseAtPath:(NSString *)databasePath 581 | { 582 | sqlite3 *sqlitedb; 583 | int err = sqlite3_open([databasePath fileSystemRepresentation], &sqlitedb); 584 | XCTAssertEqual(err, SQLITE_OK, @"error opening sqlite database: %@ - %@", @(err), @(sqlite3_errmsg(sqlitedb))); 585 | if (err != SQLITE_OK) 586 | { 587 | // NOTE: according to the docs for sqlite3_open, a database handle is returned even on 588 | // error (it will only be NULL if SQLite is unable to allocate memory), and the SQLite 589 | // close command should be used to release any resources associated with the db handle 590 | if (sqlitedb != NULL) 591 | [self closeSqliteDatabase:sqlitedb]; 592 | return NULL; 593 | } 594 | 595 | XCTAssert(sqlitedb != NULL, @"sqlite handle should not be nil after opening the database"); 596 | return sqlitedb; 597 | } 598 | 599 | - (BOOL)closeSqliteDatabase:(sqlite3 *)sqlitedb 600 | { 601 | __block int err = sqlite3_close(sqlitedb); 602 | 603 | XCTAssertNotEqual(err, SQLITE_BUSY, @"Database busy when closing"); 604 | XCTAssertEqual(err, SQLITE_OK, @"error closing sqlite database: %@ - %@", @(err), @(sqlite3_errmsg(sqlitedb))); 605 | 606 | return err != SQLITE_OK; 607 | } 608 | 609 | - (BOOL)executeStatement:(NSString *)statement sqliteDatabase:(sqlite3 *)sqlitedb 610 | { 611 | int err = sqlite3_exec(sqlitedb, [statement UTF8String], NULL, NULL, NULL); 612 | XCTAssertEqual(err, SQLITE_OK, @"error executing statement: %@ - %@", @(err), @(sqlite3_errmsg(sqlitedb))); 613 | return err != SQLITE_OK; 614 | } 615 | 616 | 617 | #pragma mark - Path Utilities 618 | 619 | - (NSString *)databaseCopyNameWithName:(NSString *)name 620 | { 621 | return [name stringByAppendingString:@"-copy"]; 622 | } 623 | 624 | - (NSString *)databasePathWithName:(NSString *)name directory:(NSString *)directory 625 | { 626 | return [directory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.db", name]]; 627 | } 628 | 629 | - (NSString *)databaseCopyPathWithName:(NSString *)name directory:(NSString *)directory 630 | { 631 | return [self databasePathWithName:[self databaseCopyNameWithName:name] directory:directory]; 632 | } 633 | 634 | - (NSString *)walPathWithName:(NSString *)name directory:(NSString *)directory 635 | { 636 | return [[self databasePathWithName:name directory:directory] stringByAppendingString:@"-wal"]; 637 | } 638 | 639 | - (NSString *)shmPathWithName:(NSString *)name directory:(NSString *)directory 640 | { 641 | return [[self databasePathWithName:name directory:directory] stringByAppendingString:@"-shm"]; 642 | } 643 | 644 | - (NSString *)journalPathWithName:(NSString *)name directory:(NSString *)directory 645 | { 646 | return [[self databasePathWithName:name directory:directory] stringByAppendingString:@"-journal"]; 647 | } 648 | 649 | - (NSString *)journalCopyPathWithName:(NSString *)name directory:(NSString *)directory 650 | { 651 | return [self journalPathWithName:[self databaseCopyNameWithName:name] directory:directory]; 652 | } 653 | 654 | 655 | #pragma mark - Assertions 656 | 657 | - (void)assertFileExists:(NSString *)path 658 | { 659 | BOOL isDir = NO; 660 | BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; 661 | XCTAssert(fileExists && !isDir, @"file should exist but %@ at path %@", isDir ? @"is a directory" : @"does not exist", path); 662 | } 663 | 664 | - (void)assertFileDoesNotExist:(NSString *)path 665 | { 666 | BOOL isDir = NO; 667 | BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; 668 | XCTAssert(!fileExists && !isDir, @"file should not exist but %@ at path %@", isDir ? @"is a directory" : @"exists", path); 669 | } 670 | 671 | - (void)assertFileIsEmpty:(NSString *)path 672 | { 673 | NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:NULL]; 674 | NSNumber *fileSize = attributes[NSFileSize]; 675 | XCTAssert(attributes != nil && [fileSize isEqualToNumber:@(0.0)] == YES, @"file size is expected to be zero but is %@ at path: %@", fileSize, path); 676 | } 677 | 678 | - (void)assertFileIsNotEmpty:(NSString *)path 679 | { 680 | NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:NULL]; 681 | NSNumber *fileSize = attributes[NSFileSize]; 682 | XCTAssert(attributes != nil && [fileSize isEqualToNumber:@(0.0)] == NO, @"file size is expected to be non-zero but is %@ at path: %@", fileSize, path); 683 | 684 | } 685 | 686 | 687 | #pragma mark - Setting Up Test Core Data Stacks 688 | 689 | - (NSManagedObjectContext *)managedObjectContext 690 | { 691 | NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; 692 | XCTAssertNotNil(psc, @"error creating persistent store coordinator"); 693 | 694 | NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init]; 695 | moc.persistentStoreCoordinator = psc; 696 | 697 | XCTAssertNotNil(psc, @"error creating managed object context"); 698 | return moc; 699 | } 700 | 701 | - (NSPersistentStore *)addPersistentStoreWithCoordinator:(NSPersistentStoreCoordinator *)psc storePath:(NSString *)storePath readOnly:(BOOL)readOnly journalMode:(NSString *)journalMode 702 | { 703 | NSError *localError = nil; 704 | NSDictionary *pragmas = @{ 705 | @"journal_mode": journalMode ?: @"WAL" 706 | }; 707 | NSDictionary *storeOptions = @{ 708 | NSReadOnlyPersistentStoreOption: @(readOnly), 709 | NSSQLitePragmasOption: pragmas, 710 | }; 711 | NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:storePath] options:storeOptions error:&localError]; 712 | 713 | XCTAssertNotNil(store, @"error creating the store at path '%@': %@", storePath, localError); 714 | return store; 715 | } 716 | 717 | - (NSManagedObjectModel *)managedObjectModel 718 | { 719 | static dispatch_once_t pred = 0; 720 | static NSManagedObjectModel *mom = nil; 721 | dispatch_once(&pred, 722 | ^{ 723 | NSAttributeDescription *fooAttribute = [[NSAttributeDescription alloc] init]; 724 | fooAttribute.name = @"foo"; 725 | fooAttribute.indexed = YES; 726 | fooAttribute.attributeType = NSStringAttributeType; 727 | 728 | NSAttributeDescription *barAttribute = [[NSAttributeDescription alloc] init]; 729 | barAttribute.name = @"bar"; 730 | barAttribute.attributeType = NSStringAttributeType; 731 | 732 | NSEntityDescription *entity = [[NSEntityDescription alloc] init]; 733 | entity.name = @"TestEntity"; 734 | entity.properties = @[barAttribute, fooAttribute]; 735 | 736 | mom = [[NSManagedObjectModel alloc] init]; 737 | mom.entities = @[entity]; 738 | }); 739 | 740 | XCTAssertNotNil(mom, @"managed object model was not properly created"); 741 | return mom; 742 | } 743 | 744 | - (NSManagedObject *)addTestManagedObjectToPersistentStore:(NSPersistentStore *)store managedObjectContext:(NSManagedObjectContext *)moc 745 | { 746 | XCTAssertTrue(store.persistentStoreCoordinator == moc.persistentStoreCoordinator, @"inconsistent store coordinators"); 747 | 748 | NSManagedObjectModel *model = store.persistentStoreCoordinator.managedObjectModel; 749 | NSEntityDescription *entity = model.entities.firstObject; 750 | NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:entity.name inManagedObjectContext:moc]; 751 | [moc assignObject:managedObject toPersistentStore:store]; 752 | [managedObject setValue:[[NSUUID UUID] UUIDString] forKey:@"foo"]; 753 | [managedObject setValue:[[NSUUID UUID] UUIDString] forKey:@"bar"]; 754 | 755 | XCTAssertNotNil(managedObject, @"managed object could not be created"); 756 | return managedObject; 757 | } 758 | 759 | - (NSArray *)allManagedObjectRepresentationsForManagedObjectContext:(NSManagedObjectContext *)moc 760 | { 761 | NSEntityDescription *entity = moc.persistentStoreCoordinator.managedObjectModel.entities.firstObject; 762 | 763 | NSError *localError = nil; 764 | NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; 765 | fetchRequest.entity = entity; 766 | fetchRequest.predicate = nil; 767 | fetchRequest.resultType = NSDictionaryResultType; 768 | NSArray *results = [moc executeFetchRequest:fetchRequest error:&localError]; 769 | results = [results sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"foo" ascending:YES]]]; 770 | 771 | XCTAssertNotNil(results, @"error fetching "); 772 | return results; 773 | } 774 | 775 | @end 776 | -------------------------------------------------------------------------------- /PARStore.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 560C93E116F1272C00C0E890 /* NSError+Factory.m in Sources */ = {isa = PBXBuildFile; fileRef = 560C93E016F1272C00C0E890 /* NSError+Factory.m */; }; 11 | 560C93E216F1272C00C0E890 /* NSError+Factory.m in Sources */ = {isa = PBXBuildFile; fileRef = 560C93E016F1272C00C0E890 /* NSError+Factory.m */; }; 12 | 561148E7196E952800F488F2 /* PARSQLiteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 561148E6196E952800F488F2 /* PARSQLiteTests.m */; }; 13 | 569D021E16E24FDA002675BA /* PARDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 569D021D16E24FDA002675BA /* PARDispatchQueue.m */; }; 14 | 569D022116E2509F002675BA /* PARAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 569D022016E2509F002675BA /* PARAppDelegate.m */; }; 15 | 569D022316E250A7002675BA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 569D022216E250A7002675BA /* MainMenu.xib */; }; 16 | 56C7EDC516E260EA00FFBBF2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C7EDC416E260EA00FFBBF2 /* UIKit.framework */; }; 17 | 56C7EDC716E260EA00FFBBF2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C7EDC616E260EA00FFBBF2 /* Foundation.framework */; }; 18 | 56C7EDC916E260EA00FFBBF2 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C7EDC816E260EA00FFBBF2 /* CoreGraphics.framework */; }; 19 | 56C7EDCF16E260EB00FFBBF2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56C7EDCD16E260EB00FFBBF2 /* InfoPlist.strings */; }; 20 | 56C7EDD116E260EB00FFBBF2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDD016E260EB00FFBBF2 /* main.m */; }; 21 | 56C7EDD516E260EB00FFBBF2 /* PARAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDD416E260EB00FFBBF2 /* PARAppDelegate.m */; }; 22 | 56C7EDD716E260EB00FFBBF2 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 56C7EDD616E260EB00FFBBF2 /* Default.png */; }; 23 | 56C7EDD916E260EB00FFBBF2 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 56C7EDD816E260EB00FFBBF2 /* Default@2x.png */; }; 24 | 56C7EDDB16E260EB00FFBBF2 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 56C7EDDA16E260EB00FFBBF2 /* Default-568h@2x.png */; }; 25 | 56C7EDE316E260EB00FFBBF2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C7EDC416E260EA00FFBBF2 /* UIKit.framework */; }; 26 | 56C7EDE416E260EB00FFBBF2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C7EDC616E260EA00FFBBF2 /* Foundation.framework */; }; 27 | 56C7EDF716E2687F00FFBBF2 /* PARStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 56EAE19416E24C7500A7F31F /* PARStoreTests.m */; }; 28 | 56C7EDF816E2688700FFBBF2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56EAE19016E24C7500A7F31F /* InfoPlist.strings */; }; 29 | 56C7EDFC16E26D0700FFBBF2 /* PARTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDFB16E26D0700FFBBF2 /* PARTestCase.m */; }; 30 | 56C7EDFD16E26D0700FFBBF2 /* PARTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDFB16E26D0700FFBBF2 /* PARTestCase.m */; }; 31 | 56C7EE0A16E2811E00FFBBF2 /* PARNotificationSemaphore.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EE0916E2811E00FFBBF2 /* PARNotificationSemaphore.m */; }; 32 | 56C7EE0B16E2811E00FFBBF2 /* PARNotificationSemaphore.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EE0916E2811E00FFBBF2 /* PARNotificationSemaphore.m */; }; 33 | 56C7EE0C16E2842A00FFBBF2 /* PARStoreExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDFF16E270C600FFBBF2 /* PARStoreExample.m */; }; 34 | 56C7EE0D16E2842B00FFBBF2 /* PARStoreExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C7EDFF16E270C600FFBBF2 /* PARStoreExample.m */; }; 35 | 56C7EE0E16E2843700FFBBF2 /* PARStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 56EAE1BD16E24EAA00A7F31F /* PARStore.m */; }; 36 | 56C7EE0F16E2848C00FFBBF2 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56EAE16E16E24C7500A7F31F /* CoreData.framework */; }; 37 | 56C7EE1216E284A600FFBBF2 /* PARDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 569D021D16E24FDA002675BA /* PARDispatchQueue.m */; }; 38 | 56D5A90617F4493800AEA626 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56EAE16E16E24C7500A7F31F /* CoreData.framework */; }; 39 | 56E196E61B448CA300A51AB0 /* PARDispatchQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 56E196E41B448BE600A51AB0 /* PARDispatchQueueTests.m */; }; 40 | 56EAE16B16E24C7500A7F31F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56EAE16A16E24C7500A7F31F /* Cocoa.framework */; }; 41 | 56EAE18A16E24C7500A7F31F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56EAE16A16E24C7500A7F31F /* Cocoa.framework */; }; 42 | 56EAE19216E24C7500A7F31F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56EAE19016E24C7500A7F31F /* InfoPlist.strings */; }; 43 | 56EAE19516E24C7500A7F31F /* PARStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 56EAE19416E24C7500A7F31F /* PARStoreTests.m */; }; 44 | 56EAE1B916E24E7300A7F31F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 56EAE1B616E24E7300A7F31F /* Credits.rtf */; }; 45 | 56EAE1BA16E24E7300A7F31F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56EAE1B716E24E7300A7F31F /* InfoPlist.strings */; }; 46 | 56EAE1BB16E24E7300A7F31F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 56EAE1B816E24E7300A7F31F /* main.m */; }; 47 | 56EAE1BE16E24EAA00A7F31F /* PARStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 56EAE1BD16E24EAA00A7F31F /* PARStore.m */; }; 48 | 56FA3D771970359C00BF81D3 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 56FA3D761970359C00BF81D3 /* libsqlite3.dylib */; }; 49 | /* End PBXBuildFile section */ 50 | 51 | /* Begin PBXContainerItemProxy section */ 52 | 56C7EDE516E260EB00FFBBF2 /* PBXContainerItemProxy */ = { 53 | isa = PBXContainerItemProxy; 54 | containerPortal = 56EAE15D16E24C7500A7F31F /* Project object */; 55 | proxyType = 1; 56 | remoteGlobalIDString = 56C7EDC116E260EA00FFBBF2; 57 | remoteInfo = "iOS-PARStore"; 58 | }; 59 | 56EAE18B16E24C7500A7F31F /* PBXContainerItemProxy */ = { 60 | isa = PBXContainerItemProxy; 61 | containerPortal = 56EAE15D16E24C7500A7F31F /* Project object */; 62 | proxyType = 1; 63 | remoteGlobalIDString = 56EAE16516E24C7500A7F31F; 64 | remoteInfo = PARStore; 65 | }; 66 | /* End PBXContainerItemProxy section */ 67 | 68 | /* Begin PBXFileReference section */ 69 | 560C93DF16F1272C00C0E890 /* NSError+Factory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+Factory.h"; sourceTree = ""; }; 70 | 560C93E016F1272C00C0E890 /* NSError+Factory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+Factory.m"; sourceTree = ""; }; 71 | 561148E6196E952800F488F2 /* PARSQLiteTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PARSQLiteTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 72 | 569D021C16E24FDA002675BA /* PARDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PARDispatchQueue.h; sourceTree = ""; }; 73 | 569D021D16E24FDA002675BA /* PARDispatchQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PARDispatchQueue.m; sourceTree = ""; }; 74 | 569D021F16E2509F002675BA /* PARAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PARAppDelegate.h; sourceTree = ""; }; 75 | 569D022016E2509F002675BA /* PARAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PARAppDelegate.m; sourceTree = ""; }; 76 | 569D022216E250A7002675BA /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 77 | 569D022916E25340002675BA /* LICENSE-BSD.txt */ = {isa = PBXFileReference; lastKnownFileType = text; lineEnding = 0; path = "LICENSE-BSD.txt"; sourceTree = ""; xcLanguageSpecificationIdentifier = ""; }; 78 | 569D022A16E25340002675BA /* README.markdown */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.markdown; sourceTree = ""; }; 79 | 56C7EDC216E260EA00FFBBF2 /* iOS-PARStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS-PARStore.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | 56C7EDC416E260EA00FFBBF2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 81 | 56C7EDC616E260EA00FFBBF2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 82 | 56C7EDC816E260EA00FFBBF2 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; 83 | 56C7EDCC16E260EB00FFBBF2 /* iOS-PARStore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS-PARStore-Info.plist"; sourceTree = ""; }; 84 | 56C7EDCE16E260EB00FFBBF2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 85 | 56C7EDD016E260EB00FFBBF2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 86 | 56C7EDD216E260EB00FFBBF2 /* iOS-PARStore-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS-PARStore-Prefix.pch"; sourceTree = ""; }; 87 | 56C7EDD316E260EB00FFBBF2 /* PARAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PARAppDelegate.h; sourceTree = ""; }; 88 | 56C7EDD416E260EB00FFBBF2 /* PARAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PARAppDelegate.m; sourceTree = ""; }; 89 | 56C7EDD616E260EB00FFBBF2 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 90 | 56C7EDD816E260EB00FFBBF2 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 91 | 56C7EDDA16E260EB00FFBBF2 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 92 | 56C7EDE116E260EB00FFBBF2 /* iOS-PARStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "iOS-PARStoreTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 93 | 56C7EDFA16E26D0700FFBBF2 /* PARTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PARTestCase.h; sourceTree = ""; }; 94 | 56C7EDFB16E26D0700FFBBF2 /* PARTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PARTestCase.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 95 | 56C7EDFE16E270C600FFBBF2 /* PARStoreExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PARStoreExample.h; sourceTree = ""; }; 96 | 56C7EDFF16E270C600FFBBF2 /* PARStoreExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PARStoreExample.m; sourceTree = ""; }; 97 | 56C7EE0816E2811E00FFBBF2 /* PARNotificationSemaphore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PARNotificationSemaphore.h; sourceTree = ""; }; 98 | 56C7EE0916E2811E00FFBBF2 /* PARNotificationSemaphore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PARNotificationSemaphore.m; sourceTree = ""; }; 99 | 56E196E41B448BE600A51AB0 /* PARDispatchQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PARDispatchQueueTests.m; sourceTree = ""; }; 100 | 56EAE16616E24C7500A7F31F /* Mac-PARStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mac-PARStore.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | 56EAE16A16E24C7500A7F31F /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 102 | 56EAE16D16E24C7500A7F31F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 103 | 56EAE16E16E24C7500A7F31F /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 104 | 56EAE16F16E24C7500A7F31F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 105 | 56EAE18716E24C7500A7F31F /* Mac-PARStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-PARStoreTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 106 | 56EAE18F16E24C7500A7F31F /* PARStoreTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "PARStoreTests-Info.plist"; sourceTree = ""; }; 107 | 56EAE19116E24C7500A7F31F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 108 | 56EAE19416E24C7500A7F31F /* PARStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PARStoreTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 109 | 56EAE1B316E24E5500A7F31F /* PARStore-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "PARStore-Info.plist"; path = "Mac/PARStore-Info.plist"; sourceTree = SOURCE_ROOT; }; 110 | 56EAE1B416E24E5500A7F31F /* PARStore-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "PARStore-Prefix.pch"; path = "Mac/PARStore-Prefix.pch"; sourceTree = SOURCE_ROOT; }; 111 | 56EAE1B616E24E7300A7F31F /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; name = Credits.rtf; path = Mac/Credits.rtf; sourceTree = SOURCE_ROOT; }; 112 | 56EAE1B716E24E7300A7F31F /* InfoPlist.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = InfoPlist.strings; path = Mac/InfoPlist.strings; sourceTree = SOURCE_ROOT; }; 113 | 56EAE1B816E24E7300A7F31F /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mac/main.m; sourceTree = SOURCE_ROOT; }; 114 | 56EAE1BC16E24EAA00A7F31F /* PARStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PARStore.h; path = Core/PARStore.h; sourceTree = SOURCE_ROOT; }; 115 | 56EAE1BD16E24EAA00A7F31F /* PARStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; name = PARStore.m; path = Core/PARStore.m; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 116 | 56FA3D761970359C00BF81D3 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; 117 | /* End PBXFileReference section */ 118 | 119 | /* Begin PBXFrameworksBuildPhase section */ 120 | 56C7EDBF16E260EA00FFBBF2 /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 56C7EDC516E260EA00FFBBF2 /* UIKit.framework in Frameworks */, 125 | 56C7EDC716E260EA00FFBBF2 /* Foundation.framework in Frameworks */, 126 | 56D5A90617F4493800AEA626 /* CoreData.framework in Frameworks */, 127 | 56C7EDC916E260EA00FFBBF2 /* CoreGraphics.framework in Frameworks */, 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | 56C7EDDD16E260EB00FFBBF2 /* Frameworks */ = { 132 | isa = PBXFrameworksBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 56C7EDE316E260EB00FFBBF2 /* UIKit.framework in Frameworks */, 136 | 56C7EDE416E260EB00FFBBF2 /* Foundation.framework in Frameworks */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | 56EAE16316E24C7500A7F31F /* Frameworks */ = { 141 | isa = PBXFrameworksBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 56C7EE0F16E2848C00FFBBF2 /* CoreData.framework in Frameworks */, 145 | 56EAE16B16E24C7500A7F31F /* Cocoa.framework in Frameworks */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | 56EAE18316E24C7500A7F31F /* Frameworks */ = { 150 | isa = PBXFrameworksBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 56FA3D771970359C00BF81D3 /* libsqlite3.dylib in Frameworks */, 154 | 56EAE18A16E24C7500A7F31F /* Cocoa.framework in Frameworks */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXFrameworksBuildPhase section */ 159 | 160 | /* Begin PBXGroup section */ 161 | 56C7EDCA16E260EB00FFBBF2 /* iOS */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 56C7EDD316E260EB00FFBBF2 /* PARAppDelegate.h */, 165 | 56C7EDD416E260EB00FFBBF2 /* PARAppDelegate.m */, 166 | 56C7EDCB16E260EB00FFBBF2 /* Supporting Files */, 167 | ); 168 | path = iOS; 169 | sourceTree = ""; 170 | }; 171 | 56C7EDCB16E260EB00FFBBF2 /* Supporting Files */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 56C7EDCC16E260EB00FFBBF2 /* iOS-PARStore-Info.plist */, 175 | 56C7EDCD16E260EB00FFBBF2 /* InfoPlist.strings */, 176 | 56C7EDD016E260EB00FFBBF2 /* main.m */, 177 | 56C7EDD216E260EB00FFBBF2 /* iOS-PARStore-Prefix.pch */, 178 | 56C7EDD616E260EB00FFBBF2 /* Default.png */, 179 | 56C7EDD816E260EB00FFBBF2 /* Default@2x.png */, 180 | 56C7EDDA16E260EB00FFBBF2 /* Default-568h@2x.png */, 181 | ); 182 | name = "Supporting Files"; 183 | sourceTree = ""; 184 | }; 185 | 56EAE15B16E24C7500A7F31F = { 186 | isa = PBXGroup; 187 | children = ( 188 | 569D022A16E25340002675BA /* README.markdown */, 189 | 569D022916E25340002675BA /* LICENSE-BSD.txt */, 190 | 56EAE17016E24C7500A7F31F /* Core */, 191 | 56EAE18D16E24C7500A7F31F /* Tests */, 192 | 56EAE1A416E24C9400A7F31F /* Mac */, 193 | 56C7EDCA16E260EB00FFBBF2 /* iOS */, 194 | 56EAE16916E24C7500A7F31F /* Frameworks */, 195 | 56EAE16716E24C7500A7F31F /* Products */, 196 | ); 197 | sourceTree = ""; 198 | }; 199 | 56EAE16716E24C7500A7F31F /* Products */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | 56EAE16616E24C7500A7F31F /* Mac-PARStore.app */, 203 | 56EAE18716E24C7500A7F31F /* Mac-PARStoreTests.xctest */, 204 | 56C7EDC216E260EA00FFBBF2 /* iOS-PARStore.app */, 205 | 56C7EDE116E260EB00FFBBF2 /* iOS-PARStoreTests.xctest */, 206 | ); 207 | name = Products; 208 | sourceTree = ""; 209 | }; 210 | 56EAE16916E24C7500A7F31F /* Frameworks */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 56FA3D761970359C00BF81D3 /* libsqlite3.dylib */, 214 | 56EAE16A16E24C7500A7F31F /* Cocoa.framework */, 215 | 56C7EDC416E260EA00FFBBF2 /* UIKit.framework */, 216 | 56C7EDC616E260EA00FFBBF2 /* Foundation.framework */, 217 | 56C7EDC816E260EA00FFBBF2 /* CoreGraphics.framework */, 218 | 56EAE16C16E24C7500A7F31F /* Other Frameworks */, 219 | ); 220 | name = Frameworks; 221 | sourceTree = ""; 222 | }; 223 | 56EAE16C16E24C7500A7F31F /* Other Frameworks */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 56EAE16D16E24C7500A7F31F /* AppKit.framework */, 227 | 56EAE16E16E24C7500A7F31F /* CoreData.framework */, 228 | 56EAE16F16E24C7500A7F31F /* Foundation.framework */, 229 | ); 230 | name = "Other Frameworks"; 231 | sourceTree = ""; 232 | }; 233 | 56EAE17016E24C7500A7F31F /* Core */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 569D021C16E24FDA002675BA /* PARDispatchQueue.h */, 237 | 569D021D16E24FDA002675BA /* PARDispatchQueue.m */, 238 | 56EAE1BC16E24EAA00A7F31F /* PARStore.h */, 239 | 56EAE1BD16E24EAA00A7F31F /* PARStore.m */, 240 | 56C7EE0816E2811E00FFBBF2 /* PARNotificationSemaphore.h */, 241 | 56C7EE0916E2811E00FFBBF2 /* PARNotificationSemaphore.m */, 242 | 560C93DF16F1272C00C0E890 /* NSError+Factory.h */, 243 | 560C93E016F1272C00C0E890 /* NSError+Factory.m */, 244 | ); 245 | path = Core; 246 | sourceTree = ""; 247 | }; 248 | 56EAE17116E24C7500A7F31F /* Supporting Files */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 56EAE1B316E24E5500A7F31F /* PARStore-Info.plist */, 252 | 56EAE1B416E24E5500A7F31F /* PARStore-Prefix.pch */, 253 | 56EAE1B616E24E7300A7F31F /* Credits.rtf */, 254 | 56EAE1B716E24E7300A7F31F /* InfoPlist.strings */, 255 | ); 256 | name = "Supporting Files"; 257 | path = PARStore; 258 | sourceTree = ""; 259 | }; 260 | 56EAE18D16E24C7500A7F31F /* Tests */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | 56C7EDFE16E270C600FFBBF2 /* PARStoreExample.h */, 264 | 56C7EDFF16E270C600FFBBF2 /* PARStoreExample.m */, 265 | 56C7EDFA16E26D0700FFBBF2 /* PARTestCase.h */, 266 | 56C7EDFB16E26D0700FFBBF2 /* PARTestCase.m */, 267 | 56EAE19416E24C7500A7F31F /* PARStoreTests.m */, 268 | 561148E6196E952800F488F2 /* PARSQLiteTests.m */, 269 | 56E196E41B448BE600A51AB0 /* PARDispatchQueueTests.m */, 270 | 56EAE18E16E24C7500A7F31F /* Supporting Files */, 271 | ); 272 | path = Tests; 273 | sourceTree = ""; 274 | }; 275 | 56EAE18E16E24C7500A7F31F /* Supporting Files */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 56EAE18F16E24C7500A7F31F /* PARStoreTests-Info.plist */, 279 | 56EAE19016E24C7500A7F31F /* InfoPlist.strings */, 280 | ); 281 | name = "Supporting Files"; 282 | sourceTree = ""; 283 | }; 284 | 56EAE1A416E24C9400A7F31F /* Mac */ = { 285 | isa = PBXGroup; 286 | children = ( 287 | 56EAE1B816E24E7300A7F31F /* main.m */, 288 | 569D021F16E2509F002675BA /* PARAppDelegate.h */, 289 | 569D022016E2509F002675BA /* PARAppDelegate.m */, 290 | 569D022216E250A7002675BA /* MainMenu.xib */, 291 | 56EAE17116E24C7500A7F31F /* Supporting Files */, 292 | ); 293 | path = Mac; 294 | sourceTree = ""; 295 | }; 296 | /* End PBXGroup section */ 297 | 298 | /* Begin PBXNativeTarget section */ 299 | 56C7EDC116E260EA00FFBBF2 /* iOS-PARStore */ = { 300 | isa = PBXNativeTarget; 301 | buildConfigurationList = 56C7EDF416E260EB00FFBBF2 /* Build configuration list for PBXNativeTarget "iOS-PARStore" */; 302 | buildPhases = ( 303 | 56C7EDBE16E260EA00FFBBF2 /* Sources */, 304 | 56C7EDBF16E260EA00FFBBF2 /* Frameworks */, 305 | 56C7EDC016E260EA00FFBBF2 /* Resources */, 306 | ); 307 | buildRules = ( 308 | ); 309 | dependencies = ( 310 | ); 311 | name = "iOS-PARStore"; 312 | productName = "iOS-PARStore"; 313 | productReference = 56C7EDC216E260EA00FFBBF2 /* iOS-PARStore.app */; 314 | productType = "com.apple.product-type.application"; 315 | }; 316 | 56C7EDE016E260EB00FFBBF2 /* iOS-PARStoreTests */ = { 317 | isa = PBXNativeTarget; 318 | buildConfigurationList = 56C7EDF516E260EB00FFBBF2 /* Build configuration list for PBXNativeTarget "iOS-PARStoreTests" */; 319 | buildPhases = ( 320 | 56C7EDDC16E260EB00FFBBF2 /* Sources */, 321 | 56C7EDDD16E260EB00FFBBF2 /* Frameworks */, 322 | 56C7EDDE16E260EB00FFBBF2 /* Resources */, 323 | ); 324 | buildRules = ( 325 | ); 326 | dependencies = ( 327 | 56C7EDE616E260EB00FFBBF2 /* PBXTargetDependency */, 328 | ); 329 | name = "iOS-PARStoreTests"; 330 | productName = "iOS-PARStoreTests"; 331 | productReference = 56C7EDE116E260EB00FFBBF2 /* iOS-PARStoreTests.xctest */; 332 | productType = "com.apple.product-type.bundle.unit-test"; 333 | }; 334 | 56EAE16516E24C7500A7F31F /* Mac-PARStore */ = { 335 | isa = PBXNativeTarget; 336 | buildConfigurationList = 56EAE19816E24C7500A7F31F /* Build configuration list for PBXNativeTarget "Mac-PARStore" */; 337 | buildPhases = ( 338 | 56EAE16216E24C7500A7F31F /* Sources */, 339 | 56EAE16316E24C7500A7F31F /* Frameworks */, 340 | 56EAE16416E24C7500A7F31F /* Resources */, 341 | ); 342 | buildRules = ( 343 | ); 344 | dependencies = ( 345 | ); 346 | name = "Mac-PARStore"; 347 | productName = PARStore; 348 | productReference = 56EAE16616E24C7500A7F31F /* Mac-PARStore.app */; 349 | productType = "com.apple.product-type.application"; 350 | }; 351 | 56EAE18616E24C7500A7F31F /* Mac-PARStoreTests */ = { 352 | isa = PBXNativeTarget; 353 | buildConfigurationList = 56EAE19B16E24C7500A7F31F /* Build configuration list for PBXNativeTarget "Mac-PARStoreTests" */; 354 | buildPhases = ( 355 | 56EAE18216E24C7500A7F31F /* Sources */, 356 | 56EAE18316E24C7500A7F31F /* Frameworks */, 357 | 56EAE18416E24C7500A7F31F /* Resources */, 358 | ); 359 | buildRules = ( 360 | ); 361 | dependencies = ( 362 | 56EAE18C16E24C7500A7F31F /* PBXTargetDependency */, 363 | ); 364 | name = "Mac-PARStoreTests"; 365 | productName = PARStoreTests; 366 | productReference = 56EAE18716E24C7500A7F31F /* Mac-PARStoreTests.xctest */; 367 | productType = "com.apple.product-type.bundle.unit-test"; 368 | }; 369 | /* End PBXNativeTarget section */ 370 | 371 | /* Begin PBXProject section */ 372 | 56EAE15D16E24C7500A7F31F /* Project object */ = { 373 | isa = PBXProject; 374 | attributes = { 375 | CLASSPREFIX = PAR; 376 | LastUpgradeCheck = 9999; 377 | ORGANIZATIONNAME = "Charles Parnot"; 378 | }; 379 | buildConfigurationList = 56EAE16016E24C7500A7F31F /* Build configuration list for PBXProject "PARStore" */; 380 | compatibilityVersion = "Xcode 3.2"; 381 | developmentRegion = en; 382 | hasScannedForEncodings = 0; 383 | knownRegions = ( 384 | en, 385 | Base, 386 | ); 387 | mainGroup = 56EAE15B16E24C7500A7F31F; 388 | productRefGroup = 56EAE16716E24C7500A7F31F /* Products */; 389 | projectDirPath = ""; 390 | projectRoot = ""; 391 | targets = ( 392 | 56EAE16516E24C7500A7F31F /* Mac-PARStore */, 393 | 56EAE18616E24C7500A7F31F /* Mac-PARStoreTests */, 394 | 56C7EDC116E260EA00FFBBF2 /* iOS-PARStore */, 395 | 56C7EDE016E260EB00FFBBF2 /* iOS-PARStoreTests */, 396 | ); 397 | }; 398 | /* End PBXProject section */ 399 | 400 | /* Begin PBXResourcesBuildPhase section */ 401 | 56C7EDC016E260EA00FFBBF2 /* Resources */ = { 402 | isa = PBXResourcesBuildPhase; 403 | buildActionMask = 2147483647; 404 | files = ( 405 | 56C7EDCF16E260EB00FFBBF2 /* InfoPlist.strings in Resources */, 406 | 56C7EDD716E260EB00FFBBF2 /* Default.png in Resources */, 407 | 56C7EDD916E260EB00FFBBF2 /* Default@2x.png in Resources */, 408 | 56C7EDDB16E260EB00FFBBF2 /* Default-568h@2x.png in Resources */, 409 | ); 410 | runOnlyForDeploymentPostprocessing = 0; 411 | }; 412 | 56C7EDDE16E260EB00FFBBF2 /* Resources */ = { 413 | isa = PBXResourcesBuildPhase; 414 | buildActionMask = 2147483647; 415 | files = ( 416 | 56C7EDF816E2688700FFBBF2 /* InfoPlist.strings in Resources */, 417 | ); 418 | runOnlyForDeploymentPostprocessing = 0; 419 | }; 420 | 56EAE16416E24C7500A7F31F /* Resources */ = { 421 | isa = PBXResourcesBuildPhase; 422 | buildActionMask = 2147483647; 423 | files = ( 424 | 56EAE1B916E24E7300A7F31F /* Credits.rtf in Resources */, 425 | 56EAE1BA16E24E7300A7F31F /* InfoPlist.strings in Resources */, 426 | 569D022316E250A7002675BA /* MainMenu.xib in Resources */, 427 | ); 428 | runOnlyForDeploymentPostprocessing = 0; 429 | }; 430 | 56EAE18416E24C7500A7F31F /* Resources */ = { 431 | isa = PBXResourcesBuildPhase; 432 | buildActionMask = 2147483647; 433 | files = ( 434 | 56EAE19216E24C7500A7F31F /* InfoPlist.strings in Resources */, 435 | ); 436 | runOnlyForDeploymentPostprocessing = 0; 437 | }; 438 | /* End PBXResourcesBuildPhase section */ 439 | 440 | /* Begin PBXSourcesBuildPhase section */ 441 | 56C7EDBE16E260EA00FFBBF2 /* Sources */ = { 442 | isa = PBXSourcesBuildPhase; 443 | buildActionMask = 2147483647; 444 | files = ( 445 | 56C7EDD116E260EB00FFBBF2 /* main.m in Sources */, 446 | 56C7EDD516E260EB00FFBBF2 /* PARAppDelegate.m in Sources */, 447 | 56C7EE0B16E2811E00FFBBF2 /* PARNotificationSemaphore.m in Sources */, 448 | 56C7EE0C16E2842A00FFBBF2 /* PARStoreExample.m in Sources */, 449 | 56C7EE0E16E2843700FFBBF2 /* PARStore.m in Sources */, 450 | 56C7EE1216E284A600FFBBF2 /* PARDispatchQueue.m in Sources */, 451 | 560C93E216F1272C00C0E890 /* NSError+Factory.m in Sources */, 452 | ); 453 | runOnlyForDeploymentPostprocessing = 0; 454 | }; 455 | 56C7EDDC16E260EB00FFBBF2 /* Sources */ = { 456 | isa = PBXSourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | 56C7EDF716E2687F00FFBBF2 /* PARStoreTests.m in Sources */, 460 | 56C7EDFD16E26D0700FFBBF2 /* PARTestCase.m in Sources */, 461 | ); 462 | runOnlyForDeploymentPostprocessing = 0; 463 | }; 464 | 56EAE16216E24C7500A7F31F /* Sources */ = { 465 | isa = PBXSourcesBuildPhase; 466 | buildActionMask = 2147483647; 467 | files = ( 468 | 56EAE1BB16E24E7300A7F31F /* main.m in Sources */, 469 | 56EAE1BE16E24EAA00A7F31F /* PARStore.m in Sources */, 470 | 569D021E16E24FDA002675BA /* PARDispatchQueue.m in Sources */, 471 | 569D022116E2509F002675BA /* PARAppDelegate.m in Sources */, 472 | 56C7EE0A16E2811E00FFBBF2 /* PARNotificationSemaphore.m in Sources */, 473 | 56C7EE0D16E2842B00FFBBF2 /* PARStoreExample.m in Sources */, 474 | 560C93E116F1272C00C0E890 /* NSError+Factory.m in Sources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | 56EAE18216E24C7500A7F31F /* Sources */ = { 479 | isa = PBXSourcesBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | 56EAE19516E24C7500A7F31F /* PARStoreTests.m in Sources */, 483 | 56E196E61B448CA300A51AB0 /* PARDispatchQueueTests.m in Sources */, 484 | 56C7EDFC16E26D0700FFBBF2 /* PARTestCase.m in Sources */, 485 | 561148E7196E952800F488F2 /* PARSQLiteTests.m in Sources */, 486 | ); 487 | runOnlyForDeploymentPostprocessing = 0; 488 | }; 489 | /* End PBXSourcesBuildPhase section */ 490 | 491 | /* Begin PBXTargetDependency section */ 492 | 56C7EDE616E260EB00FFBBF2 /* PBXTargetDependency */ = { 493 | isa = PBXTargetDependency; 494 | target = 56C7EDC116E260EA00FFBBF2 /* iOS-PARStore */; 495 | targetProxy = 56C7EDE516E260EB00FFBBF2 /* PBXContainerItemProxy */; 496 | }; 497 | 56EAE18C16E24C7500A7F31F /* PBXTargetDependency */ = { 498 | isa = PBXTargetDependency; 499 | target = 56EAE16516E24C7500A7F31F /* Mac-PARStore */; 500 | targetProxy = 56EAE18B16E24C7500A7F31F /* PBXContainerItemProxy */; 501 | }; 502 | /* End PBXTargetDependency section */ 503 | 504 | /* Begin PBXVariantGroup section */ 505 | 56C7EDCD16E260EB00FFBBF2 /* InfoPlist.strings */ = { 506 | isa = PBXVariantGroup; 507 | children = ( 508 | 56C7EDCE16E260EB00FFBBF2 /* en */, 509 | ); 510 | name = InfoPlist.strings; 511 | sourceTree = ""; 512 | }; 513 | 56EAE19016E24C7500A7F31F /* InfoPlist.strings */ = { 514 | isa = PBXVariantGroup; 515 | children = ( 516 | 56EAE19116E24C7500A7F31F /* en */, 517 | ); 518 | name = InfoPlist.strings; 519 | sourceTree = ""; 520 | }; 521 | /* End PBXVariantGroup section */ 522 | 523 | /* Begin XCBuildConfiguration section */ 524 | 56C7EDF016E260EB00FFBBF2 /* Debug */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 528 | FRAMEWORK_SEARCH_PATHS = ( 529 | "$(inherited)", 530 | "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", 531 | ); 532 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 533 | GCC_PREFIX_HEADER = "iOS/iOS-PARStore-Prefix.pch"; 534 | INFOPLIST_FILE = "iOS/iOS-PARStore-Info.plist"; 535 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 536 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 537 | PRODUCT_NAME = "$(TARGET_NAME)"; 538 | SDKROOT = iphoneos; 539 | TARGETED_DEVICE_FAMILY = "1,2"; 540 | WRAPPER_EXTENSION = app; 541 | }; 542 | name = Debug; 543 | }; 544 | 56C7EDF116E260EB00FFBBF2 /* Release */ = { 545 | isa = XCBuildConfiguration; 546 | buildSettings = { 547 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 548 | FRAMEWORK_SEARCH_PATHS = ( 549 | "$(inherited)", 550 | "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", 551 | ); 552 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 553 | GCC_PREFIX_HEADER = "iOS/iOS-PARStore-Prefix.pch"; 554 | INFOPLIST_FILE = "iOS/iOS-PARStore-Info.plist"; 555 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 556 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 557 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 558 | PRODUCT_NAME = "$(TARGET_NAME)"; 559 | SDKROOT = iphoneos; 560 | TARGETED_DEVICE_FAMILY = "1,2"; 561 | VALIDATE_PRODUCT = YES; 562 | WRAPPER_EXTENSION = app; 563 | }; 564 | name = Release; 565 | }; 566 | 56C7EDF216E260EB00FFBBF2 /* Debug */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS-PARStore.app/iOS-PARStore"; 570 | FRAMEWORK_SEARCH_PATHS = ( 571 | "\"$(SDKROOT)/Developer/Library/Frameworks\"", 572 | "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", 573 | ); 574 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 575 | GCC_PREFIX_HEADER = "iOS/iOS-PARStore-Prefix.pch"; 576 | INFOPLIST_FILE = "iOS/iOS-PARStore-Info.plist"; 577 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 578 | OTHER_LDFLAGS = ( 579 | "$(inherited)", 580 | "-framework", 581 | XCTest, 582 | ); 583 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 584 | PRODUCT_NAME = "$(TARGET_NAME)"; 585 | SDKROOT = iphoneos; 586 | TEST_HOST = "$(BUNDLE_LOADER)"; 587 | WRAPPER_EXTENSION = xctest; 588 | }; 589 | name = Debug; 590 | }; 591 | 56C7EDF316E260EB00FFBBF2 /* Release */ = { 592 | isa = XCBuildConfiguration; 593 | buildSettings = { 594 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS-PARStore.app/iOS-PARStore"; 595 | FRAMEWORK_SEARCH_PATHS = ( 596 | "\"$(SDKROOT)/Developer/Library/Frameworks\"", 597 | "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", 598 | ); 599 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 600 | GCC_PREFIX_HEADER = "iOS/iOS-PARStore-Prefix.pch"; 601 | INFOPLIST_FILE = "iOS/iOS-PARStore-Info.plist"; 602 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 603 | OTHER_LDFLAGS = ( 604 | "$(inherited)", 605 | "-framework", 606 | XCTest, 607 | ); 608 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 609 | PRODUCT_NAME = "$(TARGET_NAME)"; 610 | SDKROOT = iphoneos; 611 | TEST_HOST = "$(BUNDLE_LOADER)"; 612 | VALIDATE_PRODUCT = YES; 613 | WRAPPER_EXTENSION = xctest; 614 | }; 615 | name = Release; 616 | }; 617 | 56EAE19616E24C7500A7F31F /* Debug */ = { 618 | isa = XCBuildConfiguration; 619 | buildSettings = { 620 | ALWAYS_SEARCH_USER_PATHS = NO; 621 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 622 | CLANG_CXX_LIBRARY = "libc++"; 623 | CLANG_ENABLE_OBJC_ARC = YES; 624 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 625 | CLANG_WARN_BOOL_CONVERSION = YES; 626 | CLANG_WARN_COMMA = YES; 627 | CLANG_WARN_CONSTANT_CONVERSION = YES; 628 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 629 | CLANG_WARN_EMPTY_BODY = YES; 630 | CLANG_WARN_ENUM_CONVERSION = YES; 631 | CLANG_WARN_INFINITE_RECURSION = YES; 632 | CLANG_WARN_INT_CONVERSION = YES; 633 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 634 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 635 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 636 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 637 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 638 | CLANG_WARN_STRICT_PROTOTYPES = YES; 639 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 640 | CLANG_WARN_UNREACHABLE_CODE = YES; 641 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 642 | COPY_PHASE_STRIP = NO; 643 | ENABLE_STRICT_OBJC_MSGSEND = YES; 644 | ENABLE_TESTABILITY = YES; 645 | GCC_C_LANGUAGE_STANDARD = gnu99; 646 | GCC_DYNAMIC_NO_PIC = NO; 647 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 648 | GCC_NO_COMMON_BLOCKS = YES; 649 | GCC_OPTIMIZATION_LEVEL = 0; 650 | GCC_PREPROCESSOR_DEFINITIONS = ( 651 | "DEBUG=1", 652 | "$(inherited)", 653 | ); 654 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 655 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 656 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 657 | GCC_WARN_UNDECLARED_SELECTOR = YES; 658 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 659 | GCC_WARN_UNUSED_FUNCTION = YES; 660 | GCC_WARN_UNUSED_VARIABLE = YES; 661 | MACOSX_DEPLOYMENT_TARGET = 10.8; 662 | ONLY_ACTIVE_ARCH = YES; 663 | SDKROOT = macosx; 664 | }; 665 | name = Debug; 666 | }; 667 | 56EAE19716E24C7500A7F31F /* Release */ = { 668 | isa = XCBuildConfiguration; 669 | buildSettings = { 670 | ALWAYS_SEARCH_USER_PATHS = NO; 671 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 672 | CLANG_CXX_LIBRARY = "libc++"; 673 | CLANG_ENABLE_OBJC_ARC = YES; 674 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 675 | CLANG_WARN_BOOL_CONVERSION = YES; 676 | CLANG_WARN_COMMA = YES; 677 | CLANG_WARN_CONSTANT_CONVERSION = YES; 678 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 679 | CLANG_WARN_EMPTY_BODY = YES; 680 | CLANG_WARN_ENUM_CONVERSION = YES; 681 | CLANG_WARN_INFINITE_RECURSION = YES; 682 | CLANG_WARN_INT_CONVERSION = YES; 683 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 684 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 685 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 686 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 687 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 688 | CLANG_WARN_STRICT_PROTOTYPES = YES; 689 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 690 | CLANG_WARN_UNREACHABLE_CODE = YES; 691 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 692 | COPY_PHASE_STRIP = YES; 693 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 694 | ENABLE_STRICT_OBJC_MSGSEND = YES; 695 | GCC_C_LANGUAGE_STANDARD = gnu99; 696 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 697 | GCC_NO_COMMON_BLOCKS = YES; 698 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 699 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 700 | GCC_WARN_UNDECLARED_SELECTOR = YES; 701 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 702 | GCC_WARN_UNUSED_FUNCTION = YES; 703 | GCC_WARN_UNUSED_VARIABLE = YES; 704 | MACOSX_DEPLOYMENT_TARGET = 10.8; 705 | SDKROOT = macosx; 706 | }; 707 | name = Release; 708 | }; 709 | 56EAE19916E24C7500A7F31F /* Debug */ = { 710 | isa = XCBuildConfiguration; 711 | buildSettings = { 712 | CLANG_ENABLE_MODULES = YES; 713 | COMBINE_HIDPI_IMAGES = YES; 714 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 715 | GCC_PREFIX_HEADER = "Mac/PARStore-Prefix.pch"; 716 | INFOPLIST_FILE = "Mac/PARStore-Info.plist"; 717 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 718 | PRODUCT_NAME = "$(TARGET_NAME)"; 719 | WRAPPER_EXTENSION = app; 720 | }; 721 | name = Debug; 722 | }; 723 | 56EAE19A16E24C7500A7F31F /* Release */ = { 724 | isa = XCBuildConfiguration; 725 | buildSettings = { 726 | CLANG_ENABLE_MODULES = YES; 727 | COMBINE_HIDPI_IMAGES = YES; 728 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 729 | GCC_PREFIX_HEADER = "Mac/PARStore-Prefix.pch"; 730 | INFOPLIST_FILE = "Mac/PARStore-Info.plist"; 731 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 732 | PRODUCT_NAME = "$(TARGET_NAME)"; 733 | WRAPPER_EXTENSION = app; 734 | }; 735 | name = Release; 736 | }; 737 | 56EAE19C16E24C7500A7F31F /* Debug */ = { 738 | isa = XCBuildConfiguration; 739 | buildSettings = { 740 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Mac-PARStore.app/Contents/MacOS/Mac-PARStore"; 741 | COMBINE_HIDPI_IMAGES = YES; 742 | FRAMEWORK_SEARCH_PATHS = "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\""; 743 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 744 | GCC_PREFIX_HEADER = "Tests/PARStore-Prefix.pch"; 745 | INFOPLIST_FILE = "Tests/PARStoreTests-Info.plist"; 746 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 747 | PRODUCT_NAME = "$(TARGET_NAME)"; 748 | TEST_HOST = "$(BUNDLE_LOADER)"; 749 | }; 750 | name = Debug; 751 | }; 752 | 56EAE19D16E24C7500A7F31F /* Release */ = { 753 | isa = XCBuildConfiguration; 754 | buildSettings = { 755 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Mac-PARStore.app/Contents/MacOS/Mac-PARStore"; 756 | COMBINE_HIDPI_IMAGES = YES; 757 | FRAMEWORK_SEARCH_PATHS = "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\""; 758 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 759 | GCC_PREFIX_HEADER = "Tests/PARStore-Prefix.pch"; 760 | INFOPLIST_FILE = "Tests/PARStoreTests-Info.plist"; 761 | PRODUCT_BUNDLE_IDENTIFIER = "com.parnot.${PRODUCT_NAME:rfc1034identifier}"; 762 | PRODUCT_NAME = "$(TARGET_NAME)"; 763 | TEST_HOST = "$(BUNDLE_LOADER)"; 764 | }; 765 | name = Release; 766 | }; 767 | /* End XCBuildConfiguration section */ 768 | 769 | /* Begin XCConfigurationList section */ 770 | 56C7EDF416E260EB00FFBBF2 /* Build configuration list for PBXNativeTarget "iOS-PARStore" */ = { 771 | isa = XCConfigurationList; 772 | buildConfigurations = ( 773 | 56C7EDF016E260EB00FFBBF2 /* Debug */, 774 | 56C7EDF116E260EB00FFBBF2 /* Release */, 775 | ); 776 | defaultConfigurationIsVisible = 0; 777 | defaultConfigurationName = Release; 778 | }; 779 | 56C7EDF516E260EB00FFBBF2 /* Build configuration list for PBXNativeTarget "iOS-PARStoreTests" */ = { 780 | isa = XCConfigurationList; 781 | buildConfigurations = ( 782 | 56C7EDF216E260EB00FFBBF2 /* Debug */, 783 | 56C7EDF316E260EB00FFBBF2 /* Release */, 784 | ); 785 | defaultConfigurationIsVisible = 0; 786 | defaultConfigurationName = Release; 787 | }; 788 | 56EAE16016E24C7500A7F31F /* Build configuration list for PBXProject "PARStore" */ = { 789 | isa = XCConfigurationList; 790 | buildConfigurations = ( 791 | 56EAE19616E24C7500A7F31F /* Debug */, 792 | 56EAE19716E24C7500A7F31F /* Release */, 793 | ); 794 | defaultConfigurationIsVisible = 0; 795 | defaultConfigurationName = Release; 796 | }; 797 | 56EAE19816E24C7500A7F31F /* Build configuration list for PBXNativeTarget "Mac-PARStore" */ = { 798 | isa = XCConfigurationList; 799 | buildConfigurations = ( 800 | 56EAE19916E24C7500A7F31F /* Debug */, 801 | 56EAE19A16E24C7500A7F31F /* Release */, 802 | ); 803 | defaultConfigurationIsVisible = 0; 804 | defaultConfigurationName = Release; 805 | }; 806 | 56EAE19B16E24C7500A7F31F /* Build configuration list for PBXNativeTarget "Mac-PARStoreTests" */ = { 807 | isa = XCConfigurationList; 808 | buildConfigurations = ( 809 | 56EAE19C16E24C7500A7F31F /* Debug */, 810 | 56EAE19D16E24C7500A7F31F /* Release */, 811 | ); 812 | defaultConfigurationIsVisible = 0; 813 | defaultConfigurationName = Release; 814 | }; 815 | /* End XCConfigurationList section */ 816 | }; 817 | rootObject = 56EAE15D16E24C7500A7F31F /* Project object */; 818 | } 819 | --------------------------------------------------------------------------------