diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ddd2209 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +Closes # + +## What It Does + +## How to Test + +## Notes diff --git a/Assets/screenshots.png b/Assets/screenshots.png new file mode 100644 index 0000000..0d2956e Binary files /dev/null and b/Assets/screenshots.png differ diff --git a/Example/Gemfile b/Example/Gemfile index f7f70bc..228f10d 100644 --- a/Example/Gemfile +++ b/Example/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'cocoapods', '1.0.0' +gem 'cocoapods', '1.0.1' diff --git a/Example/Gemfile.lock b/Example/Gemfile.lock index ba80604..69fe7e7 100644 --- a/Example/Gemfile.lock +++ b/Example/Gemfile.lock @@ -8,10 +8,10 @@ GEM thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) claide (1.0.0) - cocoapods (1.0.0) + cocoapods (1.0.1) activesupport (>= 4.0.2) claide (>= 1.0.0, < 2.0) - cocoapods-core (= 1.0.0) + cocoapods-core (= 1.0.1) cocoapods-deintegrate (>= 1.0.0, < 2.0) cocoapods-downloader (>= 1.0.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -24,8 +24,8 @@ GEM fourflusher (~> 0.3.0) molinillo (~> 0.4.5) nap (~> 1.0) - xcodeproj (>= 1.0.0, < 2.0) - cocoapods-core (1.0.0) + xcodeproj (>= 1.1.0, < 2.0) + cocoapods-core (1.0.1) activesupport (>= 4.0.2) fuzzy_match (~> 2.0.4) nap (~> 1.0) @@ -41,18 +41,18 @@ GEM cocoapods-try (1.0.0) colored (1.2) escape (0.0.4) - fourflusher (0.3.0) + fourflusher (0.3.1) fuzzy_match (2.0.4) i18n (0.7.0) json (1.8.3) - minitest (5.8.4) + minitest (5.9.0) molinillo (0.4.5) nap (1.1.0) netrc (0.7.8) thread_safe (0.3.5) tzinfo (1.2.2) thread_safe (~> 0.1) - xcodeproj (1.0.0) + xcodeproj (1.1.0) activesupport (>= 3) claide (>= 1.0.0, < 2.0) colored (~> 1.2) @@ -61,7 +61,7 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (= 1.0.0) + cocoapods (= 1.0.1) BUNDLED WITH - 1.11.2 + 1.12.5 diff --git a/Example/PinpointKitExample.xcodeproj/project.pbxproj b/Example/PinpointKitExample.xcodeproj/project.pbxproj index 2b57b01..e7db788 100644 --- a/Example/PinpointKitExample.xcodeproj/project.pbxproj +++ b/Example/PinpointKitExample.xcodeproj/project.pbxproj @@ -97,12 +97,12 @@ isa = PBXNativeTarget; buildConfigurationList = DA19C3F61C67D4420016861F /* Build configuration list for PBXNativeTarget "PinpointKitExample" */; buildPhases = ( - 072623A5468F27A0975A871A /* 📦 Check Pods Manifest.lock */, + 072623A5468F27A0975A871A /* [CP] Check Pods Manifest.lock */, DA19C3E01C67D4420016861F /* Sources */, DA19C3E11C67D4420016861F /* Frameworks */, DA19C3E21C67D4420016861F /* Resources */, - E9FAB3019A9E163A9B828A51 /* 📦 Embed Pods Frameworks */, - 8B1639EE38352A1103B1DD1B /* 📦 Copy Pods Resources */, + E9FAB3019A9E163A9B828A51 /* [CP] Embed Pods Frameworks */, + 8B1639EE38352A1103B1DD1B /* [CP] Copy Pods Resources */, F2ACE9C71CEF90FD00A461E5 /* Swiftlint */, ); buildRules = ( @@ -121,11 +121,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 0820; ORGANIZATIONNAME = Lickability; TargetAttributes = { DA19C3E31C67D4420016861F = { CreatedOnToolsVersion = 7.2.1; + LastSwiftMigration = 0800; }; }; }; @@ -161,14 +162,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 072623A5468F27A0975A871A /* 📦 Check Pods Manifest.lock */ = { + 072623A5468F27A0975A871A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "📦 Check Pods Manifest.lock"; + name = "[CP] Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -176,14 +177,14 @@ shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; - 8B1639EE38352A1103B1DD1B /* 📦 Copy Pods Resources */ = { + 8B1639EE38352A1103B1DD1B /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "📦 Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -191,14 +192,14 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PinpointKitExample/Pods-PinpointKitExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - E9FAB3019A9E163A9B828A51 /* 📦 Embed Pods Frameworks */ = { + E9FAB3019A9E163A9B828A51 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "📦 Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -267,8 +268,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -312,8 +315,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -341,11 +346,13 @@ isa = XCBuildConfiguration; baseConfigurationReference = B38C00C57F922ED4177A0F43 /* Pods-PinpointKitExample.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = PinpointKitExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.Lickability.PinpointKitExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -353,11 +360,14 @@ isa = XCBuildConfiguration; baseConfigurationReference = 043312B73ABE453B100EB875 /* Pods-PinpointKitExample.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = PinpointKitExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.Lickability.PinpointKitExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/Example/PinpointKitExample/AppDelegate.swift b/Example/PinpointKitExample/AppDelegate.swift index 37e8015..0f2b3d0 100644 --- a/Example/PinpointKitExample/AppDelegate.swift +++ b/Example/PinpointKitExample/AppDelegate.swift @@ -11,10 +11,10 @@ import PinpointKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - lazy var window: UIWindow? = ShakeDetectingWindow(frame: UIScreen.mainScreen().bounds) - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { NSLog("Initial test log for the system logger.") return true } diff --git a/Example/PinpointKitExample/Info.plist b/Example/PinpointKitExample/Info.plist index 40c6215..0180b3e 100644 --- a/Example/PinpointKitExample/Info.plist +++ b/Example/PinpointKitExample/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/Example/PinpointKitExample/ViewController.swift b/Example/PinpointKitExample/ViewController.swift index 2a2a005..03e6c0f 100644 --- a/Example/PinpointKitExample/ViewController.swift +++ b/Example/PinpointKitExample/ViewController.swift @@ -11,6 +11,8 @@ import PinpointKit final class ViewController: UITableViewController { + fileprivate let pinpointKit = PinpointKit(feedbackRecipients: ["feedback@example.com"]) + override func viewDidLoad() { super.viewDidLoad() @@ -18,9 +20,9 @@ final class ViewController: UITableViewController { tableView.tableFooterView = UIView() } - override func viewDidAppear(animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - PinpointKit.defaultPinpointKit.show(fromViewController: self) + pinpointKit.show(from: self) } } diff --git a/Example/Podfile.lock b/Example/Podfile.lock index f192beb..d71e750 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,16 +1,18 @@ PODS: - - PinpointKit (0.9) + - PinpointKit (0.9): + - PinpointKit/Core (= 0.9) + - PinpointKit/Core (0.9) DEPENDENCIES: - PinpointKit (from `../`) EXTERNAL SOURCES: PinpointKit: - :path: "../" + :path: ../ SPEC CHECKSUMS: - PinpointKit: 79a54c1642c37fe466c8ff128f98ae0147067255 + PinpointKit: 6d19d08afddfad2753aee95e2a6160f2d8d30764 PODFILE CHECKSUM: 57140b686c94ba1e5869bbc5b91a657c508da681 -COCOAPODS: 1.0.0 +COCOAPODS: 1.0.1 diff --git a/Example/Pods/Local Podspecs/PinpointKit.podspec.json b/Example/Pods/Local Podspecs/PinpointKit.podspec.json index 5cdd4e4..d3bb654 100644 --- a/Example/Pods/Local Podspecs/PinpointKit.podspec.json +++ b/Example/Pods/Local Podspecs/PinpointKit.podspec.json @@ -6,9 +6,9 @@ "git": "https://github.com/Lickability/PinpointKit.git", "tag": "0.9" }, - "summary": "A library that makes bug reporting dead simple for your testers and users by allowing them to send feedback with annotated screenshots and logs using a simple gesture.", + "summary": "A library that makes bug reporting simple for your users by allowing them to send feedback with annotated screenshots and logs.", "authors": { - "Matthew Bishoff": "matt@lickability.com", + "Matthew Bischoff": "matt@lickability.com", "Brian Capps": "brian@lickability.com", "Kenneth Ackerson": "pearapps@gmail.com", "Paul Rehkugler": "paul@rehkugler.com", @@ -25,11 +25,26 @@ "requires_arc": true, "frameworks": [ "Foundation", - "UIKit", - "MessageUI" + "UIKit" ], - "source_files": "PinpointKit/PinpointKit/Sources/**/*.{h,m,swift}", - "resources": "PinpointKit/PinpointKit/Resources/*", + "default_subspec": "Core", "dependencies": { - } + }, + "subspecs": [ + { + "name": "Core", + "resources": "PinpointKit/PinpointKit/Resources/*", + "source_files": "PinpointKit/PinpointKit/Sources/Core/**/*.{h,m,swift}", + "frameworks": [ + "MessageUI" + ] + }, + { + "name": "ScreenshotDetector", + "source_files": "PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift", + "frameworks": [ + "Photos" + ] + } + ] } diff --git a/Example/Pods/Manifest.lock b/Example/Pods/Manifest.lock index f192beb..d71e750 100644 --- a/Example/Pods/Manifest.lock +++ b/Example/Pods/Manifest.lock @@ -1,16 +1,18 @@ PODS: - - PinpointKit (0.9) + - PinpointKit (0.9): + - PinpointKit/Core (= 0.9) + - PinpointKit/Core (0.9) DEPENDENCIES: - PinpointKit (from `../`) EXTERNAL SOURCES: PinpointKit: - :path: "../" + :path: ../ SPEC CHECKSUMS: - PinpointKit: 79a54c1642c37fe466c8ff128f98ae0147067255 + PinpointKit: 6d19d08afddfad2753aee95e2a6160f2d8d30764 PODFILE CHECKSUM: 57140b686c94ba1e5869bbc5b91a657c508da681 -COCOAPODS: 1.0.0 +COCOAPODS: 1.0.1 diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index d495bbf..2a576e9 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -7,69 +7,70 @@ objects = { /* Begin PBXBuildFile section */ - 0CDD1E82AC1F536E545B685D19338E14 /* Screenshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA61C7BF94F4ABC2B29F361860A79D0 /* Screenshotter.swift */; }; + 052288E94955D100411AA4FE1DEC9F11 /* KeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A854BB38F24F744BF2349410D98FEEE /* KeyboardAvoider.swift */; }; + 07EE007F2967839604C0C05A4BC4452B /* BlurAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E17E108AEFE438BC057AC55DC2096EB /* BlurAnnotationView.swift */; }; + 090599AEA853FC31AAB0BF24D8975037 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18962FC1A0DE4E3F807BE051554B5ACA /* NavigationController.swift */; }; + 0AE2BA2248C245B279E9BFF8ABB68B80 /* LogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78908646D4652689C30424B2FF2460B6 /* LogCollector.swift */; }; 0D958AB08F4BCB483E62B27AEA941426 /* Pods-PinpointKitExample-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CB56BD873C4BB171888105C6F1B545 /* Pods-PinpointKitExample-dummy.m */; }; - 15B8BBF0A81A8F699181A197A2149430 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172CDF3DB78C9F30D084A77D31F1F4E3 /* Sender.swift */; }; - 15E941F20C1943D96E3D0936B2C3429A /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEF610D46B4270498BF110D72EA7ACF8 /* Feedback.swift */; }; - 184F4416F43088C1D8E3A507017556A4 /* PinpointKit+ShakePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F573EE7EDBA5A16D155BC6E24C93EFF5 /* PinpointKit+ShakePresentation.swift */; }; - 1975343A340D6045F934EF805B9A15B5 /* AnnotationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE6113790653EAF8C1A1F7771B519259 /* AnnotationsView.swift */; }; - 1DBD1828CBDA3C905AE7CE05F9A8481F /* LogSupporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E7572A7CB5048145FA44393800859C5 /* LogSupporting.swift */; }; - 20871F1B481AC58F430CF78B90D13DB3 /* ArrowAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F026C9460FBD43D5F04EF65D2FB5362 /* ArrowAnnotationView.swift */; }; - 212F3C7EDD1D6C961F9298ED94C4A9D2 /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6945F4F6DE5BFD450D048F0081B46ACD /* Editor.swift */; }; - 23B0CF85CAF7C4FEC66DC837BF3A4A30 /* PinpointKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FA3A013B33F2A51586C9688B165DF1E /* PinpointKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 274E0F9158A3D5EE42F4D5626D7189A1 /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16C7534B3413DA346F0A193C76B2F0DA /* EditImageViewController.swift */; }; - 2AEE21DAF5E1BB743C7C724F2C0580C5 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56326EAEA58486DF118DDF5FACD210D /* CheckmarkCell.swift */; }; - 2C685245784FDB4D0343BD5B146F5F7A /* SourceSansPro-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 114AC180CB4E848C4C81449DAFA9C175 /* SourceSansPro-Bold.ttf */; }; - 2C7CF2CCC87578CA7B505217184A57CB /* ASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 283F4DBE0CD6CB330F0E6EAF93B49577 /* ASLLogger.m */; }; - 2CFEF21D51BCD815A47B017BC02338E8 /* BezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF521E391DB361765406F3397FD7F781 /* BezierPath.swift */; }; - 38BD98D6226E3C07577B8FED5E430E95 /* EditorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B1CD047C1B63EF67F23CAC1806889A /* EditorDelegate.swift */; }; - 39DA97B343CED7D31970E425862EDE70 /* MIMEType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2098013DDAC70242CAB55164809DF297 /* MIMEType.swift */; }; - 40A2B3349ED694133C5072C4DBC22CFA /* ASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AA8B2FF3CF646963D04752E114AA1C3 /* ASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 42FE10253FD7560124D2EAD781E2E445 /* MailSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBBCFF091CD4792072990AF081188C4 /* MailSender.swift */; }; - 430208290E6932815B760A0E4BAAA5B9 /* InterfaceCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53932A79D0015680899AEDBCE408C7A /* InterfaceCustomization.swift */; }; - 449AFC32CE95BD390F974C89B39DE409 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1673F23E10FFE664DC2DBCD7EECB763E /* ScreenshotDetector.swift */; }; - 461673AF60A95E83F3776A8341AC4DD8 /* KeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12221873AA420451874E0329C9CF086A /* KeyboardAvoider.swift */; }; - 4A04FE15DA509B5C89CE556770245EC7 /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C945B11B38B63C2485E7AC2F0A4652C9 /* FeedbackViewController.swift */; }; - 4C960F20EE40614A8AE012A37C165FF5 /* FeedbackTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D2FD3F625B717DC2FCD4D8F304E84A8 /* FeedbackTableViewDataSource.swift */; }; - 52D5A06B4C07CE372B8749304209A3BF /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCB6A9253314806490AED31CDE58272 /* UIColor+Palette.swift */; }; - 5644F51D8190C003E02FB4E303089D9B /* InterfaceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84AA0A8E8AF047564608AB6495DFF45 /* InterfaceCustomizable.swift */; }; - 58530163CC392331994316FA8DC8E67B /* PinpointKit.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AF939EA575E5EEA924172E930A87A9B /* PinpointKit.xcassets */; }; - 589E1AA30F83B11752923700A4CE76B2 /* ShakeDetectingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 926EAD1738C6D99FD0C0899CF01077AD /* ShakeDetectingWindow.swift */; }; - 5A57570E115B0D171402D0E96E761283 /* BasicLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CC4696A751768B9105D5565B68C3DB /* BasicLogViewController.swift */; }; - 5F7A1725DDFAAF2976FBF1C8FC5B6430 /* PinpointKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6207788D80D5F34F8E7DB00DE24A8F3E /* PinpointKit-dummy.m */; }; - 63648CCA2D63BFB731822F2120D4E739 /* AnnotationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADA031C084B042487773A8A86B9BB /* AnnotationViewFactory.swift */; }; - 638502A53000777AA194EA8426E35D77 /* NSBundle+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B5C878C283429B54DCBEC1840E5F8B /* NSBundle+PinpointKit.swift */; }; - 63B94E01A6382BB6BE9CBD5A25C047D6 /* StrokeLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAE808FC0873920E02EBCD77AD9FA43 /* StrokeLayoutManager.swift */; }; - 69463102D52C36224BAC8D49C26BE7F1 /* FeedbackCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421EAA998DAEFE3DD043FD4FDC40CA43 /* FeedbackCollector.swift */; }; - 6C7E948C1A6FB00FC132A51F613FE30E /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C940A5E713141F02CCF9A1C624E84C /* AnnotationView.swift */; }; - 7B32F67CF980FB5CBE4D2BA900F9DC95 /* SourceSansPro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E2E9D9A1E5A484B9629EFB4384002E67 /* SourceSansPro-Regular.ttf */; }; + 0E046F6AC76824707FECA1DB6F92C846 /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4073753FF952CDEBA8886D81069494B9 /* AnnotationView.swift */; }; + 139EC01917C8A5E855DC504DE3106051 /* StrokeLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5266A47E48F4745A5C6CEB10BDEAD7 /* StrokeLayoutManager.swift */; }; + 1EE4B8A5864E362992D85FC3D2646630 /* MIMEType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2615B82A48EAC611A3313F89FD1A891F /* MIMEType.swift */; }; + 1F68C92F7239601360DDA69BC26CA965 /* BoxAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CCE8624D671A64E58406872434CE53 /* BoxAnnotationView.swift */; }; + 202DB1E255DA0579773DB4DEB6CDBDE3 /* SystemLogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A77B23F366FD990B4833351E17066E /* SystemLogCollector.swift */; }; + 23B0CF85CAF7C4FEC66DC837BF3A4A30 /* PinpointKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 654E1E03496C74FF2BFAF4E714E361C2 /* PinpointKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 28C43B958164BAA8EC37092362084B99 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275A336229C4B1E788BF9857C4C390CD /* CheckmarkCell.swift */; }; + 2C685245784FDB4D0343BD5B146F5F7A /* SourceSansPro-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D14DECCAEF3C55BC06DCFAA3D6350AAF /* SourceSansPro-Bold.ttf */; }; + 3A526401832AC915A3D9B8950C39A2BF /* AnnotationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D519026547DE41B73B24649A7CB4F322 /* AnnotationsView.swift */; }; + 3C9CEEA4543560AAD89A40052B2A68A5 /* InterfaceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBE3F2E390DC6EF01039DA4F85E70A37 /* InterfaceCustomizable.swift */; }; + 3D438FB5923C138A78AA3CB2245BA23F /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB4256B443AF96A12DA037BBA014A4 /* Configuration.swift */; }; + 3DA473AE305F534F930C9A38CFC71D03 /* NSBundle+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D36298B8F364B1D3BDE61F356E44F0B /* NSBundle+PinpointKit.swift */; }; + 40A2B3349ED694133C5072C4DBC22CFA /* ASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = CBE6EBE3EE7E185D19A3DD6CD0BAA6F9 /* ASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 46AEAECF7531C2384192AAA51FDE6930 /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE276403499131D008CB6BA709475D0 /* UIColor+Palette.swift */; }; + 48F822008D266B4351F8FF4CDBF93442 /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C238D1A284842EB711EB29E9B69F299 /* Editor.swift */; }; + 4CB771201DCD316C001FC154 /* EditImageViewControllerBarButtonItemProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB7711F1DCD316C001FC154 /* EditImageViewControllerBarButtonItemProviding.swift */; }; + 4F23890ECB2FCD34611153312EB69303 /* TextAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC5604F3145AF00D9544D70E3B9C53 /* TextAnnotationView.swift */; }; + 518C1C0194CD3DE3DEDF951FE46BEED4 /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7309EA9BDF3948F545BC8DF65395B68D /* FeedbackViewController.swift */; }; + 578A5E810F8762F2A2505A7BC1F52370 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F61D6816EA76CDC4EDF0F1337891CC /* Screen.swift */; }; + 58530163CC392331994316FA8DC8E67B /* PinpointKit.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7D1971C87043401AF64BE6C207B36EE /* PinpointKit.xcassets */; }; + 5AEBDF41B7855864EB12FACA4FFCB160 /* Screenshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 521052D2CB6D84A7FEBDBFFCE2A73EEF /* Screenshotter.swift */; }; + 6BDD572C71D7C354751FD2EADB79A9FD /* ShakeDetectingWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E130DAE16C32980DA74035C2A7F56DA /* ShakeDetectingWindowDelegate.swift */; }; + 7582B792320B8C5FAF732C5824B76427 /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE2A7251C04383DC54945C6085E87F0 /* Tool.swift */; }; + 7B32F67CF980FB5CBE4D2BA900F9DC95 /* SourceSansPro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D3D575C61D6FED5D4E5616BF2C223E7E /* SourceSansPro-Regular.ttf */; }; + 7C7E2F55BD7C12777217CCE1A2E259EA /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B04D415DC6B7BCECCA3E82A9811CEB /* Annotations.swift */; }; 7D827D77ECEA52041157A8B27B4230FF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E42A9844F76F9E28FEEC5A0F0037FF76 /* UIKit.framework */; }; - 8367F623DD9416EB26F83B758471ED2C /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D08FD24EF24252EBB4B332CA01B193 /* Tool.swift */; }; + 7E0ACA11E6347B7A21E04ECF670C774D /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 442353394785B418C9A5C78E02204798 /* EditImageViewController.swift */; }; + 83473796958353E8BE8EB66029A0D289 /* LogSupporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339DE06D91B7356CFFD52EBAA861A140 /* LogSupporting.swift */; }; + 858882EA2331D64B44F9B0BA564ED504 /* PinpointKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = E996B09C3539B320359A35614257CA52 /* PinpointKit-dummy.m */; }; 85E713DC0E74AD5789ABFBFB0C8ADF55 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E4C26A72D50DB44D17501848286B727 /* Foundation.framework */; }; - 872818480C2EBD2FE0444CEDB1E0605F /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2E5F3C4E3988466B06DE2ECC89A12A /* LogViewer.swift */; }; - 8AEC98452DC1DE58309DFAEE9729F6BF /* BlurAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74167839F1DA5F75BDB902FB75AC6465 /* BlurAnnotationView.swift */; }; - 8E919986C605A84F0DB2FCB51F062C30 /* UIGestureRecognizer+FailRecognizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CFA4FDE1391F177CDEC574E5BBCE13 /* UIGestureRecognizer+FailRecognizing.swift */; }; - 9782FC9D528C610C841C54F68455F030 /* SuccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3580A274479536C33582931C0A231361 /* SuccessType.swift */; }; - 9857D52274049A04FAD33053689A4BDD /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910BE0D80B5F8D8DB848D9296CD35176 /* Screen.swift */; }; - 9BDBE9A489AFFA4F071C8E02A7F13CE1 /* FeedbackNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D1CAFDC8EA2D8703C0C6EE29E4BF08 /* FeedbackNavigationController.swift */; }; - 9BFE99527CF4FE4BF638F6044F3B0DB3 /* PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DD9AB124851614BB9239394F867C46 /* PinpointKit.swift */; }; - 9F6AAD057331F5AEAE1F170AECB69C56 /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D762474867E802650DD20E294750CF9 /* Annotations.swift */; }; - A5FBEA80715422B20BC2AF1CA21594A3 /* PinpointKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 2948F1E43A43DE0C783928765540F5DF /* PinpointKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8CC3CF0FCD630DB4FBA68C3DA4EDFDCD /* FeedbackCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EBD51700C2CE870C1CDFD1C82CF106 /* FeedbackCollector.swift */; }; + 8EA08765C0995E7A1E9B3513F3D71820 /* UIGestureRecognizer+FailRecognizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F60590139275BA7FDE230585AA870F /* UIGestureRecognizer+FailRecognizing.swift */; }; + 94CE6C5ED08777CB69F067482A22B598 /* ArrowAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740C9DA21A7E5AF6D4F2E1C4A4DC8527 /* ArrowAnnotationView.swift */; }; + 96874EA235926B466B2EBDB1B9AD2D8E /* FeedbackTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8D568E1AB3CA14CF248C022E4C9380 /* FeedbackTableViewDataSource.swift */; }; + A419CB8B58CA5422CDDDF84A777CC414 /* SuccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A7BF3700A7739FE2CCEDE8FB88D4D2 /* SuccessType.swift */; }; + A5FBEA80715422B20BC2AF1CA21594A3 /* PinpointKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AFE00EBD8BCE3AA346BD4F6F3E8C2C13 /* PinpointKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; A63D23875605C10678D08C9577880F57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3B899A0B12DE8854651E7B38551B012 /* MessageUI.framework */; }; - AB8F2EB835D432C2F9DBFC4E402038AF /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DD99DDF3EA6B4E7B6606E5B716452CE /* NavigationController.swift */; }; - AFB573692BE44C46B75A3C57D100A0F7 /* UIView+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0AAEEDEF23C45182AFF15A9E5ECBA0 /* UIView+PinpointKit.swift */; }; - B538318E434FFEEEFAC4AE570C4F5157 /* ShakeDetectingWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A966E0479BC26C2DBC553D152F08FB /* ShakeDetectingWindowDelegate.swift */; }; - B787C05963E315CC3D74E20E665C4F86 /* ScreenshotHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90A81D84172E10610AA4F014ABEFFF73 /* ScreenshotHeaderView.swift */; }; - B78F978BAA1DEFDC6E4D1002E03888D3 /* SourceSansPro-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BCEF62B55C3639F9F95A136036C883BE /* SourceSansPro-Semibold.ttf */; }; - D03FB493CB2EAD818C688224A7A70A7E /* SystemLogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB27D31AFF5B7DC180332A972560C729 /* SystemLogCollector.swift */; }; - DB67FA1BF9DAB1F7907B940E6375A3E5 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CCD074A9C9A1C3876CFAD3453DC351E /* Fonts.swift */; }; - E01B8997953630B3F39F71ACF79174A0 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4307288F72B971AF1C34C59718AF3B32 /* Configuration.swift */; }; + A6F5B257EA88D86A136A357617839D9D /* PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FB964BCCF7FDA0943A78A883E48984 /* PinpointKit.swift */; }; + AD5C0DFF2253816FB97C2E54D0C7FE2F /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D7BE871E65DAEF3917366E3F63F742 /* Fonts.swift */; }; + B0A13E1E4BAA4C7AEA33B4452344813C /* BasicLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D188B930C5F60B26592694AC9CFE174 /* BasicLogViewController.swift */; }; + B14ED341D5471E37E2814DD9F6CA9DA9 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F071490D7B28667AD6C35767DEA939 /* Feedback.swift */; }; + B1F7756DADFA236E5341C630169DB67A /* MailSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = E028E9FC3228C37744800190E638AE92 /* MailSender.swift */; }; + B5A218F5EB073167F5B66F344EF26BD7 /* AnnotationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34946ECD573272B07976767F4BD6E58B /* AnnotationViewFactory.swift */; }; + B78F978BAA1DEFDC6E4D1002E03888D3 /* SourceSansPro-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 8663664137D686BEAD8C9641EF7D26CF /* SourceSansPro-Semibold.ttf */; }; + B7DD92F9D3BD90651C43A675DBBC5419 /* FeedbackConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E4A2975E586C334A9365A231E17A7AD /* FeedbackConfiguration.swift */; }; + BCF25D3FAE000A7193231438C588546B /* EditorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4719CE9D422494280E1434908D4CC0 /* EditorDelegate.swift */; }; + BCF9EA281BEC7B3FE9F1F43EAA31820A /* InterfaceCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C252D8E2834F1FC1EC3EB006CEBDE975 /* InterfaceCustomization.swift */; }; + BF69B44F6DBF3FF0BA851C1E635DCE57 /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74733EAC28DD48B709C5963A9B9F8577 /* BarButtonItem.swift */; }; + C5FD0C29202ED7DC5BB53BAE37C26A3F /* UIView+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EA757F410F3732E8105526E28AE532 /* UIView+PinpointKit.swift */; }; + C7C4732930223A1DF3457D9B0BAD077A /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAA09D245C80378D775E7FE43737268 /* LogViewer.swift */; }; + C891F3B20C8E59859853EAC529251DBC /* ShakeDetectingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEC166FCAFFA6B1DA0D318A7A014446 /* ShakeDetectingWindow.swift */; }; + DFD170AFCD89F6FF8AA2E52D44DA5710 /* ASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = C7B546A2FA2F9B1450A085CFDDC72312 /* ASLLogger.m */; }; E2E6AFD963BB95D2F691806166B1CD20 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E4C26A72D50DB44D17501848286B727 /* Foundation.framework */; }; E56F9677A0979CF7F796282F9DA8CD80 /* Pods-PinpointKitExample-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = C29D51E4E8E93994310ADCA633D301E6 /* Pods-PinpointKitExample-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - ED09CE979FEBAC220464A3F100439358 /* BoxAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A746927132FD9B70272F614430C08EA6 /* BoxAnnotationView.swift */; }; - F1CA6DC9BE86933034C1EAA1D145A912 /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7530D7F9FCF262CAA398E4D766640E9 /* BarButtonItem.swift */; }; - F7CCF7FDB5685C48262EFAD1269EE8AE /* LogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB47DED22D3177F5032FBA88AD80A371 /* LogCollector.swift */; }; - FAB3FF0CDBE2C051ED2FEAA751F6E6AD /* TextAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24615EAB046AC39BE4981F92924EB0FF /* TextAnnotationView.swift */; }; + E8900FBFF2DDCD37FBFF0BA08F4F8838 /* PinpointKit+ShakePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8356019D3A019CF6A2FC89F97711E8 /* PinpointKit+ShakePresentation.swift */; }; + E9F4F8519601D20298330AFFEC576314 /* FeedbackNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896C98713734AD0DBD6CB9179E7F5DE1 /* FeedbackNavigationController.swift */; }; + EA83085059B5DEA148FD04E47AF3AE75 /* BezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AA5DBAD3C0B55B9756364F63A167F7 /* BezierPath.swift */; }; + EAF80411FD205A30714824F17DABF24D /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81620506367E552B680725E9437C9E51 /* Sender.swift */; }; + F7488AB5559C5A0E64A3819B5B831D42 /* ScreenshotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD59DB89487E549506D4AB9BFE9C2E6 /* ScreenshotCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -84,81 +85,82 @@ /* Begin PBXFileReference section */ 0148F29BAA8B901AF499610F69E21EB0 /* PinpointKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PinpointKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0D2FD3F625B717DC2FCD4D8F304E84A8 /* FeedbackTableViewDataSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackTableViewDataSource.swift; sourceTree = ""; }; - 0FA3A013B33F2A51586C9688B165DF1E /* PinpointKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PinpointKit-umbrella.h"; sourceTree = ""; }; - 114AC180CB4E848C4C81449DAFA9C175 /* SourceSansPro-Bold.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Bold.ttf"; sourceTree = ""; }; - 12221873AA420451874E0329C9CF086A /* KeyboardAvoider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KeyboardAvoider.swift; sourceTree = ""; }; - 1673F23E10FFE664DC2DBCD7EECB763E /* ScreenshotDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; - 16C7534B3413DA346F0A193C76B2F0DA /* EditImageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditImageViewController.swift; sourceTree = ""; }; - 172CDF3DB78C9F30D084A77D31F1F4E3 /* Sender.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; - 1AA8B2FF3CF646963D04752E114AA1C3 /* ASLLogger.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = ASLLogger.h; sourceTree = ""; }; + 05A7BF3700A7739FE2CCEDE8FB88D4D2 /* SuccessType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SuccessType.swift; sourceTree = ""; }; + 0BE2A7251C04383DC54945C6085E87F0 /* Tool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = ""; }; + 16EE28DEF733CC5A744DDB44866D5A0F /* PinpointKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PinpointKit-prefix.pch"; sourceTree = ""; }; + 17EBD51700C2CE870C1CDFD1C82CF106 /* FeedbackCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackCollector.swift; sourceTree = ""; }; + 18962FC1A0DE4E3F807BE051554B5ACA /* NavigationController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 1B5B6A4B3BF2B3CF780CDC82754080EF /* Pods-PinpointKitExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-PinpointKitExample.release.xcconfig"; sourceTree = ""; }; - 2098013DDAC70242CAB55164809DF297 /* MIMEType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MIMEType.swift; sourceTree = ""; }; - 24615EAB046AC39BE4981F92924EB0FF /* TextAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TextAnnotationView.swift; sourceTree = ""; }; - 283F4DBE0CD6CB330F0E6EAF93B49577 /* ASLLogger.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = ASLLogger.m; sourceTree = ""; }; - 2948F1E43A43DE0C783928765540F5DF /* PinpointKit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = PinpointKit.h; sourceTree = ""; }; - 2E7572A7CB5048145FA44393800859C5 /* LogSupporting.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogSupporting.swift; sourceTree = ""; }; - 3580A274479536C33582931C0A231361 /* SuccessType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SuccessType.swift; sourceTree = ""; }; - 3AF939EA575E5EEA924172E930A87A9B /* PinpointKit.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = PinpointKit.xcassets; sourceTree = ""; }; + 1E4719CE9D422494280E1434908D4CC0 /* EditorDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditorDelegate.swift; sourceTree = ""; }; + 21CCE8624D671A64E58406872434CE53 /* BoxAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BoxAnnotationView.swift; sourceTree = ""; }; + 22B04D415DC6B7BCECCA3E82A9811CEB /* Annotations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Annotations.swift; sourceTree = ""; }; + 2615B82A48EAC611A3313F89FD1A891F /* MIMEType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MIMEType.swift; sourceTree = ""; }; + 275A336229C4B1E788BF9857C4C390CD /* CheckmarkCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = ""; }; + 2BE276403499131D008CB6BA709475D0 /* UIColor+Palette.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+Palette.swift"; sourceTree = ""; }; + 2E17E108AEFE438BC057AC55DC2096EB /* BlurAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BlurAnnotationView.swift; sourceTree = ""; }; + 339DE06D91B7356CFFD52EBAA861A140 /* LogSupporting.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogSupporting.swift; sourceTree = ""; }; + 34946ECD573272B07976767F4BD6E58B /* AnnotationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationViewFactory.swift; sourceTree = ""; }; + 35EA757F410F3732E8105526E28AE532 /* UIView+PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIView+PinpointKit.swift"; sourceTree = ""; }; + 39AA5DBAD3C0B55B9756364F63A167F7 /* BezierPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BezierPath.swift; sourceTree = ""; }; 3B86491F230EF531B54484A2818BDAAE /* Pods-PinpointKitExample-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-PinpointKitExample-resources.sh"; sourceTree = ""; }; - 3CBBCFF091CD4792072990AF081188C4 /* MailSender.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailSender.swift; sourceTree = ""; }; - 3D762474867E802650DD20E294750CF9 /* Annotations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Annotations.swift; sourceTree = ""; }; - 421EAA998DAEFE3DD043FD4FDC40CA43 /* FeedbackCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackCollector.swift; sourceTree = ""; }; - 4307288F72B971AF1C34C59718AF3B32 /* Configuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - 46808D8DD82EBD9B48A30EB1E7344CF1 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 46B1CD047C1B63EF67F23CAC1806889A /* EditorDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditorDelegate.swift; sourceTree = ""; }; - 4F026C9460FBD43D5F04EF65D2FB5362 /* ArrowAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ArrowAnnotationView.swift; sourceTree = ""; }; - 6207788D80D5F34F8E7DB00DE24A8F3E /* PinpointKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "PinpointKit-dummy.m"; sourceTree = ""; }; - 624639227B49043215FCD240C6AC0F95 /* PinpointKit.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = PinpointKit.xcconfig; sourceTree = ""; }; + 3DD59DB89487E549506D4AB9BFE9C2E6 /* ScreenshotCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScreenshotCell.swift; sourceTree = ""; }; + 3E4A2975E586C334A9365A231E17A7AD /* FeedbackConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackConfiguration.swift; sourceTree = ""; }; + 3E5266A47E48F4745A5C6CEB10BDEAD7 /* StrokeLayoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StrokeLayoutManager.swift; sourceTree = ""; }; + 4073753FF952CDEBA8886D81069494B9 /* AnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; + 442353394785B418C9A5C78E02204798 /* EditImageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditImageViewController.swift; sourceTree = ""; }; + 46EB4256B443AF96A12DA037BBA014A4 /* Configuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + 490CCE9AF334D7DE35D176D874C0B3C4 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4CB7711F1DCD316C001FC154 /* EditImageViewControllerBarButtonItemProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditImageViewControllerBarButtonItemProviding.swift; sourceTree = ""; }; + 521052D2CB6D84A7FEBDBFFCE2A73EEF /* Screenshotter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Screenshotter.swift; sourceTree = ""; }; + 5C238D1A284842EB711EB29E9B69F299 /* Editor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Editor.swift; sourceTree = ""; }; + 5E130DAE16C32980DA74035C2A7F56DA /* ShakeDetectingWindowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindowDelegate.swift; sourceTree = ""; }; 63CB56BD873C4BB171888105C6F1B545 /* Pods-PinpointKitExample-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-PinpointKitExample-dummy.m"; sourceTree = ""; }; - 66DD9AB124851614BB9239394F867C46 /* PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PinpointKit.swift; sourceTree = ""; }; - 67D1CAFDC8EA2D8703C0C6EE29E4BF08 /* FeedbackNavigationController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackNavigationController.swift; sourceTree = ""; }; - 6945F4F6DE5BFD450D048F0081B46ACD /* Editor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Editor.swift; sourceTree = ""; }; - 6B5563D240AA97D9C97E09A47C540FE8 /* PinpointKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PinpointKit-prefix.pch"; sourceTree = ""; }; - 6CCD074A9C9A1C3876CFAD3453DC351E /* Fonts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; - 74167839F1DA5F75BDB902FB75AC6465 /* BlurAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BlurAnnotationView.swift; sourceTree = ""; }; + 654E1E03496C74FF2BFAF4E714E361C2 /* PinpointKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PinpointKit-umbrella.h"; sourceTree = ""; }; + 6A854BB38F24F744BF2349410D98FEEE /* KeyboardAvoider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KeyboardAvoider.swift; sourceTree = ""; }; + 7309EA9BDF3948F545BC8DF65395B68D /* FeedbackViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackViewController.swift; sourceTree = ""; }; + 740C9DA21A7E5AF6D4F2E1C4A4DC8527 /* ArrowAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ArrowAnnotationView.swift; sourceTree = ""; }; + 74733EAC28DD48B709C5963A9B9F8577 /* BarButtonItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; 747F01800B94931AC66C2CC8643AAB7E /* Pods-PinpointKitExample-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-PinpointKitExample-acknowledgements.plist"; sourceTree = ""; }; 74D6182912AA7B120B288E116A905177 /* Pods-PinpointKitExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-PinpointKitExample.debug.xcconfig"; sourceTree = ""; }; - 7BAE808FC0873920E02EBCD77AD9FA43 /* StrokeLayoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StrokeLayoutManager.swift; sourceTree = ""; }; - 7DD99DDF3EA6B4E7B6606E5B716452CE /* NavigationController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; + 78908646D4652689C30424B2FF2460B6 /* LogCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogCollector.swift; sourceTree = ""; }; + 7909253D244165734B9FC575D7A4A0C4 /* PinpointKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.module-map"; path = PinpointKit.modulemap; sourceTree = ""; }; + 7BDF63120ECF4737644A2B7425337597 /* PinpointKit.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = PinpointKit.xcconfig; sourceTree = ""; }; + 81620506367E552B680725E9437C9E51 /* Sender.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; + 8663664137D686BEAD8C9641EF7D26CF /* SourceSansPro-Semibold.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Semibold.ttf"; sourceTree = ""; }; + 896C98713734AD0DBD6CB9179E7F5DE1 /* FeedbackNavigationController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackNavigationController.swift; sourceTree = ""; }; 8E4C26A72D50DB44D17501848286B727 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - 90A81D84172E10610AA4F014ABEFFF73 /* ScreenshotHeaderView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScreenshotHeaderView.swift; sourceTree = ""; }; - 910BE0D80B5F8D8DB848D9296CD35176 /* Screen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; - 926EAD1738C6D99FD0C0899CF01077AD /* ShakeDetectingWindow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindow.swift; sourceTree = ""; }; 93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - A56326EAEA58486DF118DDF5FACD210D /* CheckmarkCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = ""; }; - A746927132FD9B70272F614430C08EA6 /* BoxAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BoxAnnotationView.swift; sourceTree = ""; }; + 9D188B930C5F60B26592694AC9CFE174 /* BasicLogViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasicLogViewController.swift; sourceTree = ""; }; + 9D36298B8F364B1D3BDE61F356E44F0B /* NSBundle+PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "NSBundle+PinpointKit.swift"; sourceTree = ""; }; + A2F071490D7B28667AD6C35767DEA939 /* Feedback.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Feedback.swift; sourceTree = ""; }; + AFE00EBD8BCE3AA346BD4F6F3E8C2C13 /* PinpointKit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = PinpointKit.h; sourceTree = ""; }; + B1F61D6816EA76CDC4EDF0F1337891CC /* Screen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; B3B899A0B12DE8854651E7B38551B012 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/Frameworks/MessageUI.framework; sourceTree = DEVELOPER_DIR; }; - B5C940A5E713141F02CCF9A1C624E84C /* AnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; - BB47DED22D3177F5032FBA88AD80A371 /* LogCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogCollector.swift; sourceTree = ""; }; - BCEF62B55C3639F9F95A136036C883BE /* SourceSansPro-Semibold.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Semibold.ttf"; sourceTree = ""; }; - C0CFA4FDE1391F177CDEC574E5BBCE13 /* UIGestureRecognizer+FailRecognizing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+FailRecognizing.swift"; sourceTree = ""; }; + BBE3F2E390DC6EF01039DA4F85E70A37 /* InterfaceCustomizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InterfaceCustomizable.swift; sourceTree = ""; }; + C252D8E2834F1FC1EC3EB006CEBDE975 /* InterfaceCustomization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InterfaceCustomization.swift; sourceTree = ""; }; C29D51E4E8E93994310ADCA633D301E6 /* Pods-PinpointKitExample-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-PinpointKitExample-umbrella.h"; sourceTree = ""; }; - C2A966E0479BC26C2DBC553D152F08FB /* ShakeDetectingWindowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindowDelegate.swift; sourceTree = ""; }; C4A6908DFBA7072A43C5A5B9C0AEBFAF /* Pods-PinpointKitExample.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.module-map"; path = "Pods-PinpointKitExample.modulemap"; sourceTree = ""; }; - C8B5C878C283429B54DCBEC1840E5F8B /* NSBundle+PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "NSBundle+PinpointKit.swift"; sourceTree = ""; }; - C945B11B38B63C2485E7AC2F0A4652C9 /* FeedbackViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackViewController.swift; sourceTree = ""; }; - CB27D31AFF5B7DC180332A972560C729 /* SystemLogCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SystemLogCollector.swift; sourceTree = ""; }; + C7B546A2FA2F9B1450A085CFDDC72312 /* ASLLogger.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = ASLLogger.m; sourceTree = ""; }; + CBE6EBE3EE7E185D19A3DD6CD0BAA6F9 /* ASLLogger.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = ASLLogger.h; sourceTree = ""; }; CDC3A4BF8774A7DEE2316927BCA8BA3A /* Pods-PinpointKitExample-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-PinpointKitExample-frameworks.sh"; sourceTree = ""; }; - CF521E391DB361765406F3397FD7F781 /* BezierPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BezierPath.swift; sourceTree = ""; }; - D03ADA031C084B042487773A8A86B9BB /* AnnotationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationViewFactory.swift; sourceTree = ""; }; - D1D08FD24EF24252EBB4B332CA01B193 /* Tool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = ""; }; + D14DECCAEF3C55BC06DCFAA3D6350AAF /* SourceSansPro-Bold.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Bold.ttf"; sourceTree = ""; }; + D1F60590139275BA7FDE230585AA870F /* UIGestureRecognizer+FailRecognizing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+FailRecognizing.swift"; sourceTree = ""; }; D37B3CC579D24F19F07B469C5B0CBF69 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D3978111B9A4397C306BABFDB5B20D6E /* Pods-PinpointKitExample-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-PinpointKitExample-acknowledgements.markdown"; sourceTree = ""; }; - D6CBD235F703EBBB5342501A1E5A09BF /* PinpointKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.module-map"; path = PinpointKit.modulemap; sourceTree = ""; }; - D84AA0A8E8AF047564608AB6495DFF45 /* InterfaceCustomizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InterfaceCustomizable.swift; sourceTree = ""; }; - DBCB6A9253314806490AED31CDE58272 /* UIColor+Palette.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+Palette.swift"; sourceTree = ""; }; - DEF610D46B4270498BF110D72EA7ACF8 /* Feedback.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Feedback.swift; sourceTree = ""; }; - E1CC4696A751768B9105D5565B68C3DB /* BasicLogViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasicLogViewController.swift; sourceTree = ""; }; - E2E9D9A1E5A484B9629EFB4384002E67 /* SourceSansPro-Regular.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Regular.ttf"; sourceTree = ""; }; + D3D575C61D6FED5D4E5616BF2C223E7E /* SourceSansPro-Regular.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = "SourceSansPro-Regular.ttf"; sourceTree = ""; }; + D3D7BE871E65DAEF3917366E3F63F742 /* Fonts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; + D519026547DE41B73B24649A7CB4F322 /* AnnotationsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationsView.swift; sourceTree = ""; }; + D7D1971C87043401AF64BE6C207B36EE /* PinpointKit.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = PinpointKit.xcassets; sourceTree = ""; }; + D7FB964BCCF7FDA0943A78A883E48984 /* PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PinpointKit.swift; sourceTree = ""; }; + DBBC5604F3145AF00D9544D70E3B9C53 /* TextAnnotationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TextAnnotationView.swift; sourceTree = ""; }; + DCAA09D245C80378D775E7FE43737268 /* LogViewer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = ""; }; + E028E9FC3228C37744800190E638AE92 /* MailSender.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailSender.swift; sourceTree = ""; }; E42A9844F76F9E28FEEC5A0F0037FF76 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; - E53932A79D0015680899AEDBCE408C7A /* InterfaceCustomization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InterfaceCustomization.swift; sourceTree = ""; }; - F573EE7EDBA5A16D155BC6E24C93EFF5 /* PinpointKit+ShakePresentation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "PinpointKit+ShakePresentation.swift"; sourceTree = ""; }; - F7530D7F9FCF262CAA398E4D766640E9 /* BarButtonItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; - FA0AAEEDEF23C45182AFF15A9E5ECBA0 /* UIView+PinpointKit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIView+PinpointKit.swift"; sourceTree = ""; }; - FA2E5F3C4E3988466B06DE2ECC89A12A /* LogViewer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = ""; }; - FDA61C7BF94F4ABC2B29F361860A79D0 /* Screenshotter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Screenshotter.swift; sourceTree = ""; }; - FE6113790653EAF8C1A1F7771B519259 /* AnnotationsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnnotationsView.swift; sourceTree = ""; }; + E7A77B23F366FD990B4833351E17066E /* SystemLogCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SystemLogCollector.swift; sourceTree = ""; }; + E996B09C3539B320359A35614257CA52 /* PinpointKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "PinpointKit-dummy.m"; sourceTree = ""; }; + EAEC166FCAFFA6B1DA0D318A7A014446 /* ShakeDetectingWindow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindow.swift; sourceTree = ""; }; + EE8356019D3A019CF6A2FC89F97711E8 /* PinpointKit+ShakePresentation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "PinpointKit+ShakePresentation.swift"; sourceTree = ""; }; + FE8D568E1AB3CA14CF248C022E4C9380 /* FeedbackTableViewDataSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FeedbackTableViewDataSource.swift; sourceTree = ""; }; FFC218B076577FDDB35FED8A4C0FBB33 /* Pods_PinpointKitExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PinpointKitExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -192,6 +194,15 @@ name = "Targets Support Files"; sourceTree = ""; }; + 0FD426EA9731B3E4890FDDA86A5ED708 /* Core */ = { + isa = PBXGroup; + children = ( + A72E71BA698C8549B3EFAEC7692679DD /* PinpointKit */, + B74F02B37E85AE56463849769BE2F410 /* Resources */, + ); + name = Core; + sourceTree = ""; + }; 122DA2E5084A4393C29BE363C764795C /* Frameworks */ = { isa = PBXGroup; children = ( @@ -218,120 +229,64 @@ path = "Target Support Files/Pods-PinpointKitExample"; sourceTree = ""; }; - 350C595D1CB6384FC85F0782BCB6CEAB /* PinpointKit */ = { + 418768C9986AE72757A0DAB393378874 /* PinpointKit */ = { isa = PBXGroup; children = ( - 90AAC607C4C5D4780C109ECDDD4EDF1E /* PinpointKit */, + F23DDF3AF599DC5AA8A402523809B81D /* Sources */, ); path = PinpointKit; sourceTree = ""; }; - 40E816C08C4D40850495DA097AC6AFE1 /* Sources */ = { + 43D8203C5478C514ACBCA3CB4F89AC05 /* Editing */ = { isa = PBXGroup; children = ( - 3D762474867E802650DD20E294750CF9 /* Annotations.swift */, - FE6113790653EAF8C1A1F7771B519259 /* AnnotationsView.swift */, - B5C940A5E713141F02CCF9A1C624E84C /* AnnotationView.swift */, - 4F026C9460FBD43D5F04EF65D2FB5362 /* ArrowAnnotationView.swift */, - 1AA8B2FF3CF646963D04752E114AA1C3 /* ASLLogger.h */, - 283F4DBE0CD6CB330F0E6EAF93B49577 /* ASLLogger.m */, - F7530D7F9FCF262CAA398E4D766640E9 /* BarButtonItem.swift */, - E1CC4696A751768B9105D5565B68C3DB /* BasicLogViewController.swift */, - CF521E391DB361765406F3397FD7F781 /* BezierPath.swift */, - 74167839F1DA5F75BDB902FB75AC6465 /* BlurAnnotationView.swift */, - A746927132FD9B70272F614430C08EA6 /* BoxAnnotationView.swift */, - A56326EAEA58486DF118DDF5FACD210D /* CheckmarkCell.swift */, - 4307288F72B971AF1C34C59718AF3B32 /* Configuration.swift */, - DEF610D46B4270498BF110D72EA7ACF8 /* Feedback.swift */, - 421EAA998DAEFE3DD043FD4FDC40CA43 /* FeedbackCollector.swift */, - 67D1CAFDC8EA2D8703C0C6EE29E4BF08 /* FeedbackNavigationController.swift */, - 0D2FD3F625B717DC2FCD4D8F304E84A8 /* FeedbackTableViewDataSource.swift */, - C945B11B38B63C2485E7AC2F0A4652C9 /* FeedbackViewController.swift */, - 6CCD074A9C9A1C3876CFAD3453DC351E /* Fonts.swift */, - D84AA0A8E8AF047564608AB6495DFF45 /* InterfaceCustomizable.swift */, - E53932A79D0015680899AEDBCE408C7A /* InterfaceCustomization.swift */, - 12221873AA420451874E0329C9CF086A /* KeyboardAvoider.swift */, - BB47DED22D3177F5032FBA88AD80A371 /* LogCollector.swift */, - 2E7572A7CB5048145FA44393800859C5 /* LogSupporting.swift */, - FA2E5F3C4E3988466B06DE2ECC89A12A /* LogViewer.swift */, - 3CBBCFF091CD4792072990AF081188C4 /* MailSender.swift */, - 2098013DDAC70242CAB55164809DF297 /* MIMEType.swift */, - 7DD99DDF3EA6B4E7B6606E5B716452CE /* NavigationController.swift */, - C8B5C878C283429B54DCBEC1840E5F8B /* NSBundle+PinpointKit.swift */, - 2948F1E43A43DE0C783928765540F5DF /* PinpointKit.h */, - 66DD9AB124851614BB9239394F867C46 /* PinpointKit.swift */, - F573EE7EDBA5A16D155BC6E24C93EFF5 /* PinpointKit+ShakePresentation.swift */, - 910BE0D80B5F8D8DB848D9296CD35176 /* Screen.swift */, - 1673F23E10FFE664DC2DBCD7EECB763E /* ScreenshotDetector.swift */, - 90A81D84172E10610AA4F014ABEFFF73 /* ScreenshotHeaderView.swift */, - FDA61C7BF94F4ABC2B29F361860A79D0 /* Screenshotter.swift */, - 172CDF3DB78C9F30D084A77D31F1F4E3 /* Sender.swift */, - 926EAD1738C6D99FD0C0899CF01077AD /* ShakeDetectingWindow.swift */, - C2A966E0479BC26C2DBC553D152F08FB /* ShakeDetectingWindowDelegate.swift */, - 7BAE808FC0873920E02EBCD77AD9FA43 /* StrokeLayoutManager.swift */, - 3580A274479536C33582931C0A231361 /* SuccessType.swift */, - CB27D31AFF5B7DC180332A972560C729 /* SystemLogCollector.swift */, - 24615EAB046AC39BE4981F92924EB0FF /* TextAnnotationView.swift */, - DBCB6A9253314806490AED31CDE58272 /* UIColor+Palette.swift */, - C0CFA4FDE1391F177CDEC574E5BBCE13 /* UIGestureRecognizer+FailRecognizing.swift */, - FA0AAEEDEF23C45182AFF15A9E5ECBA0 /* UIView+PinpointKit.swift */, - DA5A70B4A519AC3ADB427443F8A58640 /* Editing */, + 34946ECD573272B07976767F4BD6E58B /* AnnotationViewFactory.swift */, + 442353394785B418C9A5C78E02204798 /* EditImageViewController.swift */, + 4CB7711F1DCD316C001FC154 /* EditImageViewControllerBarButtonItemProviding.swift */, + 5C238D1A284842EB711EB29E9B69F299 /* Editor.swift */, + 1E4719CE9D422494280E1434908D4CC0 /* EditorDelegate.swift */, + 0BE2A7251C04383DC54945C6085E87F0 /* Tool.swift */, ); - path = Sources; + path = Editing; sourceTree = ""; }; - 455B60F7FF7B58E0AB73D84A2A3C5E6C /* PinpointKit */ = { + 5CA2FA14FCC337D97B9D50065E103D67 /* PinpointKit */ = { isa = PBXGroup; children = ( - D8956B217F59924AFC43DED78D48556A /* PinpointKit */, - 4E2D0381D08B0AD257EE71036DB1FE4D /* Resources */, - 7DA49D81A685252BEE243DBF3A3D98A3 /* Support Files */, + 0FD426EA9731B3E4890FDDA86A5ED708 /* Core */, + ADD9A692C48640393BFA02AE850A31C9 /* Support Files */, ); name = PinpointKit; path = ../..; sourceTree = ""; }; - 4E2D0381D08B0AD257EE71036DB1FE4D /* Resources */ = { + 66A75EC494012B34765C28B3B09197CE /* PinpointKit */ = { isa = PBXGroup; children = ( - 350C595D1CB6384FC85F0782BCB6CEAB /* PinpointKit */, + DB371D9423A80A44DB686C346847C263 /* PinpointKit */, ); - name = Resources; + path = PinpointKit; sourceTree = ""; }; - 7DA49D81A685252BEE243DBF3A3D98A3 /* Support Files */ = { + 759B37824AF2575908F4907C673B469A /* Development Pods */ = { isa = PBXGroup; children = ( - 46808D8DD82EBD9B48A30EB1E7344CF1 /* Info.plist */, - D6CBD235F703EBBB5342501A1E5A09BF /* PinpointKit.modulemap */, - 624639227B49043215FCD240C6AC0F95 /* PinpointKit.xcconfig */, - 6207788D80D5F34F8E7DB00DE24A8F3E /* PinpointKit-dummy.m */, - 6B5563D240AA97D9C97E09A47C540FE8 /* PinpointKit-prefix.pch */, - 0FA3A013B33F2A51586C9688B165DF1E /* PinpointKit-umbrella.h */, + 5CA2FA14FCC337D97B9D50065E103D67 /* PinpointKit */, ); - name = "Support Files"; - path = "Example/Pods/Target Support Files/PinpointKit"; + name = "Development Pods"; sourceTree = ""; }; 7DB346D0F39D3F0E887471402A8071AB = { isa = PBXGroup; children = ( 93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */, - 7EDB98DB12016E714DD1942B2E5F7E90 /* Development Pods */, + 759B37824AF2575908F4907C673B469A /* Development Pods */, 122DA2E5084A4393C29BE363C764795C /* Frameworks */, A4F71E61075C99A869D49E625CF20775 /* Products */, 024D40B0D6C60CF034E1715DEE0E8ABB /* Targets Support Files */, ); sourceTree = ""; }; - 7EDB98DB12016E714DD1942B2E5F7E90 /* Development Pods */ = { - isa = PBXGroup; - children = ( - 455B60F7FF7B58E0AB73D84A2A3C5E6C /* PinpointKit */, - ); - name = "Development Pods"; - sourceTree = ""; - }; 8A50A9293DF7E0690673BA8E7D29ABB4 /* iOS */ = { isa = PBXGroup; children = ( @@ -342,14 +297,6 @@ name = iOS; sourceTree = ""; }; - 90AAC607C4C5D4780C109ECDDD4EDF1E /* PinpointKit */ = { - isa = PBXGroup; - children = ( - EB6171513854CF5145AEE492F8A60E1A /* Resources */, - ); - path = PinpointKit; - sourceTree = ""; - }; A4F71E61075C99A869D49E625CF20775 /* Products */ = { isa = PBXGroup; children = ( @@ -359,45 +306,117 @@ name = Products; sourceTree = ""; }; - D1A31F74F276C56C5F09D8791089D128 /* PinpointKit */ = { + A72E71BA698C8549B3EFAEC7692679DD /* PinpointKit */ = { isa = PBXGroup; children = ( - 40E816C08C4D40850495DA097AC6AFE1 /* Sources */, + 418768C9986AE72757A0DAB393378874 /* PinpointKit */, ); path = PinpointKit; sourceTree = ""; }; - D8956B217F59924AFC43DED78D48556A /* PinpointKit */ = { + ADD9A692C48640393BFA02AE850A31C9 /* Support Files */ = { isa = PBXGroup; children = ( - D1A31F74F276C56C5F09D8791089D128 /* PinpointKit */, + 490CCE9AF334D7DE35D176D874C0B3C4 /* Info.plist */, + 7909253D244165734B9FC575D7A4A0C4 /* PinpointKit.modulemap */, + 7BDF63120ECF4737644A2B7425337597 /* PinpointKit.xcconfig */, + E996B09C3539B320359A35614257CA52 /* PinpointKit-dummy.m */, + 16EE28DEF733CC5A744DDB44866D5A0F /* PinpointKit-prefix.pch */, + 654E1E03496C74FF2BFAF4E714E361C2 /* PinpointKit-umbrella.h */, ); - path = PinpointKit; + name = "Support Files"; + path = "Example/Pods/Target Support Files/PinpointKit"; sourceTree = ""; }; - DA5A70B4A519AC3ADB427443F8A58640 /* Editing */ = { + B74F02B37E85AE56463849769BE2F410 /* Resources */ = { isa = PBXGroup; children = ( - D03ADA031C084B042487773A8A86B9BB /* AnnotationViewFactory.swift */, - 16C7534B3413DA346F0A193C76B2F0DA /* EditImageViewController.swift */, - 6945F4F6DE5BFD450D048F0081B46ACD /* Editor.swift */, - 46B1CD047C1B63EF67F23CAC1806889A /* EditorDelegate.swift */, - D1D08FD24EF24252EBB4B332CA01B193 /* Tool.swift */, + 66A75EC494012B34765C28B3B09197CE /* PinpointKit */, ); - path = Editing; + name = Resources; sourceTree = ""; }; - EB6171513854CF5145AEE492F8A60E1A /* Resources */ = { + BD1386543C63EC98EE5794860521F07E /* Resources */ = { isa = PBXGroup; children = ( - 3AF939EA575E5EEA924172E930A87A9B /* PinpointKit.xcassets */, - 114AC180CB4E848C4C81449DAFA9C175 /* SourceSansPro-Bold.ttf */, - E2E9D9A1E5A484B9629EFB4384002E67 /* SourceSansPro-Regular.ttf */, - BCEF62B55C3639F9F95A136036C883BE /* SourceSansPro-Semibold.ttf */, + D7D1971C87043401AF64BE6C207B36EE /* PinpointKit.xcassets */, + D14DECCAEF3C55BC06DCFAA3D6350AAF /* SourceSansPro-Bold.ttf */, + D3D575C61D6FED5D4E5616BF2C223E7E /* SourceSansPro-Regular.ttf */, + 8663664137D686BEAD8C9641EF7D26CF /* SourceSansPro-Semibold.ttf */, ); path = Resources; sourceTree = ""; }; + BEA3352DA4489986B9265A7118A6673E /* Core */ = { + isa = PBXGroup; + children = ( + 22B04D415DC6B7BCECCA3E82A9811CEB /* Annotations.swift */, + D519026547DE41B73B24649A7CB4F322 /* AnnotationsView.swift */, + 4073753FF952CDEBA8886D81069494B9 /* AnnotationView.swift */, + 740C9DA21A7E5AF6D4F2E1C4A4DC8527 /* ArrowAnnotationView.swift */, + CBE6EBE3EE7E185D19A3DD6CD0BAA6F9 /* ASLLogger.h */, + C7B546A2FA2F9B1450A085CFDDC72312 /* ASLLogger.m */, + 74733EAC28DD48B709C5963A9B9F8577 /* BarButtonItem.swift */, + 9D188B930C5F60B26592694AC9CFE174 /* BasicLogViewController.swift */, + 39AA5DBAD3C0B55B9756364F63A167F7 /* BezierPath.swift */, + 2E17E108AEFE438BC057AC55DC2096EB /* BlurAnnotationView.swift */, + 21CCE8624D671A64E58406872434CE53 /* BoxAnnotationView.swift */, + 275A336229C4B1E788BF9857C4C390CD /* CheckmarkCell.swift */, + 46EB4256B443AF96A12DA037BBA014A4 /* Configuration.swift */, + A2F071490D7B28667AD6C35767DEA939 /* Feedback.swift */, + 17EBD51700C2CE870C1CDFD1C82CF106 /* FeedbackCollector.swift */, + 3E4A2975E586C334A9365A231E17A7AD /* FeedbackConfiguration.swift */, + 896C98713734AD0DBD6CB9179E7F5DE1 /* FeedbackNavigationController.swift */, + FE8D568E1AB3CA14CF248C022E4C9380 /* FeedbackTableViewDataSource.swift */, + 7309EA9BDF3948F545BC8DF65395B68D /* FeedbackViewController.swift */, + D3D7BE871E65DAEF3917366E3F63F742 /* Fonts.swift */, + BBE3F2E390DC6EF01039DA4F85E70A37 /* InterfaceCustomizable.swift */, + C252D8E2834F1FC1EC3EB006CEBDE975 /* InterfaceCustomization.swift */, + 6A854BB38F24F744BF2349410D98FEEE /* KeyboardAvoider.swift */, + 78908646D4652689C30424B2FF2460B6 /* LogCollector.swift */, + 339DE06D91B7356CFFD52EBAA861A140 /* LogSupporting.swift */, + DCAA09D245C80378D775E7FE43737268 /* LogViewer.swift */, + E028E9FC3228C37744800190E638AE92 /* MailSender.swift */, + 2615B82A48EAC611A3313F89FD1A891F /* MIMEType.swift */, + 18962FC1A0DE4E3F807BE051554B5ACA /* NavigationController.swift */, + 9D36298B8F364B1D3BDE61F356E44F0B /* NSBundle+PinpointKit.swift */, + AFE00EBD8BCE3AA346BD4F6F3E8C2C13 /* PinpointKit.h */, + D7FB964BCCF7FDA0943A78A883E48984 /* PinpointKit.swift */, + EE8356019D3A019CF6A2FC89F97711E8 /* PinpointKit+ShakePresentation.swift */, + B1F61D6816EA76CDC4EDF0F1337891CC /* Screen.swift */, + 3DD59DB89487E549506D4AB9BFE9C2E6 /* ScreenshotCell.swift */, + 521052D2CB6D84A7FEBDBFFCE2A73EEF /* Screenshotter.swift */, + 81620506367E552B680725E9437C9E51 /* Sender.swift */, + EAEC166FCAFFA6B1DA0D318A7A014446 /* ShakeDetectingWindow.swift */, + 5E130DAE16C32980DA74035C2A7F56DA /* ShakeDetectingWindowDelegate.swift */, + 3E5266A47E48F4745A5C6CEB10BDEAD7 /* StrokeLayoutManager.swift */, + 05A7BF3700A7739FE2CCEDE8FB88D4D2 /* SuccessType.swift */, + E7A77B23F366FD990B4833351E17066E /* SystemLogCollector.swift */, + DBBC5604F3145AF00D9544D70E3B9C53 /* TextAnnotationView.swift */, + 2BE276403499131D008CB6BA709475D0 /* UIColor+Palette.swift */, + D1F60590139275BA7FDE230585AA870F /* UIGestureRecognizer+FailRecognizing.swift */, + 35EA757F410F3732E8105526E28AE532 /* UIView+PinpointKit.swift */, + 43D8203C5478C514ACBCA3CB4F89AC05 /* Editing */, + ); + path = Core; + sourceTree = ""; + }; + DB371D9423A80A44DB686C346847C263 /* PinpointKit */ = { + isa = PBXGroup; + children = ( + BD1386543C63EC98EE5794860521F07E /* Resources */, + ); + path = PinpointKit; + sourceTree = ""; + }; + F23DDF3AF599DC5AA8A402523809B81D /* Sources */ = { + isa = PBXGroup; + children = ( + BEA3352DA4489986B9265A7118A6673E /* Core */, + ); + path = Sources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -444,7 +463,7 @@ isa = PBXNativeTarget; buildConfigurationList = F3CFA4CBF154ACC9911121D1D8F3BDF6 /* Build configuration list for PBXNativeTarget "PinpointKit" */; buildPhases = ( - B374E8BB918E4DA51EF1F69AACFED05A /* Sources */, + A7E9E0C29D492C8C9E32AF0CA2C4A203 /* Sources */, E818C959662829F0D472F79DEEA911A4 /* Frameworks */, 4035822E4F4943D5F6E46709CB58F388 /* Headers */, AF01D16FF6022A272D582A9178030FEA /* Resources */, @@ -466,6 +485,14 @@ attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0700; + TargetAttributes = { + 2ED2D46342267A190C7F574FDD87649E = { + LastSwiftMigration = 0800; + }; + 2F1CAC6902BC17CD20FCF9781D7E23AE = { + LastSwiftMigration = 0800; + }; + }; }; buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */; compatibilityVersion = "Xcode 3.2"; @@ -508,60 +535,61 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B374E8BB918E4DA51EF1F69AACFED05A /* Sources */ = { + A7E9E0C29D492C8C9E32AF0CA2C4A203 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9F6AAD057331F5AEAE1F170AECB69C56 /* Annotations.swift in Sources */, - 1975343A340D6045F934EF805B9A15B5 /* AnnotationsView.swift in Sources */, - 6C7E948C1A6FB00FC132A51F613FE30E /* AnnotationView.swift in Sources */, - 63648CCA2D63BFB731822F2120D4E739 /* AnnotationViewFactory.swift in Sources */, - 20871F1B481AC58F430CF78B90D13DB3 /* ArrowAnnotationView.swift in Sources */, - 2C7CF2CCC87578CA7B505217184A57CB /* ASLLogger.m in Sources */, - F1CA6DC9BE86933034C1EAA1D145A912 /* BarButtonItem.swift in Sources */, - 5A57570E115B0D171402D0E96E761283 /* BasicLogViewController.swift in Sources */, - 2CFEF21D51BCD815A47B017BC02338E8 /* BezierPath.swift in Sources */, - 8AEC98452DC1DE58309DFAEE9729F6BF /* BlurAnnotationView.swift in Sources */, - ED09CE979FEBAC220464A3F100439358 /* BoxAnnotationView.swift in Sources */, - 2AEE21DAF5E1BB743C7C724F2C0580C5 /* CheckmarkCell.swift in Sources */, - E01B8997953630B3F39F71ACF79174A0 /* Configuration.swift in Sources */, - 274E0F9158A3D5EE42F4D5626D7189A1 /* EditImageViewController.swift in Sources */, - 212F3C7EDD1D6C961F9298ED94C4A9D2 /* Editor.swift in Sources */, - 38BD98D6226E3C07577B8FED5E430E95 /* EditorDelegate.swift in Sources */, - 15E941F20C1943D96E3D0936B2C3429A /* Feedback.swift in Sources */, - 69463102D52C36224BAC8D49C26BE7F1 /* FeedbackCollector.swift in Sources */, - 9BDBE9A489AFFA4F071C8E02A7F13CE1 /* FeedbackNavigationController.swift in Sources */, - 4C960F20EE40614A8AE012A37C165FF5 /* FeedbackTableViewDataSource.swift in Sources */, - 4A04FE15DA509B5C89CE556770245EC7 /* FeedbackViewController.swift in Sources */, - DB67FA1BF9DAB1F7907B940E6375A3E5 /* Fonts.swift in Sources */, - 5644F51D8190C003E02FB4E303089D9B /* InterfaceCustomizable.swift in Sources */, - 430208290E6932815B760A0E4BAAA5B9 /* InterfaceCustomization.swift in Sources */, - 461673AF60A95E83F3776A8341AC4DD8 /* KeyboardAvoider.swift in Sources */, - F7CCF7FDB5685C48262EFAD1269EE8AE /* LogCollector.swift in Sources */, - 1DBD1828CBDA3C905AE7CE05F9A8481F /* LogSupporting.swift in Sources */, - 872818480C2EBD2FE0444CEDB1E0605F /* LogViewer.swift in Sources */, - 42FE10253FD7560124D2EAD781E2E445 /* MailSender.swift in Sources */, - 39DA97B343CED7D31970E425862EDE70 /* MIMEType.swift in Sources */, - AB8F2EB835D432C2F9DBFC4E402038AF /* NavigationController.swift in Sources */, - 638502A53000777AA194EA8426E35D77 /* NSBundle+PinpointKit.swift in Sources */, - 184F4416F43088C1D8E3A507017556A4 /* PinpointKit+ShakePresentation.swift in Sources */, - 5F7A1725DDFAAF2976FBF1C8FC5B6430 /* PinpointKit-dummy.m in Sources */, - 9BFE99527CF4FE4BF638F6044F3B0DB3 /* PinpointKit.swift in Sources */, - 9857D52274049A04FAD33053689A4BDD /* Screen.swift in Sources */, - 449AFC32CE95BD390F974C89B39DE409 /* ScreenshotDetector.swift in Sources */, - B787C05963E315CC3D74E20E665C4F86 /* ScreenshotHeaderView.swift in Sources */, - 0CDD1E82AC1F536E545B685D19338E14 /* Screenshotter.swift in Sources */, - 15B8BBF0A81A8F699181A197A2149430 /* Sender.swift in Sources */, - 589E1AA30F83B11752923700A4CE76B2 /* ShakeDetectingWindow.swift in Sources */, - B538318E434FFEEEFAC4AE570C4F5157 /* ShakeDetectingWindowDelegate.swift in Sources */, - 63B94E01A6382BB6BE9CBD5A25C047D6 /* StrokeLayoutManager.swift in Sources */, - 9782FC9D528C610C841C54F68455F030 /* SuccessType.swift in Sources */, - D03FB493CB2EAD818C688224A7A70A7E /* SystemLogCollector.swift in Sources */, - FAB3FF0CDBE2C051ED2FEAA751F6E6AD /* TextAnnotationView.swift in Sources */, - 8367F623DD9416EB26F83B758471ED2C /* Tool.swift in Sources */, - 52D5A06B4C07CE372B8749304209A3BF /* UIColor+Palette.swift in Sources */, - 8E919986C605A84F0DB2FCB51F062C30 /* UIGestureRecognizer+FailRecognizing.swift in Sources */, - AFB573692BE44C46B75A3C57D100A0F7 /* UIView+PinpointKit.swift in Sources */, + 7C7E2F55BD7C12777217CCE1A2E259EA /* Annotations.swift in Sources */, + 3A526401832AC915A3D9B8950C39A2BF /* AnnotationsView.swift in Sources */, + 0E046F6AC76824707FECA1DB6F92C846 /* AnnotationView.swift in Sources */, + B5A218F5EB073167F5B66F344EF26BD7 /* AnnotationViewFactory.swift in Sources */, + 94CE6C5ED08777CB69F067482A22B598 /* ArrowAnnotationView.swift in Sources */, + DFD170AFCD89F6FF8AA2E52D44DA5710 /* ASLLogger.m in Sources */, + BF69B44F6DBF3FF0BA851C1E635DCE57 /* BarButtonItem.swift in Sources */, + B0A13E1E4BAA4C7AEA33B4452344813C /* BasicLogViewController.swift in Sources */, + EA83085059B5DEA148FD04E47AF3AE75 /* BezierPath.swift in Sources */, + 07EE007F2967839604C0C05A4BC4452B /* BlurAnnotationView.swift in Sources */, + 1F68C92F7239601360DDA69BC26CA965 /* BoxAnnotationView.swift in Sources */, + 28C43B958164BAA8EC37092362084B99 /* CheckmarkCell.swift in Sources */, + 3D438FB5923C138A78AA3CB2245BA23F /* Configuration.swift in Sources */, + 7E0ACA11E6347B7A21E04ECF670C774D /* EditImageViewController.swift in Sources */, + 48F822008D266B4351F8FF4CDBF93442 /* Editor.swift in Sources */, + BCF25D3FAE000A7193231438C588546B /* EditorDelegate.swift in Sources */, + B14ED341D5471E37E2814DD9F6CA9DA9 /* Feedback.swift in Sources */, + 8CC3CF0FCD630DB4FBA68C3DA4EDFDCD /* FeedbackCollector.swift in Sources */, + B7DD92F9D3BD90651C43A675DBBC5419 /* FeedbackConfiguration.swift in Sources */, + E9F4F8519601D20298330AFFEC576314 /* FeedbackNavigationController.swift in Sources */, + 96874EA235926B466B2EBDB1B9AD2D8E /* FeedbackTableViewDataSource.swift in Sources */, + 518C1C0194CD3DE3DEDF951FE46BEED4 /* FeedbackViewController.swift in Sources */, + AD5C0DFF2253816FB97C2E54D0C7FE2F /* Fonts.swift in Sources */, + 3C9CEEA4543560AAD89A40052B2A68A5 /* InterfaceCustomizable.swift in Sources */, + BCF9EA281BEC7B3FE9F1F43EAA31820A /* InterfaceCustomization.swift in Sources */, + 052288E94955D100411AA4FE1DEC9F11 /* KeyboardAvoider.swift in Sources */, + 0AE2BA2248C245B279E9BFF8ABB68B80 /* LogCollector.swift in Sources */, + 83473796958353E8BE8EB66029A0D289 /* LogSupporting.swift in Sources */, + C7C4732930223A1DF3457D9B0BAD077A /* LogViewer.swift in Sources */, + B1F7756DADFA236E5341C630169DB67A /* MailSender.swift in Sources */, + 1EE4B8A5864E362992D85FC3D2646630 /* MIMEType.swift in Sources */, + 090599AEA853FC31AAB0BF24D8975037 /* NavigationController.swift in Sources */, + 3DA473AE305F534F930C9A38CFC71D03 /* NSBundle+PinpointKit.swift in Sources */, + E8900FBFF2DDCD37FBFF0BA08F4F8838 /* PinpointKit+ShakePresentation.swift in Sources */, + 858882EA2331D64B44F9B0BA564ED504 /* PinpointKit-dummy.m in Sources */, + A6F5B257EA88D86A136A357617839D9D /* PinpointKit.swift in Sources */, + 578A5E810F8762F2A2505A7BC1F52370 /* Screen.swift in Sources */, + F7488AB5559C5A0E64A3819B5B831D42 /* ScreenshotCell.swift in Sources */, + 4CB771201DCD316C001FC154 /* EditImageViewControllerBarButtonItemProviding.swift in Sources */, + 5AEBDF41B7855864EB12FACA4FFCB160 /* Screenshotter.swift in Sources */, + EAF80411FD205A30714824F17DABF24D /* Sender.swift in Sources */, + C891F3B20C8E59859853EAC529251DBC /* ShakeDetectingWindow.swift in Sources */, + 6BDD572C71D7C354751FD2EADB79A9FD /* ShakeDetectingWindowDelegate.swift in Sources */, + 139EC01917C8A5E855DC504DE3106051 /* StrokeLayoutManager.swift in Sources */, + A419CB8B58CA5422CDDDF84A777CC414 /* SuccessType.swift in Sources */, + 202DB1E255DA0579773DB4DEB6CDBDE3 /* SystemLogCollector.swift in Sources */, + 4F23890ECB2FCD34611153312EB69303 /* TextAnnotationView.swift in Sources */, + 7582B792320B8C5FAF732C5824B76427 /* Tool.swift in Sources */, + 46AEAECF7531C2384192AAA51FDE6930 /* UIColor+Palette.swift in Sources */, + 8EA08765C0995E7A1E9B3513F3D71820 /* UIGestureRecognizer+FailRecognizing.swift in Sources */, + C5FD0C29202ED7DC5BB53BAE37C26A3F /* UIView+PinpointKit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -605,6 +633,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -655,7 +684,7 @@ }; 9721ED2627B7BF38ED8C5ABD2591A80E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 624639227B49043215FCD240C6AC0F95 /* PinpointKit.xcconfig */; + baseConfigurationReference = 7BDF63120ECF4737644A2B7425337597 /* PinpointKit.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; @@ -677,6 +706,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -685,7 +715,7 @@ }; AC28EBB7FAA6B962ACF36CEF02BA84E0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 624639227B49043215FCD240C6AC0F95 /* PinpointKit.xcconfig */; + baseConfigurationReference = 7BDF63120ECF4737644A2B7425337597 /* PinpointKit.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; @@ -706,6 +736,7 @@ PRODUCT_NAME = PinpointKit; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -739,6 +770,7 @@ PRODUCT_NAME = Pods_PinpointKitExample; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme b/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme index affed03..1d15045 100644 --- a/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme +++ b/Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme @@ -1,36 +1,39 @@ + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + BuildableIdentifier = "primary" + BlueprintIdentifier = "2F1CAC6902BC17CD20FCF9781D7E23AE" + BuildableName = "PinpointKit.framework" + BlueprintName = "PinpointKit" + ReferencedContainer = "container:Pods.xcodeproj"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + debugDocumentVersioning = "YES"> diff --git a/Example/Pods/Target Support Files/PinpointKit/Info.plist b/Example/Pods/Target Support Files/PinpointKit/Info.plist index b07fa9b..c94bde4 100644 --- a/Example/Pods/Target Support Files/PinpointKit/Info.plist +++ b/Example/Pods/Target Support Files/PinpointKit/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 0.9.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Example/Pods/Target Support Files/Pods-PinpointKitExample/Pods-PinpointKitExample-resources.sh b/Example/Pods/Target Support Files/Pods-PinpointKitExample/Pods-PinpointKitExample-resources.sh index e768f92..0a15615 100755 --- a/Example/Pods/Target Support Files/Pods-PinpointKitExample/Pods-PinpointKitExample-resources.sh +++ b/Example/Pods/Target Support Files/Pods-PinpointKitExample/Pods-PinpointKitExample-resources.sh @@ -48,8 +48,8 @@ EOM ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.xib) - echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT}" - ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.framework) echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" diff --git a/PinpointKit.podspec.json b/PinpointKit.podspec.json index fe6d016..492b24e 100644 --- a/PinpointKit.podspec.json +++ b/PinpointKit.podspec.json @@ -1,14 +1,14 @@ { "name": "PinpointKit", - "version": "0.9", + "version": "1.0.0", "homepage": "https://github.com/Lickability/PinpointKit", "source": { "git": "https://github.com/Lickability/PinpointKit.git", - "tag": "0.9" + "tag": "1.0.0" }, "summary": "A library that makes bug reporting simple for your users by allowing them to send feedback with annotated screenshots and logs.", "authors" : { - "Matthew Bishoff": "matt@lickability.com", + "Matthew Bischoff": "matt@lickability.com", "Brian Capps": "brian@lickability.com", "Kenneth Ackerson": "pearapps@gmail.com", "Paul Rehkugler": "paul@rehkugler.com", @@ -25,10 +25,25 @@ "requires_arc": true, "frameworks": [ "Foundation", - "UIKit", - "MessageUI" + "UIKit" ], - "source_files": "PinpointKit/PinpointKit/Sources/**/*.{h,m,swift}", - "resources": "PinpointKit/PinpointKit/Resources/*", + "subspecs": [ + { + "name": "Core", + "resources": "PinpointKit/PinpointKit/Resources/*", + "source_files": "PinpointKit/PinpointKit/Sources/Core/**/*.{h,m,swift}", + "frameworks": [ + "MessageUI" + ] + }, + { + "name": "ScreenshotDetector", + "source_files": "PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift", + "frameworks": [ + "Photos" + ] + } + ], + "default_subspec": "Core", "dependencies": { } } diff --git a/PinpointKit/PinpointKit.xcodeproj/project.pbxproj b/PinpointKit/PinpointKit.xcodeproj/project.pbxproj index 90dda9f..28f332f 100644 --- a/PinpointKit/PinpointKit.xcodeproj/project.pbxproj +++ b/PinpointKit/PinpointKit.xcodeproj/project.pbxproj @@ -7,65 +7,65 @@ objects = { /* Begin PBXBuildFile section */ - 12580A351C652FA000ADC01C /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A341C652FA000ADC01C /* Configuration.swift */; }; - 12580A371C65304800ADC01C /* LogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A361C65304800ADC01C /* LogCollector.swift */; }; - 12580A391C65307300ADC01C /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A381C65307300ADC01C /* Sender.swift */; }; - 12580A3B1C65309000ADC01C /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A3A1C65309000ADC01C /* Editor.swift */; }; - 12580A3D1C6530CC00ADC01C /* FeedbackCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A3C1C6530CC00ADC01C /* FeedbackCollector.swift */; }; - 12580A3F1C6530FB00ADC01C /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A3E1C6530FB00ADC01C /* LogViewer.swift */; }; - 12580A411C65330600ADC01C /* SystemLogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A401C65330600ADC01C /* SystemLogCollector.swift */; }; - 12580A431C65332C00ADC01C /* MailSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A421C65332C00ADC01C /* MailSender.swift */; }; - 12580A471C65339F00ADC01C /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A461C65339F00ADC01C /* FeedbackViewController.swift */; }; - 12580A491C6533DC00ADC01C /* BasicLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12580A481C6533DC00ADC01C /* BasicLogViewController.swift */; }; 2C5C41121C6560AD0033ED3D /* (null) in Sources */ = {isa = PBXBuildFile; }; 2C5C41141C6568D60033ED3D /* (null) in Sources */ = {isa = PBXBuildFile; }; - 2C9CB20A1CE61F37006567E2 /* PinpointKit+ShakePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB2091CE61F37006567E2 /* PinpointKit+ShakePresentation.swift */; }; - 2C9CB20C1CE62297006567E2 /* EditorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB20B1CE62297006567E2 /* EditorDelegate.swift */; }; - 2C9CB20E1CE622A3006567E2 /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB20D1CE622A3006567E2 /* Tool.swift */; }; - 2C9CB2101CE622BC006567E2 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB20F1CE622BC006567E2 /* NavigationController.swift */; }; - 2C9CB2121CE622CD006567E2 /* AnnotationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB2111CE622CD006567E2 /* AnnotationViewFactory.swift */; }; - 2C9CB2141CE622EB006567E2 /* UIView+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB2131CE622EB006567E2 /* UIView+PinpointKit.swift */; }; - 2C9CB2161CE65895006567E2 /* InterfaceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CB2151CE65895006567E2 /* InterfaceCustomizable.swift */; }; - 2CACDFCD1C9C83CE0002ECBF /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CACDFCC1C9C83CE0002ECBF /* CheckmarkCell.swift */; }; 2CACDFCF1C9C8AFB0002ECBF /* PinpointKit.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CACDFCE1C9C8AFB0002ECBF /* PinpointKit.xcassets */; }; - 2CB63F1B1C77B1B6000CEFC6 /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB63F1A1C77B1B6000CEFC6 /* UIColor+Palette.swift */; }; - 2CB63F1D1C77B50D000CEFC6 /* Screenshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB63F1C1C77B50D000CEFC6 /* Screenshotter.swift */; }; - 2CB63F1F1C77C38B000CEFC6 /* ScreenshotHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB63F1E1C77C38B000CEFC6 /* ScreenshotHeaderView.swift */; }; - 2CB63F211C77CBE9000CEFC6 /* FeedbackTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB63F201C77CBE9000CEFC6 /* FeedbackTableViewDataSource.swift */; }; - 2CB6C1F21C652D6400022CEB /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6C1F11C652D6400022CEB /* Feedback.swift */; }; - 2CB6C1F61C65361900022CEB /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB6C1F51C65361900022CEB /* ScreenshotDetector.swift */; }; - 2CE6D3FF1C6E5D8D00DD8189 /* MIMEType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE6D3FE1C6E5D8D00DD8189 /* MIMEType.swift */; }; - 2CE6D4011C6E5DA400DD8189 /* SuccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE6D4001C6E5DA400DD8189 /* SuccessType.swift */; }; - 2CEAF65C1C779C8D009368B8 /* FeedbackNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEAF65B1C779C8D009368B8 /* FeedbackNavigationController.swift */; }; - 2CF3B9321CAF142400C8B29B /* NSBundle+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF3B9311CAF142400C8B29B /* NSBundle+PinpointKit.swift */; }; - B929017E1C555F86007CCA5E /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B929017D1C555F86007CCA5E /* EditImageViewController.swift */; }; - B92901811C556185007CCA5E /* UIGestureRecognizer+FailRecognizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901801C556185007CCA5E /* UIGestureRecognizer+FailRecognizing.swift */; }; - B92901841C55639B007CCA5E /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901831C55639B007CCA5E /* Fonts.swift */; }; - B92901861C55648A007CCA5E /* KeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901851C55648A007CCA5E /* KeyboardAvoider.swift */; }; - B929019B1C556774007CCA5E /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901991C556774007CCA5E /* AnnotationView.swift */; }; - B929019C1C556774007CCA5E /* ArrowAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B929019A1C556774007CCA5E /* ArrowAnnotationView.swift */; }; - B929019E1C556778007CCA5E /* BlurAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B929019D1C556778007CCA5E /* BlurAnnotationView.swift */; }; - B92901A11C55677C007CCA5E /* BoxAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B929019F1C55677C007CCA5E /* BoxAnnotationView.swift */; }; - B92901A21C55677C007CCA5E /* TextAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901A01C55677C007CCA5E /* TextAnnotationView.swift */; }; - B92901A51C556791007CCA5E /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901A41C556791007CCA5E /* Annotations.swift */; }; - B92901A71C5567EC007CCA5E /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901A61C5567EC007CCA5E /* BarButtonItem.swift */; }; - B92901A91C556831007CCA5E /* AnnotationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901A81C556831007CCA5E /* AnnotationsView.swift */; }; - B92901B71C557406007CCA5E /* BezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901B41C557406007CCA5E /* BezierPath.swift */; }; - B92901B81C557406007CCA5E /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901B51C557406007CCA5E /* Screen.swift */; }; - B92901B91C557406007CCA5E /* StrokeLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92901B61C557406007CCA5E /* StrokeLayoutManager.swift */; }; - DA0DA6061C53049B0012ADBE /* PinpointKit.h in Headers */ = {isa = PBXBuildFile; fileRef = DA0DA6051C53049B0012ADBE /* PinpointKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C4037C41D9EAE9500305A6E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C01D9EAE9500305A6E /* Configuration.swift */; }; + 4C4037C51D9EAE9500305A6E /* InterfaceCustomizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C11D9EAE9500305A6E /* InterfaceCustomizable.swift */; }; + 4C4037C61D9EAE9500305A6E /* InterfaceCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C21D9EAE9500305A6E /* InterfaceCustomization.swift */; }; + 4C4037C71D9EAE9500305A6E /* PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C31D9EAE9500305A6E /* PinpointKit.swift */; }; + 4C4037CB1D9EAEBD00305A6E /* LogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C81D9EAEBD00305A6E /* LogCollector.swift */; }; + 4C4037CC1D9EAEBD00305A6E /* LogSupporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037C91D9EAEBD00305A6E /* LogSupporting.swift */; }; + 4C4037CD1D9EAEBD00305A6E /* SystemLogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037CA1D9EAEBD00305A6E /* SystemLogCollector.swift */; }; + 4C4037D01D9EAECF00305A6E /* MailSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037CE1D9EAECF00305A6E /* MailSender.swift */; }; + 4C4037D11D9EAECF00305A6E /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037CF1D9EAECF00305A6E /* Sender.swift */; }; + 4C4037D51D9EAF0D00305A6E /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037D21D9EAF0D00305A6E /* Editor.swift */; }; + 4C4037D61D9EAF0D00305A6E /* EditorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037D31D9EAF0D00305A6E /* EditorDelegate.swift */; }; + 4C4037D71D9EAF0D00305A6E /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037D41D9EAF0D00305A6E /* Tool.swift */; }; + 4C4037D91D9EAF1F00305A6E /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037D81D9EAF1F00305A6E /* EditImageViewController.swift */; }; + 4C4037DB1D9EAF3400305A6E /* AnnotationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037DA1D9EAF3400305A6E /* AnnotationViewFactory.swift */; }; + 4C4037DD1D9EAF4E00305A6E /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037DC1D9EAF4E00305A6E /* Annotations.swift */; }; + 4C4037E41D9EAFA000305A6E /* AnnotationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037DE1D9EAFA000305A6E /* AnnotationsView.swift */; }; + 4C4037E51D9EAFA000305A6E /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037DF1D9EAFA000305A6E /* AnnotationView.swift */; }; + 4C4037E61D9EAFA000305A6E /* ArrowAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037E01D9EAFA000305A6E /* ArrowAnnotationView.swift */; }; + 4C4037E71D9EAFA000305A6E /* BlurAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037E11D9EAFA000305A6E /* BlurAnnotationView.swift */; }; + 4C4037E81D9EAFA000305A6E /* BoxAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037E21D9EAFA000305A6E /* BoxAnnotationView.swift */; }; + 4C4037E91D9EAFA000305A6E /* TextAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037E31D9EAFA000305A6E /* TextAnnotationView.swift */; }; + 4C4037EB1D9EAFC700305A6E /* UIGestureRecognizer+FailRecognizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037EA1D9EAFC700305A6E /* UIGestureRecognizer+FailRecognizing.swift */; }; + 4C4037F21D9EB01800305A6E /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037EC1D9EB01800305A6E /* Feedback.swift */; }; + 4C4037F31D9EB01800305A6E /* FeedbackCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037ED1D9EB01800305A6E /* FeedbackCollector.swift */; }; + 4C4037F41D9EB01800305A6E /* FeedbackConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037EE1D9EB01800305A6E /* FeedbackConfiguration.swift */; }; + 4C4037F51D9EB01800305A6E /* FeedbackNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037EF1D9EB01800305A6E /* FeedbackNavigationController.swift */; }; + 4C4037F61D9EB01800305A6E /* FeedbackTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037F01D9EB01800305A6E /* FeedbackTableViewDataSource.swift */; }; + 4C4037F71D9EB01800305A6E /* FeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037F11D9EB01800305A6E /* FeedbackViewController.swift */; }; + 4C4037FA1D9EB02300305A6E /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037F81D9EB02300305A6E /* CheckmarkCell.swift */; }; + 4C4037FB1D9EB02300305A6E /* ScreenshotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037F91D9EB02300305A6E /* ScreenshotCell.swift */; }; + 4C4037FD1D9EB04400305A6E /* Screenshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037FC1D9EB04400305A6E /* Screenshotter.swift */; }; + 4C4037FF1D9EB06900305A6E /* BasicLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4037FE1D9EB06900305A6E /* BasicLogViewController.swift */; }; + 4C4038011D9EB07000305A6E /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038001D9EB07000305A6E /* LogViewer.swift */; }; + 4C4038051D9EB08E00305A6E /* PinpointKit+ShakePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038021D9EB08E00305A6E /* PinpointKit+ShakePresentation.swift */; }; + 4C4038061D9EB08E00305A6E /* ShakeDetectingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038031D9EB08E00305A6E /* ShakeDetectingWindow.swift */; }; + 4C4038071D9EB08E00305A6E /* ShakeDetectingWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038041D9EB08E00305A6E /* ShakeDetectingWindowDelegate.swift */; }; + 4C4038091D9EB0CB00305A6E /* UIView+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038081D9EB0CB00305A6E /* UIView+PinpointKit.swift */; }; + 4C40380B1D9EB0D600305A6E /* NSBundle+PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40380A1D9EB0D600305A6E /* NSBundle+PinpointKit.swift */; }; + 4C40380D1D9EB0DD00305A6E /* MIMEType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40380C1D9EB0DD00305A6E /* MIMEType.swift */; }; + 4C40380F1D9EB0E400305A6E /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40380E1D9EB0E400305A6E /* Fonts.swift */; }; + 4C4038111D9EB0EE00305A6E /* KeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038101D9EB0EE00305A6E /* KeyboardAvoider.swift */; }; + 4C4038131D9EB0FB00305A6E /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038121D9EB0FB00305A6E /* BarButtonItem.swift */; }; + 4C4038151D9EB10500305A6E /* BezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038141D9EB10500305A6E /* BezierPath.swift */; }; + 4C4038171D9EB10C00305A6E /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038161D9EB10C00305A6E /* Screen.swift */; }; + 4C4038191D9EB11500305A6E /* StrokeLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038181D9EB11500305A6E /* StrokeLayoutManager.swift */; }; + 4C40381B1D9EB11E00305A6E /* SuccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40381A1D9EB11E00305A6E /* SuccessType.swift */; }; + 4C40381D1D9EB12900305A6E /* UIColor+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40381C1D9EB12900305A6E /* UIColor+Palette.swift */; }; + 4C4038201D9EB14600305A6E /* ASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C40381E1D9EB14600305A6E /* ASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C4038211D9EB14600305A6E /* ASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C40381F1D9EB14600305A6E /* ASLLogger.m */; }; + 4C4038261D9EB1B700305A6E /* PinpointKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C4038251D9EB1B700305A6E /* PinpointKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C4038281D9EB27A00305A6E /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4038271D9EB27A00305A6E /* NavigationController.swift */; }; + 4C40382C1D9EB7A800305A6E /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C40382B1D9EB7A800305A6E /* ScreenshotDetector.swift */; }; + 4CFB58801E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFB587F1E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift */; }; DA0DA60D1C53049B0012ADBE /* PinpointKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0DA6021C53049B0012ADBE /* PinpointKit.framework */; }; - DA0DA6121C53049B0012ADBE /* PinpointKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0DA6111C53049B0012ADBE /* PinpointKitTests.swift */; }; - DA0DA61D1C5305C00012ADBE /* PinpointKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0DA61C1C5305C00012ADBE /* PinpointKit.swift */; }; - DA3B16661C9CD2DE00DBFDE6 /* ShakeDetectingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3B16651C9CD2DE00DBFDE6 /* ShakeDetectingWindow.swift */; }; - DA7B94F01CC28DE300242E49 /* ShakeDetectingWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7B94EF1CC28DE300242E49 /* ShakeDetectingWindowDelegate.swift */; }; - F2ACE95F1CE6532000A461E5 /* LogSupporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ACE95E1CE6532000A461E5 /* LogSupporting.swift */; }; - F2ACE9611CE6539200A461E5 /* InterfaceCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ACE9601CE6539200A461E5 /* InterfaceCustomization.swift */; }; - F2ACE9E11CEF9E2100A461E5 /* SourceSansPro-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F2ACE9DE1CEF9E2100A461E5 /* SourceSansPro-Bold.ttf */; }; - F2ACE9E21CEF9E2100A461E5 /* SourceSansPro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F2ACE9DF1CEF9E2100A461E5 /* SourceSansPro-Regular.ttf */; }; - F2ACE9E31CEF9E2100A461E5 /* SourceSansPro-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F2ACE9E01CEF9E2100A461E5 /* SourceSansPro-Semibold.ttf */; }; - F2BF24051CB9A69200867E01 /* ASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = F2BF24031CB9A69200867E01 /* ASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F2BF24061CB9A69200867E01 /* ASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = F2BF24041CB9A69200867E01 /* ASLLogger.m */; }; + F24381121D54CBAB004CC87F /* SystemLogCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */; }; + F24381151D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -79,67 +79,69 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 12580A341C652FA000ADC01C /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - 12580A361C65304800ADC01C /* LogCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogCollector.swift; sourceTree = ""; }; - 12580A381C65307300ADC01C /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; - 12580A3A1C65309000ADC01C /* Editor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Editor.swift; path = Editing/Editor.swift; sourceTree = ""; }; - 12580A3C1C6530CC00ADC01C /* FeedbackCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackCollector.swift; sourceTree = ""; }; - 12580A3E1C6530FB00ADC01C /* LogViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = ""; }; - 12580A401C65330600ADC01C /* SystemLogCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemLogCollector.swift; sourceTree = ""; }; - 12580A421C65332C00ADC01C /* MailSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailSender.swift; sourceTree = ""; }; - 12580A461C65339F00ADC01C /* FeedbackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackViewController.swift; sourceTree = ""; }; - 12580A481C6533DC00ADC01C /* BasicLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicLogViewController.swift; sourceTree = ""; }; - 2C9CB2091CE61F37006567E2 /* PinpointKit+ShakePresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PinpointKit+ShakePresentation.swift"; sourceTree = ""; }; - 2C9CB20B1CE62297006567E2 /* EditorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditorDelegate.swift; path = Editing/EditorDelegate.swift; sourceTree = ""; }; - 2C9CB20D1CE622A3006567E2 /* Tool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tool.swift; path = Editing/Tool.swift; sourceTree = ""; }; - 2C9CB20F1CE622BC006567E2 /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; - 2C9CB2111CE622CD006567E2 /* AnnotationViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnnotationViewFactory.swift; path = Editing/AnnotationViewFactory.swift; sourceTree = ""; }; - 2C9CB2131CE622EB006567E2 /* UIView+PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+PinpointKit.swift"; sourceTree = ""; }; - 2C9CB2151CE65895006567E2 /* InterfaceCustomizable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterfaceCustomizable.swift; sourceTree = ""; }; - 2CACDFCC1C9C83CE0002ECBF /* CheckmarkCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = ""; }; 2CACDFCE1C9C8AFB0002ECBF /* PinpointKit.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = PinpointKit.xcassets; path = ../Resources/PinpointKit.xcassets; sourceTree = ""; }; - 2CB63F1A1C77B1B6000CEFC6 /* UIColor+Palette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Palette.swift"; sourceTree = ""; }; - 2CB63F1C1C77B50D000CEFC6 /* Screenshotter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screenshotter.swift; sourceTree = ""; }; - 2CB63F1E1C77C38B000CEFC6 /* ScreenshotHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenshotHeaderView.swift; sourceTree = ""; }; - 2CB63F201C77CBE9000CEFC6 /* FeedbackTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackTableViewDataSource.swift; sourceTree = ""; }; - 2CB6C1F11C652D6400022CEB /* Feedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feedback.swift; sourceTree = ""; }; - 2CB6C1F51C65361900022CEB /* ScreenshotDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; - 2CE6D3FE1C6E5D8D00DD8189 /* MIMEType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MIMEType.swift; sourceTree = ""; }; - 2CE6D4001C6E5DA400DD8189 /* SuccessType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuccessType.swift; sourceTree = ""; }; - 2CEAF65B1C779C8D009368B8 /* FeedbackNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackNavigationController.swift; sourceTree = ""; }; - 2CF3B9311CAF142400C8B29B /* NSBundle+PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSBundle+PinpointKit.swift"; sourceTree = ""; }; - B929017D1C555F86007CCA5E /* EditImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditImageViewController.swift; path = Editing/EditImageViewController.swift; sourceTree = ""; }; - B92901801C556185007CCA5E /* UIGestureRecognizer+FailRecognizing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+FailRecognizing.swift"; sourceTree = ""; }; - B92901831C55639B007CCA5E /* Fonts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; - B92901851C55648A007CCA5E /* KeyboardAvoider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAvoider.swift; sourceTree = ""; }; - B92901991C556774007CCA5E /* AnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; - B929019A1C556774007CCA5E /* ArrowAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrowAnnotationView.swift; sourceTree = ""; }; - B929019D1C556778007CCA5E /* BlurAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurAnnotationView.swift; sourceTree = ""; }; - B929019F1C55677C007CCA5E /* BoxAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxAnnotationView.swift; sourceTree = ""; }; - B92901A01C55677C007CCA5E /* TextAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAnnotationView.swift; sourceTree = ""; }; - B92901A41C556791007CCA5E /* Annotations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Annotations.swift; sourceTree = ""; }; - B92901A61C5567EC007CCA5E /* BarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; - B92901A81C556831007CCA5E /* AnnotationsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationsView.swift; sourceTree = ""; }; - B92901B21C557401007CCA5E /* AssetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetViewModel.swift; sourceTree = ""; }; - B92901B41C557406007CCA5E /* BezierPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPath.swift; sourceTree = ""; }; - B92901B51C557406007CCA5E /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; - B92901B61C557406007CCA5E /* StrokeLayoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeLayoutManager.swift; sourceTree = ""; }; + 4C4037C01D9EAE9500305A6E /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Configuration.swift; path = Core/Configuration.swift; sourceTree = ""; }; + 4C4037C11D9EAE9500305A6E /* InterfaceCustomizable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InterfaceCustomizable.swift; path = Core/InterfaceCustomizable.swift; sourceTree = ""; }; + 4C4037C21D9EAE9500305A6E /* InterfaceCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InterfaceCustomization.swift; path = Core/InterfaceCustomization.swift; sourceTree = ""; }; + 4C4037C31D9EAE9500305A6E /* PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PinpointKit.swift; path = Core/PinpointKit.swift; sourceTree = ""; }; + 4C4037C81D9EAEBD00305A6E /* LogCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LogCollector.swift; path = Core/LogCollector.swift; sourceTree = ""; }; + 4C4037C91D9EAEBD00305A6E /* LogSupporting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LogSupporting.swift; path = Core/LogSupporting.swift; sourceTree = ""; }; + 4C4037CA1D9EAEBD00305A6E /* SystemLogCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SystemLogCollector.swift; path = Core/SystemLogCollector.swift; sourceTree = ""; }; + 4C4037CE1D9EAECF00305A6E /* MailSender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MailSender.swift; path = Core/MailSender.swift; sourceTree = ""; }; + 4C4037CF1D9EAECF00305A6E /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Sender.swift; path = Core/Sender.swift; sourceTree = ""; }; + 4C4037D21D9EAF0D00305A6E /* Editor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Editor.swift; path = Core/Editing/Editor.swift; sourceTree = ""; }; + 4C4037D31D9EAF0D00305A6E /* EditorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditorDelegate.swift; path = Core/Editing/EditorDelegate.swift; sourceTree = ""; }; + 4C4037D41D9EAF0D00305A6E /* Tool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tool.swift; path = Core/Editing/Tool.swift; sourceTree = ""; }; + 4C4037D81D9EAF1F00305A6E /* EditImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditImageViewController.swift; path = Core/Editing/EditImageViewController.swift; sourceTree = ""; }; + 4C4037DA1D9EAF3400305A6E /* AnnotationViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnnotationViewFactory.swift; path = Core/Editing/AnnotationViewFactory.swift; sourceTree = ""; }; + 4C4037DC1D9EAF4E00305A6E /* Annotations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Annotations.swift; path = Core/Annotations.swift; sourceTree = ""; }; + 4C4037DE1D9EAFA000305A6E /* AnnotationsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnnotationsView.swift; path = Core/AnnotationsView.swift; sourceTree = ""; }; + 4C4037DF1D9EAFA000305A6E /* AnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnnotationView.swift; path = Core/AnnotationView.swift; sourceTree = ""; }; + 4C4037E01D9EAFA000305A6E /* ArrowAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ArrowAnnotationView.swift; path = Core/ArrowAnnotationView.swift; sourceTree = ""; }; + 4C4037E11D9EAFA000305A6E /* BlurAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlurAnnotationView.swift; path = Core/BlurAnnotationView.swift; sourceTree = ""; }; + 4C4037E21D9EAFA000305A6E /* BoxAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BoxAnnotationView.swift; path = Core/BoxAnnotationView.swift; sourceTree = ""; }; + 4C4037E31D9EAFA000305A6E /* TextAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextAnnotationView.swift; path = Core/TextAnnotationView.swift; sourceTree = ""; }; + 4C4037EA1D9EAFC700305A6E /* UIGestureRecognizer+FailRecognizing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIGestureRecognizer+FailRecognizing.swift"; path = "Core/UIGestureRecognizer+FailRecognizing.swift"; sourceTree = ""; }; + 4C4037EC1D9EB01800305A6E /* Feedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Feedback.swift; path = Core/Feedback.swift; sourceTree = ""; }; + 4C4037ED1D9EB01800305A6E /* FeedbackCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackCollector.swift; path = Core/FeedbackCollector.swift; sourceTree = ""; }; + 4C4037EE1D9EB01800305A6E /* FeedbackConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackConfiguration.swift; path = Core/FeedbackConfiguration.swift; sourceTree = ""; }; + 4C4037EF1D9EB01800305A6E /* FeedbackNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackNavigationController.swift; path = Core/FeedbackNavigationController.swift; sourceTree = ""; }; + 4C4037F01D9EB01800305A6E /* FeedbackTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackTableViewDataSource.swift; path = Core/FeedbackTableViewDataSource.swift; sourceTree = ""; }; + 4C4037F11D9EB01800305A6E /* FeedbackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedbackViewController.swift; path = Core/FeedbackViewController.swift; sourceTree = ""; }; + 4C4037F81D9EB02300305A6E /* CheckmarkCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheckmarkCell.swift; path = Core/CheckmarkCell.swift; sourceTree = ""; }; + 4C4037F91D9EB02300305A6E /* ScreenshotCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenshotCell.swift; path = Core/ScreenshotCell.swift; sourceTree = ""; }; + 4C4037FC1D9EB04400305A6E /* Screenshotter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Screenshotter.swift; path = Core/Screenshotter.swift; sourceTree = ""; }; + 4C4037FE1D9EB06900305A6E /* BasicLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BasicLogViewController.swift; path = Core/BasicLogViewController.swift; sourceTree = ""; }; + 4C4038001D9EB07000305A6E /* LogViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LogViewer.swift; path = Core/LogViewer.swift; sourceTree = ""; }; + 4C4038021D9EB08E00305A6E /* PinpointKit+ShakePresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "PinpointKit+ShakePresentation.swift"; path = "Core/PinpointKit+ShakePresentation.swift"; sourceTree = ""; }; + 4C4038031D9EB08E00305A6E /* ShakeDetectingWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShakeDetectingWindow.swift; path = Core/ShakeDetectingWindow.swift; sourceTree = ""; }; + 4C4038041D9EB08E00305A6E /* ShakeDetectingWindowDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShakeDetectingWindowDelegate.swift; path = Core/ShakeDetectingWindowDelegate.swift; sourceTree = ""; }; + 4C4038081D9EB0CB00305A6E /* UIView+PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+PinpointKit.swift"; path = "Core/UIView+PinpointKit.swift"; sourceTree = ""; }; + 4C40380A1D9EB0D600305A6E /* NSBundle+PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "NSBundle+PinpointKit.swift"; path = "Core/NSBundle+PinpointKit.swift"; sourceTree = ""; }; + 4C40380C1D9EB0DD00305A6E /* MIMEType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MIMEType.swift; path = Core/MIMEType.swift; sourceTree = ""; }; + 4C40380E1D9EB0E400305A6E /* Fonts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Fonts.swift; path = Core/Fonts.swift; sourceTree = ""; }; + 4C4038101D9EB0EE00305A6E /* KeyboardAvoider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = KeyboardAvoider.swift; path = Core/KeyboardAvoider.swift; sourceTree = ""; }; + 4C4038121D9EB0FB00305A6E /* BarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BarButtonItem.swift; path = Core/BarButtonItem.swift; sourceTree = ""; }; + 4C4038141D9EB10500305A6E /* BezierPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BezierPath.swift; path = Core/BezierPath.swift; sourceTree = ""; }; + 4C4038161D9EB10C00305A6E /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Screen.swift; path = Core/Screen.swift; sourceTree = ""; }; + 4C4038181D9EB11500305A6E /* StrokeLayoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StrokeLayoutManager.swift; path = Core/StrokeLayoutManager.swift; sourceTree = ""; }; + 4C40381A1D9EB11E00305A6E /* SuccessType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SuccessType.swift; path = Core/SuccessType.swift; sourceTree = ""; }; + 4C40381C1D9EB12900305A6E /* UIColor+Palette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIColor+Palette.swift"; path = "Core/UIColor+Palette.swift"; sourceTree = ""; }; + 4C40381E1D9EB14600305A6E /* ASLLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLLogger.h; path = Core/ASLLogger.h; sourceTree = ""; }; + 4C40381F1D9EB14600305A6E /* ASLLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASLLogger.m; path = Core/ASLLogger.m; sourceTree = ""; }; + 4C4038221D9EB19400305A6E /* SourceSansPro-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SourceSansPro-Bold.ttf"; path = "Core/SourceSansPro-Bold.ttf"; sourceTree = ""; }; + 4C4038231D9EB19400305A6E /* SourceSansPro-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SourceSansPro-Regular.ttf"; path = "Core/SourceSansPro-Regular.ttf"; sourceTree = ""; }; + 4C4038241D9EB19400305A6E /* SourceSansPro-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SourceSansPro-Semibold.ttf"; path = "Core/SourceSansPro-Semibold.ttf"; sourceTree = ""; }; + 4C4038251D9EB1B700305A6E /* PinpointKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PinpointKit.h; path = PinpointKit/Sources/Core/PinpointKit.h; sourceTree = ""; }; + 4C4038271D9EB27A00305A6E /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NavigationController.swift; path = Core/NavigationController.swift; sourceTree = ""; }; + 4C40382B1D9EB7A800305A6E /* ScreenshotDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenshotDetector.swift; path = ScreenshotDetector/ScreenshotDetector.swift; sourceTree = ""; }; + 4CFB587F1E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditImageViewControllerBarButtonItemProviding.swift; path = Core/Editing/EditImageViewControllerBarButtonItemProviding.swift; sourceTree = ""; }; DA0DA6021C53049B0012ADBE /* PinpointKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PinpointKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DA0DA6051C53049B0012ADBE /* PinpointKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PinpointKit.h; path = PinpointKit/Sources/PinpointKit.h; sourceTree = ""; }; DA0DA6071C53049B0012ADBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = PinpointKit/Info.plist; sourceTree = ""; }; DA0DA60C1C53049B0012ADBE /* PinpointKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PinpointKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DA0DA6111C53049B0012ADBE /* PinpointKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinpointKitTests.swift; sourceTree = ""; }; DA0DA6131C53049B0012ADBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DA0DA61C1C5305C00012ADBE /* PinpointKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinpointKit.swift; sourceTree = ""; }; - DA3B16651C9CD2DE00DBFDE6 /* ShakeDetectingWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindow.swift; sourceTree = ""; }; - DA7B94EF1CC28DE300242E49 /* ShakeDetectingWindowDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShakeDetectingWindowDelegate.swift; sourceTree = ""; }; - F2ACE95E1CE6532000A461E5 /* LogSupporting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogSupporting.swift; sourceTree = ""; }; - F2ACE9601CE6539200A461E5 /* InterfaceCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterfaceCustomization.swift; sourceTree = ""; }; - F2ACE9DE1CEF9E2100A461E5 /* SourceSansPro-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceSansPro-Bold.ttf"; sourceTree = ""; }; - F2ACE9DF1CEF9E2100A461E5 /* SourceSansPro-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceSansPro-Regular.ttf"; sourceTree = ""; }; - F2ACE9E01CEF9E2100A461E5 /* SourceSansPro-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceSansPro-Semibold.ttf"; sourceTree = ""; }; - F2BF24031CB9A69200867E01 /* ASLLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLLogger.h; sourceTree = ""; }; - F2BF24041CB9A69200867E01 /* ASLLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLLogger.m; sourceTree = ""; }; + F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemLogCollectorTests.swift; sourceTree = ""; }; + F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCaseExpectationExtensions.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -164,9 +166,9 @@ 12580A4A1C654B3E00ADC01C /* Logging */ = { isa = PBXGroup; children = ( - 12580A361C65304800ADC01C /* LogCollector.swift */, - F2ACE95E1CE6532000A461E5 /* LogSupporting.swift */, - 12580A401C65330600ADC01C /* SystemLogCollector.swift */, + 4C4037C81D9EAEBD00305A6E /* LogCollector.swift */, + 4C4037C91D9EAEBD00305A6E /* LogSupporting.swift */, + 4C4037CA1D9EAEBD00305A6E /* SystemLogCollector.swift */, ); name = Logging; sourceTree = ""; @@ -174,8 +176,8 @@ 12580A4B1C654B4700ADC01C /* Sending */ = { isa = PBXGroup; children = ( - 12580A381C65307300ADC01C /* Sender.swift */, - 12580A421C65332C00ADC01C /* MailSender.swift */, + 4C4037CF1D9EAECF00305A6E /* Sender.swift */, + 4C4037CE1D9EAECF00305A6E /* MailSender.swift */, ); name = Sending; sourceTree = ""; @@ -183,9 +185,9 @@ 12580A4C1C654B6500ADC01C /* Editing */ = { isa = PBXGroup; children = ( - 12580A3A1C65309000ADC01C /* Editor.swift */, - 2C9CB20B1CE62297006567E2 /* EditorDelegate.swift */, - 2C9CB20D1CE622A3006567E2 /* Tool.swift */, + 4C4037D21D9EAF0D00305A6E /* Editor.swift */, + 4C4037D31D9EAF0D00305A6E /* EditorDelegate.swift */, + 4C4037D41D9EAF0D00305A6E /* Tool.swift */, B929017F1C555FDB007CCA5E /* EditImageViewController */, ); name = Editing; @@ -194,14 +196,15 @@ 12580A4D1C654B7E00ADC01C /* Feedback */ = { isa = PBXGroup; children = ( - 2CB6C1F11C652D6400022CEB /* Feedback.swift */, - 12580A3C1C6530CC00ADC01C /* FeedbackCollector.swift */, - 12580A461C65339F00ADC01C /* FeedbackViewController.swift */, - 2CEAF65B1C779C8D009368B8 /* FeedbackNavigationController.swift */, - 2CB63F1C1C77B50D000CEFC6 /* Screenshotter.swift */, - 2CB63F1E1C77C38B000CEFC6 /* ScreenshotHeaderView.swift */, - 2CB63F201C77CBE9000CEFC6 /* FeedbackTableViewDataSource.swift */, - 2CACDFCC1C9C83CE0002ECBF /* CheckmarkCell.swift */, + 4C4037EC1D9EB01800305A6E /* Feedback.swift */, + 4C4037ED1D9EB01800305A6E /* FeedbackCollector.swift */, + 4C4037F11D9EB01800305A6E /* FeedbackViewController.swift */, + 4C4037EF1D9EB01800305A6E /* FeedbackNavigationController.swift */, + 4C4037FC1D9EB04400305A6E /* Screenshotter.swift */, + 4C4037F91D9EB02300305A6E /* ScreenshotCell.swift */, + 4C4037EE1D9EB01800305A6E /* FeedbackConfiguration.swift */, + 4C4037F01D9EB01800305A6E /* FeedbackTableViewDataSource.swift */, + 4C4037F81D9EB02300305A6E /* CheckmarkCell.swift */, ); name = Feedback; sourceTree = ""; @@ -209,8 +212,8 @@ 12580A4E1C654B9700ADC01C /* Log Viewing */ = { isa = PBXGroup; children = ( - 12580A3E1C6530FB00ADC01C /* LogViewer.swift */, - 12580A481C6533DC00ADC01C /* BasicLogViewController.swift */, + 4C4038001D9EB07000305A6E /* LogViewer.swift */, + 4C4037FE1D9EB06900305A6E /* BasicLogViewController.swift */, ); name = "Log Viewing"; sourceTree = ""; @@ -218,7 +221,7 @@ 2C5C41101C655B7D0033ED3D /* Screenshot Detection */ = { isa = PBXGroup; children = ( - 2CB6C1F51C65361900022CEB /* ScreenshotDetector.swift */, + 4C40382B1D9EB7A800305A6E /* ScreenshotDetector.swift */, ); name = "Screenshot Detection"; sourceTree = ""; @@ -226,12 +229,12 @@ B929017F1C555FDB007CCA5E /* EditImageViewController */ = { isa = PBXGroup; children = ( - 2C9CB20F1CE622BC006567E2 /* NavigationController.swift */, - B92901B21C557401007CCA5E /* AssetViewModel.swift */, + 4C4038271D9EB27A00305A6E /* NavigationController.swift */, B92901A31C556783007CCA5E /* Annotations */, B92901871C55650A007CCA5E /* Views */, - B929017D1C555F86007CCA5E /* EditImageViewController.swift */, - B92901801C556185007CCA5E /* UIGestureRecognizer+FailRecognizing.swift */, + 4C4037D81D9EAF1F00305A6E /* EditImageViewController.swift */, + 4CFB587F1E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift */, + 4C4037EA1D9EAFC700305A6E /* UIGestureRecognizer+FailRecognizing.swift */, ); name = EditImageViewController; sourceTree = ""; @@ -239,17 +242,17 @@ B92901821C556383007CCA5E /* Helpers */ = { isa = PBXGroup; children = ( - 2C9CB2131CE622EB006567E2 /* UIView+PinpointKit.swift */, - 2CF3B9311CAF142400C8B29B /* NSBundle+PinpointKit.swift */, - 2CE6D3FE1C6E5D8D00DD8189 /* MIMEType.swift */, - B92901831C55639B007CCA5E /* Fonts.swift */, - B92901851C55648A007CCA5E /* KeyboardAvoider.swift */, - B92901A61C5567EC007CCA5E /* BarButtonItem.swift */, - B92901B41C557406007CCA5E /* BezierPath.swift */, - B92901B51C557406007CCA5E /* Screen.swift */, - B92901B61C557406007CCA5E /* StrokeLayoutManager.swift */, - 2CE6D4001C6E5DA400DD8189 /* SuccessType.swift */, - 2CB63F1A1C77B1B6000CEFC6 /* UIColor+Palette.swift */, + 4C4038081D9EB0CB00305A6E /* UIView+PinpointKit.swift */, + 4C40380A1D9EB0D600305A6E /* NSBundle+PinpointKit.swift */, + 4C40380C1D9EB0DD00305A6E /* MIMEType.swift */, + 4C40380E1D9EB0E400305A6E /* Fonts.swift */, + 4C4038101D9EB0EE00305A6E /* KeyboardAvoider.swift */, + 4C4038121D9EB0FB00305A6E /* BarButtonItem.swift */, + 4C4038141D9EB10500305A6E /* BezierPath.swift */, + 4C4038161D9EB10C00305A6E /* Screen.swift */, + 4C4038181D9EB11500305A6E /* StrokeLayoutManager.swift */, + 4C40381A1D9EB11E00305A6E /* SuccessType.swift */, + 4C40381C1D9EB12900305A6E /* UIColor+Palette.swift */, ); name = Helpers; sourceTree = ""; @@ -257,12 +260,12 @@ B92901871C55650A007CCA5E /* Views */ = { isa = PBXGroup; children = ( - B92901991C556774007CCA5E /* AnnotationView.swift */, - B929019A1C556774007CCA5E /* ArrowAnnotationView.swift */, - B929019D1C556778007CCA5E /* BlurAnnotationView.swift */, - B929019F1C55677C007CCA5E /* BoxAnnotationView.swift */, - B92901A01C55677C007CCA5E /* TextAnnotationView.swift */, - B92901A81C556831007CCA5E /* AnnotationsView.swift */, + 4C4037DF1D9EAFA000305A6E /* AnnotationView.swift */, + 4C4037E01D9EAFA000305A6E /* ArrowAnnotationView.swift */, + 4C4037E11D9EAFA000305A6E /* BlurAnnotationView.swift */, + 4C4037E21D9EAFA000305A6E /* BoxAnnotationView.swift */, + 4C4037E31D9EAFA000305A6E /* TextAnnotationView.swift */, + 4C4037DE1D9EAFA000305A6E /* AnnotationsView.swift */, ); name = Views; sourceTree = ""; @@ -270,8 +273,8 @@ B92901A31C556783007CCA5E /* Annotations */ = { isa = PBXGroup; children = ( - 2C9CB2111CE622CD006567E2 /* AnnotationViewFactory.swift */, - B92901A41C556791007CCA5E /* Annotations.swift */, + 4C4037DA1D9EAF3400305A6E /* AnnotationViewFactory.swift */, + 4C4037DC1D9EAF4E00305A6E /* Annotations.swift */, ); name = Annotations; sourceTree = ""; @@ -282,7 +285,7 @@ DA0DA6041C53049B0012ADBE /* PinpointKit */, F2ACE9DD1CEF9E0B00A461E5 /* PinpointKitResources */, DA0DA6071C53049B0012ADBE /* Info.plist */, - DA0DA6051C53049B0012ADBE /* PinpointKit.h */, + 4C4038251D9EB1B700305A6E /* PinpointKit.h */, DA0DA6101C53049B0012ADBE /* PinpointKitTests */, DA0DA6031C53049B0012ADBE /* Products */, ); @@ -300,10 +303,10 @@ DA0DA6041C53049B0012ADBE /* PinpointKit */ = { isa = PBXGroup; children = ( - DA0DA61C1C5305C00012ADBE /* PinpointKit.swift */, - 12580A341C652FA000ADC01C /* Configuration.swift */, - F2ACE9601CE6539200A461E5 /* InterfaceCustomization.swift */, - 2C9CB2151CE65895006567E2 /* InterfaceCustomizable.swift */, + 4C4037C31D9EAE9500305A6E /* PinpointKit.swift */, + 4C4037C01D9EAE9500305A6E /* Configuration.swift */, + 4C4037C21D9EAE9500305A6E /* InterfaceCustomization.swift */, + 4C4037C11D9EAE9500305A6E /* InterfaceCustomizable.swift */, 12580A4A1C654B3E00ADC01C /* Logging */, 12580A4B1C654B4700ADC01C /* Sending */, 12580A4C1C654B6500ADC01C /* Editing */, @@ -312,8 +315,8 @@ 2C5C41101C655B7D0033ED3D /* Screenshot Detection */, DA3B16641C9CD26F00DBFDE6 /* Shake Detection */, B92901821C556383007CCA5E /* Helpers */, - F2BF24031CB9A69200867E01 /* ASLLogger.h */, - F2BF24041CB9A69200867E01 /* ASLLogger.m */, + 4C40381E1D9EB14600305A6E /* ASLLogger.h */, + 4C40381F1D9EB14600305A6E /* ASLLogger.m */, ); name = PinpointKit; path = PinpointKit/Sources; @@ -322,7 +325,8 @@ DA0DA6101C53049B0012ADBE /* PinpointKitTests */ = { isa = PBXGroup; children = ( - DA0DA6111C53049B0012ADBE /* PinpointKitTests.swift */, + F24381131D54ECC7004CC87F /* Helpers */, + F24381111D54CBAB004CC87F /* SystemLogCollectorTests.swift */, DA0DA6131C53049B0012ADBE /* Info.plist */, ); path = PinpointKitTests; @@ -331,19 +335,27 @@ DA3B16641C9CD26F00DBFDE6 /* Shake Detection */ = { isa = PBXGroup; children = ( - DA3B16651C9CD2DE00DBFDE6 /* ShakeDetectingWindow.swift */, - DA7B94EF1CC28DE300242E49 /* ShakeDetectingWindowDelegate.swift */, - 2C9CB2091CE61F37006567E2 /* PinpointKit+ShakePresentation.swift */, + 4C4038031D9EB08E00305A6E /* ShakeDetectingWindow.swift */, + 4C4038041D9EB08E00305A6E /* ShakeDetectingWindowDelegate.swift */, + 4C4038021D9EB08E00305A6E /* PinpointKit+ShakePresentation.swift */, ); name = "Shake Detection"; sourceTree = ""; }; + F24381131D54ECC7004CC87F /* Helpers */ = { + isa = PBXGroup; + children = ( + F24381141D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift */, + ); + name = Helpers; + sourceTree = ""; + }; F2ACE9DD1CEF9E0B00A461E5 /* PinpointKitResources */ = { isa = PBXGroup; children = ( - F2ACE9DE1CEF9E2100A461E5 /* SourceSansPro-Bold.ttf */, - F2ACE9DF1CEF9E2100A461E5 /* SourceSansPro-Regular.ttf */, - F2ACE9E01CEF9E2100A461E5 /* SourceSansPro-Semibold.ttf */, + 4C4038221D9EB19400305A6E /* SourceSansPro-Bold.ttf */, + 4C4038231D9EB19400305A6E /* SourceSansPro-Regular.ttf */, + 4C4038241D9EB19400305A6E /* SourceSansPro-Semibold.ttf */, 2CACDFCE1C9C8AFB0002ECBF /* PinpointKit.xcassets */, ); name = PinpointKitResources; @@ -357,8 +369,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DA0DA6061C53049B0012ADBE /* PinpointKit.h in Headers */, - F2BF24051CB9A69200867E01 /* ASLLogger.h in Headers */, + 4C4038201D9EB14600305A6E /* ASLLogger.h in Headers */, + 4C4038261D9EB1B700305A6E /* PinpointKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -408,14 +420,16 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = Lickability; TargetAttributes = { DA0DA6011C53049B0012ADBE = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0800; }; DA0DA60B1C53049B0012ADBE = { CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0800; }; }; }; @@ -443,9 +457,6 @@ buildActionMask = 2147483647; files = ( 2CACDFCF1C9C8AFB0002ECBF /* PinpointKit.xcassets in Resources */, - F2ACE9E31CEF9E2100A461E5 /* SourceSansPro-Semibold.ttf in Resources */, - F2ACE9E11CEF9E2100A461E5 /* SourceSansPro-Bold.ttf in Resources */, - F2ACE9E21CEF9E2100A461E5 /* SourceSansPro-Regular.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -463,57 +474,59 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2CB63F1F1C77C38B000CEFC6 /* ScreenshotHeaderView.swift in Sources */, - 12580A431C65332C00ADC01C /* MailSender.swift in Sources */, - DA7B94F01CC28DE300242E49 /* ShakeDetectingWindowDelegate.swift in Sources */, - B92901A21C55677C007CCA5E /* TextAnnotationView.swift in Sources */, - 2CB6C1F61C65361900022CEB /* ScreenshotDetector.swift in Sources */, - 12580A411C65330600ADC01C /* SystemLogCollector.swift in Sources */, - 2C9CB2161CE65895006567E2 /* InterfaceCustomizable.swift in Sources */, - B92901B71C557406007CCA5E /* BezierPath.swift in Sources */, - 2CB6C1F21C652D6400022CEB /* Feedback.swift in Sources */, - 2CB63F211C77CBE9000CEFC6 /* FeedbackTableViewDataSource.swift in Sources */, - B92901861C55648A007CCA5E /* KeyboardAvoider.swift in Sources */, - 2C9CB2101CE622BC006567E2 /* NavigationController.swift in Sources */, - B929019C1C556774007CCA5E /* ArrowAnnotationView.swift in Sources */, - DA3B16661C9CD2DE00DBFDE6 /* ShakeDetectingWindow.swift in Sources */, - 2CEAF65C1C779C8D009368B8 /* FeedbackNavigationController.swift in Sources */, - 2C9CB20C1CE62297006567E2 /* EditorDelegate.swift in Sources */, - 12580A391C65307300ADC01C /* Sender.swift in Sources */, - 2C9CB20E1CE622A3006567E2 /* Tool.swift in Sources */, - B92901B91C557406007CCA5E /* StrokeLayoutManager.swift in Sources */, - B92901A11C55677C007CCA5E /* BoxAnnotationView.swift in Sources */, - 12580A3B1C65309000ADC01C /* Editor.swift in Sources */, - 12580A371C65304800ADC01C /* LogCollector.swift in Sources */, - 2CB63F1B1C77B1B6000CEFC6 /* UIColor+Palette.swift in Sources */, - B92901A91C556831007CCA5E /* AnnotationsView.swift in Sources */, - 12580A491C6533DC00ADC01C /* BasicLogViewController.swift in Sources */, - B929019B1C556774007CCA5E /* AnnotationView.swift in Sources */, - B92901B81C557406007CCA5E /* Screen.swift in Sources */, - B92901841C55639B007CCA5E /* Fonts.swift in Sources */, - B92901A71C5567EC007CCA5E /* BarButtonItem.swift in Sources */, - 2CE6D4011C6E5DA400DD8189 /* SuccessType.swift in Sources */, - 2CE6D3FF1C6E5D8D00DD8189 /* MIMEType.swift in Sources */, - B92901811C556185007CCA5E /* UIGestureRecognizer+FailRecognizing.swift in Sources */, - 12580A351C652FA000ADC01C /* Configuration.swift in Sources */, - 12580A3F1C6530FB00ADC01C /* LogViewer.swift in Sources */, - 2CACDFCD1C9C83CE0002ECBF /* CheckmarkCell.swift in Sources */, - 12580A3D1C6530CC00ADC01C /* FeedbackCollector.swift in Sources */, - 2CB63F1D1C77B50D000CEFC6 /* Screenshotter.swift in Sources */, - 2C9CB2121CE622CD006567E2 /* AnnotationViewFactory.swift in Sources */, - F2ACE9611CE6539200A461E5 /* InterfaceCustomization.swift in Sources */, + 4C40380F1D9EB0E400305A6E /* Fonts.swift in Sources */, + 4C4037C61D9EAE9500305A6E /* InterfaceCustomization.swift in Sources */, + 4C4037E51D9EAFA000305A6E /* AnnotationView.swift in Sources */, + 4C4038091D9EB0CB00305A6E /* UIView+PinpointKit.swift in Sources */, + 4CFB58801E29464B00B64D0D /* EditImageViewControllerBarButtonItemProviding.swift in Sources */, + 4C40382C1D9EB7A800305A6E /* ScreenshotDetector.swift in Sources */, + 4C4038011D9EB07000305A6E /* LogViewer.swift in Sources */, + 4C4037FF1D9EB06900305A6E /* BasicLogViewController.swift in Sources */, + 4C4037D51D9EAF0D00305A6E /* Editor.swift in Sources */, + 4C4038191D9EB11500305A6E /* StrokeLayoutManager.swift in Sources */, + 4C4037D71D9EAF0D00305A6E /* Tool.swift in Sources */, + 4C4037F51D9EB01800305A6E /* FeedbackNavigationController.swift in Sources */, + 4C4037D91D9EAF1F00305A6E /* EditImageViewController.swift in Sources */, + 4C4037F71D9EB01800305A6E /* FeedbackViewController.swift in Sources */, + 4C4037CD1D9EAEBD00305A6E /* SystemLogCollector.swift in Sources */, + 4C4038051D9EB08E00305A6E /* PinpointKit+ShakePresentation.swift in Sources */, + 4C40381B1D9EB11E00305A6E /* SuccessType.swift in Sources */, + 4C4037C71D9EAE9500305A6E /* PinpointKit.swift in Sources */, + 4C4037E81D9EAFA000305A6E /* BoxAnnotationView.swift in Sources */, + 4C4037D61D9EAF0D00305A6E /* EditorDelegate.swift in Sources */, + 4C4038211D9EB14600305A6E /* ASLLogger.m in Sources */, + 4C4037C41D9EAE9500305A6E /* Configuration.swift in Sources */, + 4C40380D1D9EB0DD00305A6E /* MIMEType.swift in Sources */, + 4C4037D11D9EAECF00305A6E /* Sender.swift in Sources */, + 4C4038071D9EB08E00305A6E /* ShakeDetectingWindowDelegate.swift in Sources */, + 4C40381D1D9EB12900305A6E /* UIColor+Palette.swift in Sources */, + 4C4038281D9EB27A00305A6E /* NavigationController.swift in Sources */, + 4C4037E61D9EAFA000305A6E /* ArrowAnnotationView.swift in Sources */, + 4C4038151D9EB10500305A6E /* BezierPath.swift in Sources */, + 4C4037E91D9EAFA000305A6E /* TextAnnotationView.swift in Sources */, + 4C4038061D9EB08E00305A6E /* ShakeDetectingWindow.swift in Sources */, + 4C4037F31D9EB01800305A6E /* FeedbackCollector.swift in Sources */, + 4C4037CB1D9EAEBD00305A6E /* LogCollector.swift in Sources */, + 4C4038171D9EB10C00305A6E /* Screen.swift in Sources */, + 4C4038131D9EB0FB00305A6E /* BarButtonItem.swift in Sources */, + 4C4037FA1D9EB02300305A6E /* CheckmarkCell.swift in Sources */, + 4C4038111D9EB0EE00305A6E /* KeyboardAvoider.swift in Sources */, + 4C4037E41D9EAFA000305A6E /* AnnotationsView.swift in Sources */, + 4C4037DD1D9EAF4E00305A6E /* Annotations.swift in Sources */, + 4C4037F41D9EB01800305A6E /* FeedbackConfiguration.swift in Sources */, 2C5C41121C6560AD0033ED3D /* (null) in Sources */, - B929017E1C555F86007CCA5E /* EditImageViewController.swift in Sources */, - B929019E1C556778007CCA5E /* BlurAnnotationView.swift in Sources */, - B92901A51C556791007CCA5E /* Annotations.swift in Sources */, - F2BF24061CB9A69200867E01 /* ASLLogger.m in Sources */, - F2ACE95F1CE6532000A461E5 /* LogSupporting.swift in Sources */, + 4C4037D01D9EAECF00305A6E /* MailSender.swift in Sources */, + 4C4037E71D9EAFA000305A6E /* BlurAnnotationView.swift in Sources */, + 4C4037F61D9EB01800305A6E /* FeedbackTableViewDataSource.swift in Sources */, + 4C4037DB1D9EAF3400305A6E /* AnnotationViewFactory.swift in Sources */, + 4C4037F21D9EB01800305A6E /* Feedback.swift in Sources */, + 4C4037FD1D9EB04400305A6E /* Screenshotter.swift in Sources */, + 4C4037CC1D9EAEBD00305A6E /* LogSupporting.swift in Sources */, + 4C4037EB1D9EAFC700305A6E /* UIGestureRecognizer+FailRecognizing.swift in Sources */, + 4C4037FB1D9EB02300305A6E /* ScreenshotCell.swift in Sources */, 2C5C41141C6568D60033ED3D /* (null) in Sources */, - 2CF3B9321CAF142400C8B29B /* NSBundle+PinpointKit.swift in Sources */, - 12580A471C65339F00ADC01C /* FeedbackViewController.swift in Sources */, - DA0DA61D1C5305C00012ADBE /* PinpointKit.swift in Sources */, - 2C9CB2141CE622EB006567E2 /* UIView+PinpointKit.swift in Sources */, - 2C9CB20A1CE61F37006567E2 /* PinpointKit+ShakePresentation.swift in Sources */, + 4C4037C51D9EAE9500305A6E /* InterfaceCustomizable.swift in Sources */, + 4C40380B1D9EB0D600305A6E /* NSBundle+PinpointKit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -521,7 +534,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DA0DA6121C53049B0012ADBE /* PinpointKitTests.swift in Sources */, + F24381121D54CBAB004CC87F /* SystemLogCollectorTests.swift in Sources */, + F24381151D54ECD2004CC87F /* XCTestCaseExpectationExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -549,8 +563,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -578,6 +594,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -597,8 +614,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -618,6 +637,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -629,6 +650,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -641,6 +663,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -648,6 +671,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -659,6 +683,7 @@ PRODUCT_BUNDLE_IDENTIFIER = net.Lickability.PinpointKit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -669,6 +694,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.Lickability.PinpointKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -679,6 +705,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = net.Lickability.PinpointKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/PinpointKit/PinpointKit.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme b/PinpointKit/PinpointKit.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme index 6d5e383..3eea93d 100644 --- a/PinpointKit/PinpointKit.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme +++ b/PinpointKit/PinpointKit.xcodeproj/xcshareddata/xcschemes/PinpointKit.xcscheme @@ -1,6 +1,6 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.9 + 1.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/PinpointKit/PinpointKit/Sources/ASLLogger.m b/PinpointKit/PinpointKit/Sources/ASLLogger.m deleted file mode 100644 index c665f45..0000000 --- a/PinpointKit/PinpointKit/Sources/ASLLogger.m +++ /dev/null @@ -1,58 +0,0 @@ -// -// ASLLogger.m -// PinpointKit -// -// Created by Andrew Harrison on 4/9/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -#import "ASLLogger.h" -#import - -@implementation ASLLogger - -- (NSArray *)retrieveLogs { - NSMutableArray *logs = [NSMutableArray array]; - - aslmsg query = NULL, message = NULL; - aslresponse response = NULL; - - query = asl_new(ASL_TYPE_QUERY); - asl_set_query(query, ASL_KEY_FACILITY, [[[NSBundle mainBundle] bundleIdentifier] UTF8String], ASL_QUERY_OP_EQUAL); - - response = asl_search(NULL, query); - - pid_t myPID = getpid(); - - while ((message = asl_next(response)) != NULL) { - - if (myPID != atol(asl_get(message, ASL_KEY_PID))) { - continue; - } - - const char *content = asl_get(message, ASL_KEY_MSG); - NSTimeInterval msgTime = (NSTimeInterval) atol(asl_get(message, ASL_KEY_TIME)) + ((NSTimeInterval) atol(asl_get(message, ASL_KEY_TIME_NSEC)) / 1000000000.0); - - NSString *contentString = [[NSString alloc] initWithUTF8String:content]; - NSString *timeString = [self stringFromTimeInterval:msgTime]; - NSString *loggedText = [NSString stringWithFormat:@"%@ %@", timeString, contentString]; - - [logs addObject:loggedText]; - } - - asl_release(response); - asl_free(query); - - return logs; -} - -- (NSString *)stringFromTimeInterval:(NSTimeInterval)interval { - char fdate[24]; - time_t timestamp = (time_t)interval; - struct tm *lt = localtime(×tamp); - strftime(fdate, 24, "%B %d %T", lt); - - return [NSString stringWithFormat:@"%s", fdate]; -} - -@end diff --git a/PinpointKit/PinpointKit/Sources/BezierPath.swift b/PinpointKit/PinpointKit/Sources/BezierPath.swift deleted file mode 100644 index bdaf420..0000000 --- a/PinpointKit/PinpointKit/Sources/BezierPath.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// BezierPath.swift -// Pinpoint -// -// Created by Brian Capps on 5/5/15. -// Copyright (c) 2015 Lickability. All rights reserved. -// - -import UIKit - -extension UIBezierPath { - private static let PaintCodeArrowPathWidth: CGFloat = 267.0 - - /** - Creates a bezier path in the shape of an arrow. - - - parameter startPoint: The starting control point of the shape. - - parameter endPoint: The ending control point of the shape. - - - returns: A `UIBezierPath` in the shape of an arrow. - */ - static func arrowBezierPath(startPoint startPoint: CGPoint, endPoint: CGPoint) -> UIBezierPath { - let length = hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y) - - // Shape 267x120 from PaintCode. 0 is the mid Y of the arrow to match the original arrow Y. - let bezierPath = UIBezierPath() - bezierPath.moveToPoint(CGPoint(x: 197.29, y: -57.56)) - bezierPath.addLineToPoint(CGPoint(x: 194.32, y: -54.58)) - bezierPath.addCurveToPoint(CGPoint(x: 193.82, y: -43.36), controlPoint1: CGPoint(x: 191.31, y: -51.53), controlPoint2: CGPoint(x: 191.09, y: -46.67)) - bezierPath.addLineToPoint(CGPoint(x: 215.25, y: -17.45)) - bezierPath.addCurveToPoint(CGPoint(x: 213.11, y: -12.74), controlPoint1: CGPoint(x: 216.79, y: -15.59), controlPoint2: CGPoint(x: 215.5, y: -12.77)) - bezierPath.addLineToPoint(CGPoint(x: 8.15, y: -10.22)) - bezierPath.addCurveToPoint(CGPoint(x: 0, y: -1.9), controlPoint1: CGPoint(x: 3.63, y: -10.17), controlPoint2: CGPoint(x: 0, y: -6.46)) - bezierPath.addLineToPoint(CGPoint(x: 0, y: 1.81)) - bezierPath.addCurveToPoint(CGPoint(x: 8.15, y: 10.13), controlPoint1: CGPoint(x: 0, y: 6.37), controlPoint2: CGPoint(x: 3.63, y: 10.08)) - bezierPath.addLineToPoint(CGPoint(x: 213.18, y: 12.65)) - bezierPath.addCurveToPoint(CGPoint(x: 215.33, y: 17.36), controlPoint1: CGPoint(x: 215.58, y: 12.68), controlPoint2: CGPoint(x: 216.86, y: 15.5)) - bezierPath.addLineToPoint(CGPoint(x: 193.82, y: 43.36)) - bezierPath.addCurveToPoint(CGPoint(x: 194.32, y: 54.58), controlPoint1: CGPoint(x: 191.09, y: 46.67), controlPoint2: CGPoint(x: 191.31, y: 51.53)) - bezierPath.addLineToPoint(CGPoint(x: 197.29, y: 57.56)) - bezierPath.addCurveToPoint(CGPoint(x: 208.95, y: 57.56), controlPoint1: CGPoint(x: 200.51, y: 60.81), controlPoint2: CGPoint(x: 205.73, y: 60.81)) - bezierPath.addLineToPoint(CGPoint(x: 266, y: -0)) - bezierPath.addLineToPoint(CGPoint(x: 208.95, y: -57.56)) - bezierPath.addCurveToPoint(CGPoint(x: 197.29, y: -57.56), controlPoint1: CGPoint(x: 205.73, y: -60.81), controlPoint2: CGPoint(x: 200.51, y: -60.81)) - bezierPath.closePath() - bezierPath.usesEvenOddFillRule = true - - bezierPath.applyTransform(transformForStartPoint(startPoint, endPoint: endPoint, length: length)) - - return bezierPath - } - - private static func transformForStartPoint(startPoint: CGPoint, endPoint: CGPoint, length: CGFloat) -> CGAffineTransform { - let cosine = (endPoint.x - startPoint.x) / length - let sine = (endPoint.y - startPoint.y) / length - - let scale: CGFloat = length / PaintCodeArrowPathWidth - let scaleTransform = CGAffineTransformMakeScale(scale, scale) - - let rotationAndSizeTransform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: startPoint.x, ty: startPoint.y) - return CGAffineTransformConcat(scaleTransform, rotationAndSizeTransform) - } -} diff --git a/PinpointKit/PinpointKit/Sources/BoxAnnotationView.swift b/PinpointKit/PinpointKit/Sources/BoxAnnotationView.swift deleted file mode 100644 index 2f894d6..0000000 --- a/PinpointKit/PinpointKit/Sources/BoxAnnotationView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// BoxAnnotationView.swift -// Pinpoint -// -// Created by Caleb Davenport on 3/29/15. -// Copyright (c) 2015 Lickability. All rights reserved. -// - -import UIKit - -/// The default box annotation view. -public class BoxAnnotationView: AnnotationView { - - // MARK: - Properties - - /// The corresponding annotation. - var annotation: BoxAnnotation? { - didSet { - layer.shadowPath = annotation.flatMap(PathForDrawingBoxAnnotation)?.CGPath - setNeedsDisplay() - } - } - - override var annotationFrame: CGRect? { - return annotation?.frame - } - - - // MARK: - Initializers - - convenience init() { - self.init(frame: CGRect.zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - - opaque = false - contentMode = .Redraw - - layer.shadowOffset = CGSize.zero - layer.shadowColor = UIColor.blackColor().CGColor - layer.shadowOpacity = 1 - layer.shadowRadius = 4 - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - // MARK: - UIView - - override public func tintColorDidChange() { - super.tintColorDidChange() - setNeedsDisplay() - } - - override public func drawRect(rect: CGRect) { - tintColor.setFill() - annotation?.strokeColor.setStroke() - - let path = annotation.flatMap(PathForDrawingBoxAnnotation) - path?.fill() - path?.stroke() - } - - override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { - return annotation.flatMap(PathForPointInsideBoxAnnotation).map { $0.containsPoint(point) } ?? false - } - - - // MARK: - AnnotationView - - override func setSecondControlPoint(point: CGPoint) { - guard let previousAnnotation = annotation else { return } - - annotation = BoxAnnotation(startLocation: previousAnnotation.startLocation, endLocation: point, strokeColor: previousAnnotation.strokeColor) - } - - override func moveControlPoints(translation: CGPoint) { - guard let previousAnnotation = annotation else { return } - let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translation.x, y: previousAnnotation.startLocation.y + translation.y) - let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translation.x, y: previousAnnotation.endLocation.y + translation.y) - - annotation = BoxAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) - } - - override func scaleControlPoints(scale: CGFloat) { - guard let previousAnnotation = annotation else { return } - let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scale) - let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scale) - - annotation = BoxAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) - } -} - -private func PathForDrawingBoxAnnotation(annotation: BoxAnnotation) -> UIBezierPath? { - let frame = annotation.frame - let strokeWidth = annotation.strokeWidth - let borderWidth = annotation.borderWidth - let cornerRadius = annotation.cornerRadius - - let outerBox = frame.insetBy(dx: strokeWidth, dy: strokeWidth) - let innerBox = outerBox.insetBy(dx: borderWidth + strokeWidth, dy: borderWidth + strokeWidth) - - if min(innerBox.size.height, innerBox.size.width) < (borderWidth + strokeWidth) * 2.0 { - return nil - } - - if min(innerBox.size.height, innerBox.size.width) < cornerRadius * 2.5 { - return nil - } - - let firstPath = CGPathCreateWithRoundedRect(innerBox, cornerRadius, cornerRadius, nil) - let secondPath = CGPathCreateCopyByStrokingPath(firstPath, nil, borderWidth + strokeWidth, .Butt, .Bevel, 100) - - guard let strokePath = secondPath else { return nil } - - let path = UIBezierPath(CGPath: strokePath) - path.lineWidth = strokeWidth - path.closePath() - return path -} - -private func PathForPointInsideBoxAnnotation(annotation: BoxAnnotation) -> UIBezierPath? { - let outsideStrokeWidth = annotation.borderWidth * 2.0 - - return PathForDrawingBoxAnnotation(annotation) - .flatMap { path in - CGPathCreateCopyByStrokingPath(path.CGPath, nil, outsideStrokeWidth, .Butt, .Bevel, 0) - } - .map { path in - UIBezierPath(CGPath: path) - } -} diff --git a/PinpointKit/PinpointKit/Sources/ASLLogger.h b/PinpointKit/PinpointKit/Sources/Core/ASLLogger.h similarity index 67% rename from PinpointKit/PinpointKit/Sources/ASLLogger.h rename to PinpointKit/PinpointKit/Sources/Core/ASLLogger.h index 51e5a5d..84bfcc2 100644 --- a/PinpointKit/PinpointKit/Sources/ASLLogger.h +++ b/PinpointKit/PinpointKit/Sources/Core/ASLLogger.h @@ -14,6 +14,22 @@ NS_ASSUME_NONNULL_BEGIN /// @warning: Use the Swift `SystemLogCollector` object instead of using this object directly. @interface ASLLogger : NSObject +/** + * Initialize the ASLLogger with a bundle identifier. + * + * @param senderName The bundle identifier to retrieve logs for. + */ +- (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier NS_DESIGNATED_INITIALIZER; + +/** + * Initialize the ASLLogger with a specific sender name. + * + * @param senderName The sender name to retrieve logs for. + */ +- (instancetype)initWithSenderName:(NSString *)senderName NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + /** Using the Apple System Logger API, retrieves and returns logs as an ordered list of strings. diff --git a/PinpointKit/PinpointKit/Sources/Core/ASLLogger.m b/PinpointKit/PinpointKit/Sources/Core/ASLLogger.m new file mode 100644 index 0000000..d8db210 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/ASLLogger.m @@ -0,0 +1,95 @@ +// +// ASLLogger.m +// PinpointKit +// +// Created by Andrew Harrison on 4/9/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +#import "ASLLogger.h" +#import + +@interface ASLLogger () + +@property (nonatomic, nullable) NSString *bundleIdentifier; +@property (nonatomic, nullable) NSString *senderName; +@property (nonatomic) NSDate *logDate; + +@end + +@implementation ASLLogger + +- (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier { + self = [super init]; + + _bundleIdentifier = bundleIdentifier; + [self commonInitialization]; + + return self; +} + +- (instancetype)initWithSenderName:(NSString *)senderName { + self = [super init]; + + _senderName = senderName; + [self commonInitialization]; + + return self; +} + +- (void)commonInitialization { + _logDate = [NSDate date]; +} + +- (NSArray *)retrieveLogs { + NSMutableArray *logs = [NSMutableArray array]; + + aslmsg query = NULL, message = NULL; + aslresponse response = NULL; + + query = asl_new(ASL_TYPE_QUERY); + + if (self.bundleIdentifier) { + asl_set_query(query, ASL_KEY_FACILITY, self.bundleIdentifier.UTF8String, ASL_QUERY_OP_EQUAL); + } else if (self.senderName) { + asl_set_query(query, ASL_KEY_SENDER, self.senderName.UTF8String, ASL_QUERY_OP_EQUAL); + } + + response = asl_search(NULL, query); + + pid_t myPID = getpid(); + + while ((message = asl_next(response)) != NULL) { + + if (myPID != atol(asl_get(message, ASL_KEY_PID))) { + continue; + } + + const char *content = asl_get(message, ASL_KEY_MSG); + NSTimeInterval msgTime = (NSTimeInterval) atol(asl_get(message, ASL_KEY_TIME)) + ((NSTimeInterval) atol(asl_get(message, ASL_KEY_TIME_NSEC)) / 1000000000.0); + + if (self.logDate.timeIntervalSince1970 < msgTime) { + NSString *contentString = [[NSString alloc] initWithUTF8String:content]; + NSString *timeString = [self stringFromTimeInterval:msgTime]; + NSString *loggedText = [NSString stringWithFormat:@"%@ %@", timeString, contentString]; + + [logs addObject:loggedText]; + } + } + + asl_release(response); + asl_free(query); + + return logs; +} + +- (NSString *)stringFromTimeInterval:(NSTimeInterval)interval { + char fdate[24]; + time_t timestamp = (time_t)interval; + struct tm *lt = localtime(×tamp); + strftime(fdate, 24, "%B %d %T", lt); + + return [NSString stringWithFormat:@"%s", fdate]; +} + +@end diff --git a/PinpointKit/PinpointKit/Sources/AnnotationView.swift b/PinpointKit/PinpointKit/Sources/Core/AnnotationView.swift similarity index 67% rename from PinpointKit/PinpointKit/Sources/AnnotationView.swift rename to PinpointKit/PinpointKit/Sources/Core/AnnotationView.swift index dc784ea..d639002 100644 --- a/PinpointKit/PinpointKit/Sources/AnnotationView.swift +++ b/PinpointKit/PinpointKit/Sources/Core/AnnotationView.swift @@ -9,7 +9,7 @@ import UIKit /// The base annotation `UIView` subclass. -public class AnnotationView: UIView { +open class AnnotationView: UIView { /// The alpha value used for annotation borders. static let BorderAlpha: CGFloat = 0.7 @@ -24,20 +24,20 @@ public class AnnotationView: UIView { // MARK: - Helpers /** - Moves the control points of the annotation by the amount specified in `translation`. + Moves the control points of the annotation by the amount specified in `translationAmount`. - - parameter translation: The amount to translate the control points. + - parameter translationAmount: The amount to translate the control points. */ - func moveControlPoints(translation: CGPoint) { + func move(controlPointsBy translationAmount: CGPoint) { } /** - Scales the control points of the annotation by the amount specified in `scale`. + Scales the control points of the annotation by the amount specified in `scaleFactor`. - - parameter scale: The factor by which to scale the annotation. + - parameter scaleFactor: The factor by which to scale the annotation. */ - func scaleControlPoints(scale: CGFloat) { + func scale(controlPointsBy scaleFactor: CGFloat) { } @@ -46,7 +46,7 @@ public class AnnotationView: UIView { - parameter point: The new value for the annotation’s second control point. */ - func setSecondControlPoint(point: CGPoint) { + func setSecondControlPoint(_ point: CGPoint) { } } diff --git a/PinpointKit/PinpointKit/Sources/Annotations.swift b/PinpointKit/PinpointKit/Sources/Core/Annotations.swift similarity index 90% rename from PinpointKit/PinpointKit/Sources/Annotations.swift rename to PinpointKit/PinpointKit/Sources/Core/Annotations.swift index 41fae6f..c2e99c0 100644 --- a/PinpointKit/PinpointKit/Sources/Annotations.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Annotations.swift @@ -46,8 +46,8 @@ class Annotation { - returns: The scaled point. */ - func scaledPoint(point: CGPoint, scale: CGFloat) -> CGPoint { - var scaledRect = CGRectApplyAffineTransform(frame, CGAffineTransformMakeScale(scale, scale)) + func scaledPoint(_ point: CGPoint, scale: CGFloat) -> CGPoint { + var scaledRect = frame.applying(CGAffineTransform(scaleX: scale, y: scale)) let centeredXDistance = scaledRect.width / 2.0 - frame.width / 2.0 let centeredYDistance = scaledRect.height / 2.0 - frame.height / 2.0 @@ -160,25 +160,25 @@ class BlurAnnotation: Annotation { var image: CIImage? = self.image let extent = image?.extent - let transform = NSValue(CGAffineTransform: CGAffineTransformIdentity) + let transform = NSValue(cgAffineTransform: CGAffineTransform.identity) let affineClampFilter = CIFilter(name: "CIAffineClamp") affineClampFilter?.setValue(image, forKey: kCIInputImageKey) affineClampFilter?.setValue(transform, forKey: kCIInputTransformKey) - image = affineClampFilter?.valueForKey(kCIOutputImageKey) as? CIImage + image = affineClampFilter?.value(forKey: kCIOutputImageKey) as? CIImage let pixellateFilter = CIFilter(name: "CIPixellate") pixellateFilter?.setValue(image, forKey: kCIInputImageKey) let inputScale = 16 pixellateFilter?.setValue(inputScale, forKey: kCIInputScaleKey) - image = pixellateFilter?.valueForKey(kCIOutputImageKey) as? CIImage + image = pixellateFilter?.value(forKey: kCIOutputImageKey) as? CIImage - if let imageValue = image, extentValue = extent { - let vector = CIVector(CGRect: extentValue) + if let imageValue = image, let extentValue = extent { + let vector = CIVector(cgRect: extentValue) let filter: CIFilter? = CIFilter(name: "CICrop") filter?.setValue(imageValue, forKey: kCIInputImageKey) filter?.setValue(vector, forKey: "inputRectangle") - image = filter?.valueForKey(kCIOutputImageKey) as? CIImage + image = filter?.value(forKey: kCIOutputImageKey) as? CIImage } return image @@ -195,6 +195,6 @@ class BlurAnnotation: Annotation { */ init(startLocation: CGPoint, endLocation: CGPoint, image: CIImage) { self.image = image - super.init(startLocation: startLocation, endLocation: endLocation, strokeColor: .clearColor()) + super.init(startLocation: startLocation, endLocation: endLocation, strokeColor: .clear) } } diff --git a/PinpointKit/PinpointKit/Sources/AnnotationsView.swift b/PinpointKit/PinpointKit/Sources/Core/AnnotationsView.swift similarity index 72% rename from PinpointKit/PinpointKit/Sources/AnnotationsView.swift rename to PinpointKit/PinpointKit/Sources/Core/AnnotationsView.swift index b4d65f2..92a4825 100644 --- a/PinpointKit/PinpointKit/Sources/AnnotationsView.swift +++ b/PinpointKit/PinpointKit/Sources/Core/AnnotationsView.swift @@ -10,13 +10,13 @@ import UIKit /// A `UIView` subclass that displays annotations. class AnnotationsView: UIView { - override func addSubview(view: UIView) { + override func addSubview(_ view: UIView) { super.addSubview(view) moveViewIfAppropriate(view) } - override func bringSubviewToFront(view: UIView) { - super.bringSubviewToFront(view) + override func bringSubview(toFront view: UIView) { + super.bringSubview(toFront: view) moveViewIfAppropriate(view) } @@ -25,9 +25,9 @@ class AnnotationsView: UIView { - parameter view: The view to potentially move. */ - func moveViewIfAppropriate(view: UIView) { + func moveViewIfAppropriate(_ view: UIView) { if let blurView = view as? BlurAnnotationView { - moveBlurViewAboveBlurViewsAndUnderOthers(blurView: blurView) + moveBlurViewAboveBlurViewsAndUnderOthers(blurView) } } @@ -36,16 +36,22 @@ class AnnotationsView: UIView { - parameter blurView: The blur view to move. */ - func moveBlurViewAboveBlurViewsAndUnderOthers(blurView blurView: BlurAnnotationView) { + func moveBlurViewAboveBlurViewsAndUnderOthers(_ blurView: BlurAnnotationView) { var lastBlurViewIndex: Int? - for (index, subview) in subviews.enumerate() { + for (index, subview) in subviews.enumerated() { if subview is BlurAnnotationView && subview as? BlurAnnotationView != blurView { lastBlurViewIndex = index } } - let index = lastBlurViewIndex?.successor() ?? 0 - insertSubview(blurView, atIndex: index) + let index: Int + if let lastIndex = lastBlurViewIndex { + index = lastIndex + 1 + } else { + index = 0 + } + + insertSubview(blurView, at: index) } override func tintColorDidChange() { diff --git a/PinpointKit/PinpointKit/Sources/ArrowAnnotationView.swift b/PinpointKit/PinpointKit/Sources/Core/ArrowAnnotationView.swift similarity index 67% rename from PinpointKit/PinpointKit/Sources/ArrowAnnotationView.swift rename to PinpointKit/PinpointKit/Sources/Core/ArrowAnnotationView.swift index 6b47fd2..8009e2d 100644 --- a/PinpointKit/PinpointKit/Sources/ArrowAnnotationView.swift +++ b/PinpointKit/PinpointKit/Sources/Core/ArrowAnnotationView.swift @@ -9,7 +9,7 @@ import UIKit /// The default arrow annotation view. -public class ArrowAnnotationView: AnnotationView { +open class ArrowAnnotationView: AnnotationView { // MARK: - Properties @@ -17,7 +17,7 @@ public class ArrowAnnotationView: AnnotationView { var annotation: ArrowAnnotation? { didSet { setNeedsDisplay() - layer.shadowPath = annotation?.path?.CGPath + layer.shadowPath = annotation?.path?.cgPath } } @@ -34,11 +34,11 @@ public class ArrowAnnotationView: AnnotationView { override init(frame: CGRect) { super.init(frame: frame) - opaque = false - contentMode = .Redraw + isOpaque = false + contentMode = .redraw layer.shadowOffset = CGSize.zero - layer.shadowColor = UIColor.blackColor().CGColor + layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 1 layer.shadowRadius = 4 } @@ -50,12 +50,12 @@ public class ArrowAnnotationView: AnnotationView { // MARK: - UIView - override public func tintColorDidChange() { + override open func tintColorDidChange() { super.tintColorDidChange() setNeedsDisplay() } - override public func drawRect(rect: CGRect) { + override open func draw(_ rect: CGRect) { tintColor.setFill() annotation?.strokeColor.setStroke() @@ -64,31 +64,31 @@ public class ArrowAnnotationView: AnnotationView { path?.stroke() } - override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { - return annotation?.touchTargetPath?.containsPoint(point) ?? false + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return annotation?.touchTargetPath?.contains(point) ?? false } // MARK: - AnnotationView - override func setSecondControlPoint(point: CGPoint) { + override func setSecondControlPoint(_ point: CGPoint) { guard let previousAnnotation = annotation else { return } annotation = ArrowAnnotation(startLocation: previousAnnotation.startLocation, endLocation: point, strokeColor: previousAnnotation.strokeColor) } - override func moveControlPoints(translation: CGPoint) { + override func move(controlPointsBy translationAmount: CGPoint) { guard let previousAnnotation = annotation else { return } - let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translation.x, y: previousAnnotation.startLocation.y + translation.y) - let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translation.x, y: previousAnnotation.endLocation.y + translation.y) + let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translationAmount.x, y: previousAnnotation.startLocation.y + translationAmount.y) + let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translationAmount.x, y: previousAnnotation.endLocation.y + translationAmount.y) annotation = ArrowAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) } - override func scaleControlPoints(scale: CGFloat) { + override func scale(controlPointsBy scaleFactor: CGFloat) { guard let previousAnnotation = annotation else { return } - let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scale) - let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scale) + let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scaleFactor) + let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scaleFactor) annotation = ArrowAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) } @@ -101,10 +101,7 @@ private extension ArrowAnnotation { return nil } - let path = UIBezierPath.arrowBezierPath( - startPoint: startLocation, - endPoint: endLocation - ) + let path = UIBezierPath.arrowBezierPath(startLocation, endPoint: endLocation) path.lineWidth = strokeWidth return path @@ -114,8 +111,9 @@ private extension ArrowAnnotation { guard let path = path else { return nil } let outsideStrokeWidth = strokeWidth * 5.0 - guard let strokedPath = CGPathCreateCopyByStrokingPath(path.CGPath, nil, outsideStrokeWidth, .Butt, .Bevel, 0) else { return nil } - return UIBezierPath(CGPath: strokedPath) + let strokedPath = path.cgPath.copy(strokingWithWidth: outsideStrokeWidth, lineCap: .butt, lineJoin: .bevel, miterLimit: 0) + + return UIBezierPath(cgPath: strokedPath) } } diff --git a/PinpointKit/PinpointKit/Sources/BarButtonItem.swift b/PinpointKit/PinpointKit/Sources/Core/BarButtonItem.swift similarity index 63% rename from PinpointKit/PinpointKit/Sources/BarButtonItem.swift rename to PinpointKit/PinpointKit/Sources/Core/BarButtonItem.swift index 7d1fdd2..885f5f0 100644 --- a/PinpointKit/PinpointKit/Sources/BarButtonItem.swift +++ b/PinpointKit/PinpointKit/Sources/Core/BarButtonItem.swift @@ -14,12 +14,13 @@ extension UIBarButtonItem { Convenience initializer for creating a “Done” `UIBarButtonItem` with a specified target, action, and font. - parameter target: The bar button item’s target. + - parameter title: The bar button item’s title. - parameter font: The font of the bar button item’s title. - parameter action: The bar button item’s action. */ - convenience init(doneButtonWithTarget target: AnyObject?, font: UIFont, action: Selector) { - self.init(barButtonSystemItem: .Done, target: target, action: action) + convenience init(doneButtonWithTarget target: AnyObject?, title: String, font: UIFont, action: Selector) { + self.init(title: title, style: .done, target: target, action: action) - setTitleTextAttributes([NSFontAttributeName: font], forState: .Normal) + setTitleTextAttributes([NSFontAttributeName: font], for: UIControlState()) } } diff --git a/PinpointKit/PinpointKit/Sources/BasicLogViewController.swift b/PinpointKit/PinpointKit/Sources/Core/BasicLogViewController.swift similarity index 53% rename from PinpointKit/PinpointKit/Sources/BasicLogViewController.swift rename to PinpointKit/PinpointKit/Sources/Core/BasicLogViewController.swift index 3c7d4b7..743bc94 100644 --- a/PinpointKit/PinpointKit/Sources/BasicLogViewController.swift +++ b/PinpointKit/PinpointKit/Sources/Core/BasicLogViewController.swift @@ -9,11 +9,11 @@ import UIKit /// The default view controller for the text log. -public class BasicLogViewController: UIViewController, LogViewer { +open class BasicLogViewController: UIViewController, LogViewer { // MARK: - InterfaceCustomizable - public var interfaceCustomization: InterfaceCustomization? { + open var interfaceCustomization: InterfaceCustomization? { didSet { title = interfaceCustomization?.interfaceText.logCollectorTitle textView.font = interfaceCustomization?.appearance.logFont @@ -25,30 +25,30 @@ public class BasicLogViewController: UIViewController, LogViewer { private let textView: UITextView = { let textView = UITextView() textView.translatesAutoresizingMaskIntoConstraints = false - textView.editable = false - textView.dataDetectorTypes = .None + textView.isEditable = false + textView.dataDetectorTypes = UIDataDetectorTypes() return textView }() // MARK: - UIViewController - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() func setUpTextView() { view.addSubview(textView) - textView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor, constant: 0).active = true - textView.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor, constant: 0).active = true - textView.widthAnchor.constraintEqualToAnchor(view.widthAnchor, constant: 0).active = true - textView.heightAnchor.constraintEqualToAnchor(view.heightAnchor, constant: 0).active = true + textView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true + textView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true + textView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: 0).isActive = true + textView.heightAnchor.constraint(equalTo: view.heightAnchor, constant: 0).isActive = true } setUpTextView() } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) textView.scrollRangeToVisible(NSRange(location: (textView.text as NSString).length, length: 0)) @@ -56,10 +56,10 @@ public class BasicLogViewController: UIViewController, LogViewer { // MARK: - LogViewer - public func viewLog(collector: LogCollector, fromViewController viewController: UIViewController) { - let logText = collector.retrieveLogs().joinWithSeparator("\n") + open func viewLog(in collector: LogCollector, from viewController: UIViewController) { + let logText = collector.retrieveLogs().joined(separator: "\n") textView.text = logText - viewController.showViewController(self, sender: viewController) + viewController.show(self, sender: viewController) } } diff --git a/PinpointKit/PinpointKit/Sources/Core/BezierPath.swift b/PinpointKit/PinpointKit/Sources/Core/BezierPath.swift new file mode 100644 index 0000000..de81d21 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/BezierPath.swift @@ -0,0 +1,63 @@ +// +// BezierPath.swift +// Pinpoint +// +// Created by Brian Capps on 5/5/15. +// Copyright (c) 2015 Lickability. All rights reserved. +// + +import UIKit + +extension UIBezierPath { + private static let PaintCodeArrowPathWidth: CGFloat = 267.0 + + /** + Creates a bezier path in the shape of an arrow. + + - parameter startPoint: The starting control point of the shape. + - parameter endPoint: The ending control point of the shape. + + - returns: A `UIBezierPath` in the shape of an arrow. + */ + static func arrowBezierPath(_ startPoint: CGPoint, endPoint: CGPoint) -> UIBezierPath { + let length = hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y) + + // Shape 267x120 from PaintCode. 0 is the mid Y of the arrow to match the original arrow Y. + let bezierPath = UIBezierPath() + bezierPath.move(to: CGPoint(x: 197.29, y: -57.56)) + bezierPath.addLine(to: CGPoint(x: 194.32, y: -54.58)) + bezierPath.addCurve(to: CGPoint(x: 193.82, y: -43.36), controlPoint1: CGPoint(x: 191.31, y: -51.53), controlPoint2: CGPoint(x: 191.09, y: -46.67)) + bezierPath.addLine(to: CGPoint(x: 215.25, y: -17.45)) + bezierPath.addCurve(to: CGPoint(x: 213.11, y: -12.74), controlPoint1: CGPoint(x: 216.79, y: -15.59), controlPoint2: CGPoint(x: 215.5, y: -12.77)) + bezierPath.addLine(to: CGPoint(x: 8.15, y: -10.22)) + bezierPath.addCurve(to: CGPoint(x: 0, y: -1.9), controlPoint1: CGPoint(x: 3.63, y: -10.17), controlPoint2: CGPoint(x: 0, y: -6.46)) + bezierPath.addLine(to: CGPoint(x: 0, y: 1.81)) + bezierPath.addCurve(to: CGPoint(x: 8.15, y: 10.13), controlPoint1: CGPoint(x: 0, y: 6.37), controlPoint2: CGPoint(x: 3.63, y: 10.08)) + bezierPath.addLine(to: CGPoint(x: 213.18, y: 12.65)) + bezierPath.addCurve(to: CGPoint(x: 215.33, y: 17.36), controlPoint1: CGPoint(x: 215.58, y: 12.68), controlPoint2: CGPoint(x: 216.86, y: 15.5)) + bezierPath.addLine(to: CGPoint(x: 193.82, y: 43.36)) + bezierPath.addCurve(to: CGPoint(x: 194.32, y: 54.58), controlPoint1: CGPoint(x: 191.09, y: 46.67), controlPoint2: CGPoint(x: 191.31, y: 51.53)) + bezierPath.addLine(to: CGPoint(x: 197.29, y: 57.56)) + bezierPath.addCurve(to: CGPoint(x: 208.95, y: 57.56), controlPoint1: CGPoint(x: 200.51, y: 60.81), controlPoint2: CGPoint(x: 205.73, y: 60.81)) + bezierPath.addLine(to: CGPoint(x: 266, y: -0)) + bezierPath.addLine(to: CGPoint(x: 208.95, y: -57.56)) + bezierPath.addCurve(to: CGPoint(x: 197.29, y: -57.56), controlPoint1: CGPoint(x: 205.73, y: -60.81), controlPoint2: CGPoint(x: 200.51, y: -60.81)) + bezierPath.close() + bezierPath.usesEvenOddFillRule = true + + bezierPath.apply(transform(forStartPoint: startPoint, endPoint: endPoint, length: length)) + + return bezierPath + } + + private static func transform(forStartPoint startPoint: CGPoint, endPoint: CGPoint, length: CGFloat) -> CGAffineTransform { + let cosine = (endPoint.x - startPoint.x) / length + let sine = (endPoint.y - startPoint.y) / length + + let scale: CGFloat = length / PaintCodeArrowPathWidth + let scaleTransform = CGAffineTransform(scaleX: scale, y: scale) + + let rotationAndSizeTransform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: startPoint.x, ty: startPoint.y) + return scaleTransform.concatenating(rotationAndSizeTransform) + } +} diff --git a/PinpointKit/PinpointKit/Sources/BlurAnnotationView.swift b/PinpointKit/PinpointKit/Sources/Core/BlurAnnotationView.swift similarity index 73% rename from PinpointKit/PinpointKit/Sources/BlurAnnotationView.swift rename to PinpointKit/PinpointKit/Sources/Core/BlurAnnotationView.swift index 81f0d9d..031ea42 100644 --- a/PinpointKit/PinpointKit/Sources/BlurAnnotationView.swift +++ b/PinpointKit/PinpointKit/Sources/Core/BlurAnnotationView.swift @@ -11,7 +11,7 @@ import GLKit import CoreImage /// The default blur annotation view. -public class BlurAnnotationView: AnnotationView, GLKViewDelegate { +open class BlurAnnotationView: AnnotationView, GLKViewDelegate { // MARK: - Properties @@ -26,7 +26,7 @@ public class BlurAnnotationView: AnnotationView, GLKViewDelegate { let layer = CAShapeLayer() if let annotationFrame = annotationFrame { - layer.path = UIBezierPath(rect: annotationFrame).CGPath + layer.path = UIBezierPath(rect: annotationFrame).cgPath } GLKView.layer.mask = layer @@ -65,87 +65,87 @@ public class BlurAnnotationView: AnnotationView, GLKViewDelegate { public override init(frame: CGRect) { let bounds = CGRect(origin: CGPoint.zero, size: frame.size) - EAGLContext = OpenGLES.EAGLContext(API: .OpenGLES2) + EAGLContext = OpenGLES.EAGLContext(api: .openGLES2) GLKView = GLKit.GLKView(frame: bounds, context: EAGLContext) - CIContext = CoreImage.CIContext(EAGLContext: EAGLContext, options: [ + CIContext = CoreImage.CIContext(eaglContext: EAGLContext, options: [ kCIContextUseSoftwareRenderer: false ]) super.init(frame: frame) - opaque = false + isOpaque = false - GLKView.userInteractionEnabled = false + GLKView.isUserInteractionEnabled = false GLKView.delegate = self - GLKView.contentMode = .Redraw + GLKView.contentMode = .redraw addSubview(GLKView) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - + // MARK: - UIView - override public func layoutSubviews() { + override open func layoutSubviews() { super.layoutSubviews() GLKView.frame = bounds } - override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return touchTargetFrame?.contains(point) ?? false } - override public func drawRect(rect: CGRect) { - super.drawRect(rect) + override open func draw(_ rect: CGRect) { + super.draw(rect) if drawsBorder { - let context = UIGraphicsGetCurrentContext() - tintColor?.colorWithAlphaComponent(self.dynamicType.BorderAlpha).setStroke() + guard let context = UIGraphicsGetCurrentContext() else { return } + + tintColor?.withAlphaComponent(type(of: self).BorderAlpha).setStroke() // Since this draws under the GLKView, and strokes extend both inside and outside, we have to double the intended width. let strokeWidth: CGFloat = 1.0 - CGContextSetLineWidth(context, strokeWidth * 2.0) + context.setLineWidth(strokeWidth * 2.0) let rect = annotationFrame ?? CGRect.zero - CGContextStrokeRect(context, rect) + context.stroke(rect) } } // MARK: - AnnotationView - override func setSecondControlPoint(point: CGPoint) { + override func setSecondControlPoint(_ point: CGPoint) { guard let previousAnnotation = annotation else { return } annotation = BlurAnnotation(startLocation: previousAnnotation.startLocation, endLocation: point, image: previousAnnotation.image) } - override func moveControlPoints(translation: CGPoint) { + override func move(controlPointsBy translationAmount: CGPoint) { guard let previousAnnotation = annotation else { return } - let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translation.x, y: previousAnnotation.startLocation.y + translation.y) - let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translation.x, y: previousAnnotation.endLocation.y + translation.y) + let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translationAmount.x, y: previousAnnotation.startLocation.y + translationAmount.y) + let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translationAmount.x, y: previousAnnotation.endLocation.y + translationAmount.y) annotation = BlurAnnotation(startLocation: startLocation, endLocation: endLocation, image: previousAnnotation.image) } - override func scaleControlPoints(scale: CGFloat) { + override func scale(controlPointsBy scaleFactor: CGFloat) { guard let previousAnnotation = annotation else { return } - let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scale) - let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scale) + let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scaleFactor) + let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scaleFactor) annotation = BlurAnnotation(startLocation: startLocation, endLocation: endLocation, image: previousAnnotation.image) } // MARK: - GLKViewDelegate - public func glkView(view: GLKit.GLKView, drawInRect rect: CGRect) { + open func glkView(_ view: GLKit.GLKView, drawIn rect: CGRect) { glClearColor(0, 0, 0, 0) glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) if let image = annotation?.blurredImage { let drawableRect = CGRect(x: 0, y: 0, width: view.drawableWidth, height: view.drawableHeight) - CIContext.drawImage(image, inRect: drawableRect, fromRect: image.extent) + CIContext.draw(image, in: drawableRect, from: image.extent) } } } diff --git a/PinpointKit/PinpointKit/Sources/Core/BoxAnnotationView.swift b/PinpointKit/PinpointKit/Sources/Core/BoxAnnotationView.swift new file mode 100644 index 0000000..23fd607 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/BoxAnnotationView.swift @@ -0,0 +1,143 @@ +// +// BoxAnnotationView.swift +// Pinpoint +// +// Created by Caleb Davenport on 3/29/15. +// Copyright (c) 2015 Lickability. All rights reserved. +// + +import UIKit + +/// The default box annotation view. +open class BoxAnnotationView: AnnotationView { + + // MARK: - Properties + + /// The corresponding annotation. + var annotation: BoxAnnotation? { + didSet { + if let annotation = annotation { + layer.shadowPath = type(of: self).path(for: annotation)?.cgPath + } else { + layer.shadowPath = nil + } + + setNeedsDisplay() + } + } + + override var annotationFrame: CGRect? { + return annotation?.frame + } + + // MARK: - Initializers + + convenience init() { + self.init(frame: CGRect.zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + isOpaque = false + contentMode = .redraw + + layer.shadowOffset = CGSize.zero + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 1 + layer.shadowRadius = 4 + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UIView + + override open func tintColorDidChange() { + super.tintColorDidChange() + setNeedsDisplay() + } + + override open func draw(_ rect: CGRect) { + guard let annotation = annotation else { return } + + tintColor.setFill() + annotation.strokeColor.setStroke() + + let path = type(of: self).path(for: annotation) + path?.fill() + path?.stroke() + } + + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let annotation = annotation else { return false } + + return type(of: self).path(forPointInside: annotation)?.contains(point) ?? false + } + + // MARK: - AnnotationView + + override func setSecondControlPoint(_ point: CGPoint) { + guard let previousAnnotation = annotation else { return } + + annotation = BoxAnnotation(startLocation: previousAnnotation.startLocation, endLocation: point, strokeColor: previousAnnotation.strokeColor) + } + + override func move(controlPointsBy translationAmount: CGPoint) { + guard let previousAnnotation = annotation else { return } + let startLocation = CGPoint(x: previousAnnotation.startLocation.x + translationAmount.x, y: previousAnnotation.startLocation.y + translationAmount.y) + let endLocation = CGPoint(x: previousAnnotation.endLocation.x + translationAmount.x, y: previousAnnotation.endLocation.y + translationAmount.y) + + annotation = BoxAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) + } + + override func scale(controlPointsBy scaleFactor: CGFloat) { + guard let previousAnnotation = annotation else { return } + let startLocation = previousAnnotation.scaledPoint(previousAnnotation.startLocation, scale: scaleFactor) + let endLocation = previousAnnotation.scaledPoint(previousAnnotation.endLocation, scale: scaleFactor) + + annotation = BoxAnnotation(startLocation: startLocation, endLocation: endLocation, strokeColor: previousAnnotation.strokeColor) + } +} + +private extension BoxAnnotationView { + + static func path(for annotation: BoxAnnotation) -> UIBezierPath? { + let frame = annotation.frame + let strokeWidth = annotation.strokeWidth + let borderWidth = annotation.borderWidth + let cornerRadius = annotation.cornerRadius + + let outerBox = frame.insetBy(dx: strokeWidth, dy: strokeWidth) + let innerBox = outerBox.insetBy(dx: borderWidth + strokeWidth, dy: borderWidth + strokeWidth) + + if min(innerBox.size.height, innerBox.size.width) < (borderWidth + strokeWidth) * 2.0 { + return nil + } + + if min(innerBox.size.height, innerBox.size.width) < cornerRadius * 2.5 { + return nil + } + + let firstPath = CGPath(roundedRect: innerBox, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) + let secondPath = firstPath.copy(strokingWithWidth: borderWidth + strokeWidth, lineCap: .butt, lineJoin: .bevel, miterLimit: 100) + + let path = UIBezierPath(cgPath: secondPath) + path.lineWidth = strokeWidth + path.close() + return path + } + + static func path(forPointInside annotation: BoxAnnotation) -> UIBezierPath? { + let outsideStrokeWidth = annotation.borderWidth * 2.0 + + return path(for: annotation) + .flatMap { path in + path.cgPath.copy(strokingWithWidth: outsideStrokeWidth, lineCap: .butt, lineJoin: .bevel, miterLimit: 0) + } + .map { path in + UIBezierPath(cgPath: path) + } + } +} diff --git a/PinpointKit/PinpointKit/Sources/CheckmarkCell.swift b/PinpointKit/PinpointKit/Sources/Core/CheckmarkCell.swift similarity index 81% rename from PinpointKit/PinpointKit/Sources/CheckmarkCell.swift rename to PinpointKit/PinpointKit/Sources/Core/CheckmarkCell.swift index af9e422..f9e4a09 100644 --- a/PinpointKit/PinpointKit/Sources/CheckmarkCell.swift +++ b/PinpointKit/PinpointKit/Sources/Core/CheckmarkCell.swift @@ -14,14 +14,14 @@ final class CheckmarkCell: UITableViewCell { /// Controls whether the receiver displays a checkmark in `imageView`. var isChecked: Bool = false { didSet { - imageView?.hidden = !isChecked + imageView?.isHidden = !isChecked } } override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - imageView?.image = UIImage(named: "Checkmark", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil) + imageView?.image = UIImage(named: "Checkmark", in: Bundle(for: type(of: self)), compatibleWith: nil) } required init?(coder aDecoder: NSCoder) { diff --git a/PinpointKit/PinpointKit/Sources/Configuration.swift b/PinpointKit/PinpointKit/Sources/Core/Configuration.swift similarity index 73% rename from PinpointKit/PinpointKit/Sources/Configuration.swift rename to PinpointKit/PinpointKit/Sources/Core/Configuration.swift index eeef3f6..e24762e 100644 --- a/PinpointKit/PinpointKit/Sources/Configuration.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Configuration.swift @@ -8,7 +8,6 @@ import UIKit - /// Encapsulates configuration information for the behavior and appearance of PinpointKit. public struct Configuration { @@ -44,14 +43,14 @@ public struct Configuration { /** Initializes a `Configuration` object with optionally customizable appearance and behaviors. - - parameter appearance: A struct containing information about the appearance of displayed components. - - parameter interfaceText: The text to be displayed in the interface. - - parameter logCollector: An optional type that collects logs to be displayed and sent with feedback. - - parameter logViewer: An optional type the shows logs. - - parameter feedbackCollector: A feedback collector that obtains the feedback, by default in the form of annotated screenshots, to send. - - parameter editor: An editor that allows annotation of images. - - parameter sender: A sender that allows sending the feedback outside the framework. - - parameter feedbackRecipients: The recipients of the feedback submission. Suitable for email recipients in the "To:" field. + - parameter appearance: A struct containing information about the appearance of displayed components. + - parameter interfaceText: The text to be displayed in the interface. + - parameter logCollector: An optional type that collects logs to be displayed and sent with feedback. + - parameter logViewer: An optional type the shows logs. + - parameter feedbackCollector: A feedback collector that obtains the feedback, by default in the form of annotated screenshots, to send. + - parameter editor: An editor that allows annotation of images. + - parameter sender: A sender that allows sending the feedback outside the framework. + - parameter feedbackConfiguration: Configuration properties for all feedback to be sent. */ public init(appearance: InterfaceCustomization.Appearance = InterfaceCustomization.Appearance(), interfaceText: InterfaceCustomization.InterfaceText = InterfaceCustomization.InterfaceText(), @@ -60,7 +59,7 @@ public struct Configuration { feedbackCollector: FeedbackCollector = FeedbackNavigationController(), editor: Editor = EditImageViewController(), sender: Sender = MailSender(), - feedbackRecipients: [String]? = nil) { + feedbackConfiguration: FeedbackConfiguration) { self.feedbackCollector = feedbackCollector self.editor = editor @@ -75,6 +74,6 @@ public struct Configuration { self.feedbackCollector.logViewer = logViewer self.feedbackCollector.logViewer?.interfaceCustomization = interfaceCustomization self.feedbackCollector.editor?.interfaceCustomization = interfaceCustomization - self.feedbackCollector.feedbackRecipients = feedbackRecipients + self.feedbackCollector.feedbackConfiguration = feedbackConfiguration } } diff --git a/PinpointKit/PinpointKit/Sources/Editing/AnnotationViewFactory.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/AnnotationViewFactory.swift similarity index 86% rename from PinpointKit/PinpointKit/Sources/Editing/AnnotationViewFactory.swift rename to PinpointKit/PinpointKit/Sources/Core/Editing/AnnotationViewFactory.swift index 8e766ed..b180e90 100644 --- a/PinpointKit/PinpointKit/Sources/Editing/AnnotationViewFactory.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/AnnotationViewFactory.swift @@ -21,6 +21,9 @@ struct AnnotationViewFactory { /// The stroke color of the annotation. let strokeColor: UIColor + /// The text attributes of the annotation. + let textAttributes: [String: AnyObject] + /** Constructs an annotation view. @@ -28,26 +31,27 @@ struct AnnotationViewFactory { */ func annotationView() -> AnnotationView { switch tool { - case .Arrow: + case .arrow: let view = ArrowAnnotationView() view.annotation = ArrowAnnotation(startLocation: currentLocation, endLocation: currentLocation, strokeColor: strokeColor) return view - case .Box: + case .box: let view = BoxAnnotationView() view.annotation = BoxAnnotation(startLocation: currentLocation, endLocation: currentLocation, strokeColor: strokeColor) return view - case .Text: + case .text: let view = TextAnnotationView() + view.textAttributes = textAttributes let minimumSize = view.minimumTextSize let endLocation = CGPoint(x: currentLocation.x + minimumSize.width, y: currentLocation.y + minimumSize.height) view.annotation = Annotation(startLocation: currentLocation, endLocation: endLocation, strokeColor: strokeColor) return view - case .Blur: + case .blur: let view = BlurAnnotationView() view.drawsBorder = true if let image = image { - let CIImage = CoreImage.CIImage(CGImage: image) + let CIImage = CoreImage.CIImage(cgImage: image) view.annotation = BlurAnnotation(startLocation: currentLocation, endLocation: currentLocation, image: CIImage) } diff --git a/PinpointKit/PinpointKit/Sources/Editing/EditImageViewController.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewController.swift similarity index 55% rename from PinpointKit/PinpointKit/Sources/Editing/EditImageViewController.swift rename to PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewController.swift index 6ae2a6b..a367f88 100644 --- a/PinpointKit/PinpointKit/Sources/Editing/EditImageViewController.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewController.swift @@ -7,18 +7,13 @@ // import UIKit -import Photos import CoreImage /// The default view controller responsible for editing an image. -public final class EditImageViewController: UIViewController, UIGestureRecognizerDelegate { - static let TextViewEditingBarAnimationDuration = 0.25 - static let MinimumAnnotationsNeededToPromptBeforeDismissal = 3 +open class EditImageViewController: UIViewController, UIGestureRecognizerDelegate { + private static let TextViewEditingBarAnimationDuration = 0.25 - // Defaults to true since all compositions comes from the Photo Library to start. - private var hasACopyOfCurrentComposition: Bool = true - - private var hasSavedOrSharedAnyComposion: Bool = false + // MARK: - Editor public weak var delegate: EditorDelegate? @@ -26,7 +21,7 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize public var interfaceCustomization: InterfaceCustomization? { didSet { - guard isViewLoaded() else { return } + guard isViewLoaded else { return } updateInterfaceCustomization() } @@ -34,13 +29,31 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize // MARK: - Properties + /// The bar button item provider that specifies the left and right bar button items. + public var barButtonItemProvider: EditImageViewControllerBarButtonItemProviding? { + get { + return barButtonItemProviderBackingStore ?? defaultBarButtonItemProvider + } + set { + barButtonItemProviderBackingStore = newValue + } + } + + private lazy var defaultBarButtonItemProvider: EditImageViewControllerBarButtonItemProviding? = { + guard let interfaceCustomization = self.interfaceCustomization else { assertionFailure(); return nil } + + return DefaultBarButtonItemProvider(interfaceCustomization: interfaceCustomization, rightBarButtonItemTarget: self, rightBarButtonItemSelector: #selector(EditImageViewController.doneButtonTapped(_:))) + }() + + private var barButtonItemProviderBackingStore: EditImageViewControllerBarButtonItemProviding? = nil + private lazy var segmentedControl: UISegmentedControl = { [unowned self] in - let segmentArray = [Tool.Arrow, Tool.Box, Tool.Text, Tool.Blur] + let segmentArray = [Tool.arrow, Tool.box, Tool.text, Tool.blur] let view = UISegmentedControl(items: segmentArray.map { $0.segmentedControlItem }) view.selectedSegmentIndex = 0 - let textToolIndex = segmentArray.indexOf(Tool.Text) + let textToolIndex = segmentArray.index(of: Tool.text) if let index = textToolIndex { let segment = view.subviews[index] @@ -48,30 +61,28 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } for i in 0..= self.dynamicType.MinimumAnnotationsNeededToPromptBeforeDismissal - } - private var createAnnotationPanGestureRecognizer: UIPanGestureRecognizer! = nil private var updateAnnotationPanGestureRecognizer: UIPanGestureRecognizer! = nil private var createOrUpdateAnnotationTapGestureRecognizer: UITapGestureRecognizer! = nil @@ -82,20 +93,11 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize private var previousUpdateAnnotationPanGestureRecognizerLocation: CGPoint! private lazy var keyboardAvoider: KeyboardAvoider? = { - guard let window = UIApplication.sharedApplication().keyWindow else { assertionFailure("PinpointKit did not find a keyWindow."); return nil } + guard let window = UIApplication.shared.keyWindow else { assertionFailure("PinpointKit did not find a keyWindow."); return nil } return KeyboardAvoider(window: window) }() - private lazy var closeBarButtonItem: UIBarButtonItem = { - UIBarButtonItem(image: UIImage(named: "CloseButtonX", inBundle: .pinpointKitBundle(), compatibleWithTraitCollection: nil), landscapeImagePhone: nil, style: .Plain, target: self, action: #selector(EditImageViewController.closeButtonTapped(_:))) - }() - - private lazy var doneBarButtonItem: UIBarButtonItem = { - guard let doneButtonFont = self.interfaceCustomization?.appearance.editorTextAnnotationDoneButtonFont else { assertionFailure(); return UIBarButtonItem() } - return UIBarButtonItem(doneButtonWithTarget: self, font: doneButtonFont, action: #selector(EditImageViewController.doneButtonTapped(_:))) - }() - private var currentTool: Tool? { return Tool(rawValue: segmentedControl.selectedSegmentIndex) } @@ -103,13 +105,15 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize private var currentAnnotationView: AnnotationView? { didSet { if let oldTextAnnotationView = oldValue as? TextAnnotationView { - NSNotificationCenter.defaultCenter().removeObserver(self, name: UITextViewTextDidEndEditingNotification, object: oldTextAnnotationView.textView) + NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidChange, object: oldTextAnnotationView.textView) + NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidEndEditing, object: oldTextAnnotationView.textView) } if let currentTextAnnotationView = currentTextAnnotationView { keyboardAvoider?.triggerViews = [currentTextAnnotationView.textView] - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(EditImageViewController.forceEndEditingTextView), name: UITextViewTextDidEndEditingNotification, object: currentTextAnnotationView.textView) + NotificationCenter.default.addObserver(self, selector: #selector(EditImageViewController.textViewTextDidChange), name: .UITextViewTextDidChange, object: currentTextAnnotationView.textView) + NotificationCenter.default.addObserver(self, selector: #selector(EditImageViewController.forceEndEditingTextView), name: .UITextViewTextDidEndEditing, object: currentTextAnnotationView.textView) } } } @@ -149,8 +153,6 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize annotationsView.isAccessibilityElement = true annotationsView.accessibilityTraits = annotationsView.accessibilityTraits | UIAccessibilityTraitAllowsDirectInteraction - - closeBarButtonItem.accessibilityLabel = "Close" } @available(*, unavailable) @@ -159,7 +161,7 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } deinit { - NSNotificationCenter.defaultCenter().removeObserver(self) + NotificationCenter.default.removeObserver(self) createAnnotationPanGestureRecognizer.delegate = nil updateAnnotationPanGestureRecognizer.delegate = nil createOrUpdateAnnotationTapGestureRecognizer.delegate = nil @@ -169,25 +171,26 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize // MARK: - UIResponder - public override func canBecomeFirstResponder() -> Bool { + open override var canBecomeFirstResponder: Bool { return true } - public override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool { - let textViewIsEditing = currentTextAnnotationView?.textView.isFirstResponder() ?? false + open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + let textViewIsEditing = currentTextAnnotationView?.textView.isFirstResponder ?? false return action == #selector(EditImageViewController.deleteSelectedAnnotationView) && !textViewIsEditing } // MARK: - UIViewController - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() assert(imageView.image != nil, "A screenshot must be set using `setScreenshot(_:)` before loading the view.") - navigationItem.rightBarButtonItem = doneBarButtonItem + navigationItem.leftBarButtonItem = barButtonItemProvider?.leftBarButtonItem + navigationItem.rightBarButtonItem = barButtonItemProvider?.rightBarButtonItem - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = .white view.addSubview(imageView) view.addSubview(annotationsView) @@ -197,7 +200,7 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize doubleTapGestureRecognizer.numberOfTapsRequired = 2 doubleTapGestureRecognizer.delegate = self - createOrUpdateAnnotationTapGestureRecognizer.requireGestureRecognizerToFail(doubleTapGestureRecognizer) + createOrUpdateAnnotationTapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) view.addGestureRecognizer(touchDownGestureRecognizer) view.addGestureRecognizer(doubleTapGestureRecognizer) @@ -221,25 +224,25 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize setupConstraints() } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.hidesBarsOnTap = true navigationController?.setNavigationBarHidden(true, animated: false) } - public override func viewDidAppear(animated: Bool) { + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationController?.setNavigationBarHidden(false, animated: true) } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.hidesBarsOnTap = true } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { if let height = navigationController?.navigationBar.frame.height { var rect = annotationsView.frame rect.origin.y += height @@ -248,35 +251,59 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } } - public override func prefersStatusBarHidden() -> Bool { + open override var prefersStatusBarHidden: Bool { return true } - public override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) { + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { setNeedsStatusBarAppearanceUpdate() } - public override func shouldAutorotate() -> Bool { + open override var shouldAutorotate: Bool { return false } - public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { - return imageIsLandscape() ? .Landscape : [.Portrait, .PortraitUpsideDown] + open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return imageIsLandscape() ? .landscape : [.portrait, .portraitUpsideDown] } - public override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation { - var landscapeOrientation = UIInterfaceOrientation.LandscapeRight - var portraitOrientation = UIInterfaceOrientation.Portrait + open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { + var landscapeOrientation = UIInterfaceOrientation.landscapeRight + var portraitOrientation = UIInterfaceOrientation.portrait - if traitCollection.userInterfaceIdiom == .Pad { - let deviceOrientation = UIDevice.currentDevice().orientation - landscapeOrientation = (deviceOrientation == .LandscapeRight ? .LandscapeLeft : .LandscapeRight) - portraitOrientation = (deviceOrientation == .PortraitUpsideDown ? .PortraitUpsideDown : .Portrait) + if traitCollection.userInterfaceIdiom == .pad { + let deviceOrientation = UIDevice.current.orientation + landscapeOrientation = (deviceOrientation == .landscapeRight ? .landscapeLeft : .landscapeRight) + portraitOrientation = (deviceOrientation == .portraitUpsideDown ? .portraitUpsideDown : .portrait) } return imageIsLandscape() ? landscapeOrientation : portraitOrientation } + // MARK: - EditImageViewController + + /** + Dismisses the receiver if `delegate` is `nil` or returns `true` for `editorShouldDismiss(_:with:)`. If `delegate` returns `true`, `editorWillDismiss(_:with:)` and `editorDidDismiss(_:with:)` will be called before and after, respectively. + + - parameter animated: Whether dismissal is animated. + */ + public func attemptToDismiss(animated: Bool) { + guard let delegate = delegate else { + dismiss(animated: animated, completion: nil) + return + } + + let screenshot = self.view.pinpoint_screenshot + if delegate.editorShouldDismiss(self, with: screenshot) { + delegate.editorWillDismiss(self, with: screenshot) + + dismiss(animated: animated) { [weak self] in + guard let strongSelf = self else { return } + delegate.editorDidDismiss(strongSelf, with: screenshot) + } + } + } + // MARK: - Private private func setupConstraints() { @@ -285,59 +312,31 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize "annotationsView": annotationsView ] - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[imageView]|", options: [], metrics: nil, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[annotationsView]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[imageView]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[annotationsView]|", options: [], metrics: nil, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[imageView]|", options: [], metrics: nil, views: views)) - view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[annotationsView]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[imageView]|", options: [], metrics: nil, views: views)) + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[annotationsView]|", options: [], metrics: nil, views: views)) } - private func newCloseScreenshotAlert() -> UIAlertController { - let alert = UIAlertController(title: nil, message: NSLocalizedString("Your edits to this screenshot will be lost unless you share it or save a copy.", comment: "Alert title for closing a screenshot that has annotations that hasn’t been shared."), preferredStyle: .ActionSheet) - alert.addAction(UIAlertAction(title: NSLocalizedString("Discard", comment: "Alert button title to close a screenshot and discard edits"), style: .Destructive) { action in - self.delegate?.editorWillDismiss(self, screenshot: self.view.pinpoint_screenshot) - self.dismissViewControllerAnimated(true, completion: nil) - }) - - alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Alert button title to cancel the alert."), style: .Cancel, handler: nil)) - return alert + @objc private func doneButtonTapped(_ button: UIBarButtonItem) { + attemptToDismiss(animated: true) } - @objc private func closeButtonTapped(button: UIBarButtonItem) { - guard let image = imageView.image else { assertionFailure(); return } - - if let delegate = self.delegate { - if delegate.editorShouldDismiss(self, screenshot: image) { - delegate.editorWillDismiss(self, screenshot: image) - - dismissViewControllerAnimated(true, completion: nil) - } - } else { - dismissViewControllerAnimated(true, completion: nil) - } - } - - @objc private func doneButtonTapped(button: UIBarButtonItem) { - if let delegate = self.delegate { - if delegate.editorShouldDismiss(self, screenshot: self.view.pinpoint_screenshot) { - self.delegate?.editorWillDismiss(self, screenshot: self.view.pinpoint_screenshot) - - dismissViewControllerAnimated(true, completion: nil) - } - } else { - dismissViewControllerAnimated(true, completion: nil) - } - } - - @objc private func handleTouchDownGestureRecognizer(gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .Began { - let possibleAnnotationView = annotationViewWithGestureRecognizer(gestureRecognizer) + @objc private func handleTouchDownGestureRecognizer(_ gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state == .began { + let possibleAnnotationView = annotationView(with: gestureRecognizer) let annotationViewIsNotBlurView = !(possibleAnnotationView is BlurAnnotationView) if let annotationView = possibleAnnotationView { - annotationsView.bringSubviewToFront(annotationView) + let wasTopmostAnnotationView = (annotationsView.subviews.last == annotationView) + annotationsView.bringSubview(toFront: annotationView) if annotationViewIsNotBlurView { + if !wasTopmostAnnotationView { + informDelegate(of: .broughtToFront) + } + navigationController?.barHideOnTapGestureRecognizer.failRecognizing() } } @@ -359,15 +358,15 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } } - private func annotationViewWithGestureRecognizer(gestureRecognizer: UIGestureRecognizer) -> AnnotationView? { + private func annotationView(with gestureRecognizer: UIGestureRecognizer) -> AnnotationView? { let view = annotationsView if gestureRecognizer is UIPinchGestureRecognizer { var annotationViews: [AnnotationView] = [] - let numberOfTouches = gestureRecognizer.numberOfTouches() + let numberOfTouches = gestureRecognizer.numberOfTouches for index in 0.. AnnotationView? { - let hitView = view.hitTest(location, withEvent: nil) + private func annotationView(in view: UIView, with location: CGPoint) -> AnnotationView? { + let hitView = view.hitTest(location, with: nil) let hitTextView = hitView as? UITextView let hitTextViewSuperview = hitTextView?.superview as? AnnotationView let hitAnnotationView = hitView as? AnnotationView @@ -395,7 +394,7 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize let imagePixelSize = CGSize(width: imageSize.width * imageScale, height: imageSize.height * imageScale) - let portraitPixelSize = UIScreen.mainScreen().portraitPixelSize() + let portraitPixelSize = UIScreen.main.portraitPixelSize return CGFloat(imagePixelSize.width) == portraitPixelSize.height && CGFloat(imagePixelSize.height) == portraitPixelSize.width } @@ -403,12 +402,18 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize guard let currentTextAnnotationView = currentTextAnnotationView else { return } currentTextAnnotationView.beginEditing() - guard let buttonFont = interfaceCustomization?.appearance.editorTextAnnotationDoneButtonFont else { assertionFailure(); return } - let dismissButton = UIBarButtonItem(title: interfaceCustomization?.interfaceText.textEditingDismissButtonTitle, style: .Done, target: self, action: #selector(EditImageViewController.endEditingTextViewIfFirstResponder)) - dismissButton.setTitleTextAttributes([NSFontAttributeName: buttonFont], forState: .Normal) + guard barButtonItemProvider?.hidesBarButtonItemsWhileEditingTextAnnotations == true else { return } - navigationItem.setRightBarButtonItem(dismissButton, animated: true) - navigationItem.setLeftBarButtonItem(nil, animated: true) + guard let buttonFont = interfaceCustomization?.appearance.editorTextAnnotationDismissButtonFont else { assertionFailure(); return } + let dismissButton = UIBarButtonItem(title: interfaceCustomization?.interfaceText.textEditingDismissButtonTitle, style: .done, target: self, action: #selector(EditImageViewController.endEditingTextViewIfFirstResponder)) + dismissButton.setTitleTextAttributes([NSFontAttributeName: buttonFont], for: UIControlState()) + + navigationItem.setRightBarButton(dismissButton, animated: true) + navigationItem.setLeftBarButton(nil, animated: true) + } + + @objc private func textViewTextDidChange() { + informDelegate(of: .textEdited) } @objc private func forceEndEditingTextView() { @@ -419,38 +424,45 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize endEditingTextView(true) } - private func endEditingTextView(checksFirstResponder: Bool = true) { - if let textView = currentTextAnnotationView?.textView where !checksFirstResponder || textView.isFirstResponder() { + private func endEditingTextView(_ checksFirstResponder: Bool = true) { + if let textView = currentTextAnnotationView?.textView, !checksFirstResponder || textView.isFirstResponder { textView.resignFirstResponder() - if !textView.hasText() { + if !textView.hasText && currentTextAnnotationView?.superview != nil { currentTextAnnotationView?.removeFromSuperview() + informDelegate(of: .deleted(animated: false)) } - navigationItem.setRightBarButtonItem(doneBarButtonItem, animated: true) + navigationItem.setLeftBarButton(barButtonItemProvider?.leftBarButtonItem, animated: true) + navigationItem.setRightBarButton(barButtonItemProvider?.rightBarButtonItem, animated: true) currentAnnotationView = nil } } private func handleGestureRecognizerFinished() { - hasACopyOfCurrentComposition = false currentBlurAnnotationView?.drawsBorder = false - let isEditingTextView = currentTextAnnotationView?.textView.isFirstResponder() ?? false + let isEditingTextView = currentTextAnnotationView?.textView.isFirstResponder ?? false currentAnnotationView = isEditingTextView ? currentAnnotationView : nil } - @objc private func toolChanged(segmentedControl: UISegmentedControl) { + @objc private func toolChanged(_ segmentedControl: UISegmentedControl) { + if let tool = currentTool { + delegate?.editor(_editor: self, didSelect: tool) + } + endEditingTextView() // Disable the bar hiding behavior when selecting the text tool. Enable for all others. - navigationController?.barHideOnTapGestureRecognizer.enabled = currentTool != .Text + navigationController?.barHideOnTapGestureRecognizer.isEnabled = currentTool != .text } private func updateInterfaceCustomization() { guard let appearance = interfaceCustomization?.appearance else { assertionFailure(); return } - segmentedControl.setTitleTextAttributes([NSFontAttributeName: appearance.editorTextAnnotationSegmentFont], forState: UIControlState.Normal) - UITextView.appearanceWhenContainedInInstancesOfClasses([TextAnnotationView.self]).font = appearance.editorTextAnnotationFont + segmentedControl.setTitleTextAttributes([NSFontAttributeName: appearance.editorTextAnnotationSegmentFont], for: UIControlState()) + + guard let annotationFont = appearance.annotationTextAttributes[NSFontAttributeName] as? UIFont else { assertionFailure(); return } + UITextView.appearance(whenContainedInInstancesOf: [TextAnnotationView.self]).font = annotationFont if let annotationFillColor = appearance.annotationFillColor { annotationsView.tintColor = annotationFillColor @@ -459,80 +471,95 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize // MARK: - Create annotations - @objc private func handleCreateAnnotationGestureRecognizer(gestureRecognizer: UIPanGestureRecognizer) { + @objc private func handleCreateAnnotationGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { - case .Began: + case .began: handleCreateAnnotationGestureRecognizerBegan(gestureRecognizer) - case .Changed: + case .changed: handleCreateAnnotationGestureRecognizerChanged(gestureRecognizer) - case .Cancelled, .Failed, .Ended: + case .ended: + handleGestureRecognizerFinished() + + // We inform the delegate of text annotation view creation on initial creation. + if !(currentAnnotationView is TextAnnotationView) { + informDelegate(of: .added) + } + case .cancelled, .failed: handleGestureRecognizerFinished() default: break } } - private func handleCreateAnnotationGestureRecognizerBegan(gestureRecognizer: UIGestureRecognizer) { + private func handleCreateAnnotationGestureRecognizerBegan(_ gestureRecognizer: UIGestureRecognizer) { guard let currentTool = currentTool else { return } guard let annotationStrokeColor = interfaceCustomization?.appearance.annotationStrokeColor else { return } + guard let annotationTextAttributes = interfaceCustomization?.appearance.annotationTextAttributes else { return } - let currentLocation = gestureRecognizer.locationInView(annotationsView) + let currentLocation = gestureRecognizer.location(in: annotationsView) - let factory = AnnotationViewFactory(image: imageView.image?.CGImage, currentLocation: currentLocation, tool: currentTool, strokeColor: annotationStrokeColor) + let factory = AnnotationViewFactory(image: imageView.image?.cgImage, currentLocation: currentLocation, tool: currentTool, strokeColor: annotationStrokeColor, textAttributes: annotationTextAttributes) let view: AnnotationView = factory.annotationView() view.frame = annotationsView.bounds - view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] annotationsView.addSubview(view) currentAnnotationView = view - beginEditingTextView() + + if currentAnnotationView is TextAnnotationView { + beginEditingTextView() + informDelegate(of: .added) + } } - private func handleCreateAnnotationGestureRecognizerChanged(gestureRecognizer: UIPanGestureRecognizer) { - let currentLocation = gestureRecognizer.locationInView(annotationsView) + private func handleCreateAnnotationGestureRecognizerChanged(_ gestureRecognizer: UIPanGestureRecognizer) { + let currentLocation = gestureRecognizer.location(in: annotationsView) currentAnnotationView?.setSecondControlPoint(currentLocation) } // MARK: - Update annotations - @objc private func handleUpdateAnnotationGestureRecognizer(gestureRecognizer: UIPanGestureRecognizer) { + @objc private func handleUpdateAnnotationGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { - case .Began: + case .began: handleUpdateAnnotationGestureRecognizerBegan(gestureRecognizer) - case .Changed: + case .changed: handleUpdateAnnotationGestureRecognizerChanged(gestureRecognizer) - case .Cancelled, .Failed, .Ended: + case .ended: + handleGestureRecognizerFinished() + informDelegate(of: .moved) + case .cancelled, .failed: handleGestureRecognizerFinished() default: break } } - private func handleUpdateAnnotationGestureRecognizerBegan(gestureRecognizer: UIPanGestureRecognizer) { - currentAnnotationView = annotationViewWithGestureRecognizer(gestureRecognizer) - previousUpdateAnnotationPanGestureRecognizerLocation = gestureRecognizer.locationInView(gestureRecognizer.view) + private func handleUpdateAnnotationGestureRecognizerBegan(_ gestureRecognizer: UIPanGestureRecognizer) { + currentAnnotationView = annotationView(with: gestureRecognizer) + previousUpdateAnnotationPanGestureRecognizerLocation = gestureRecognizer.location(in: gestureRecognizer.view) currentBlurAnnotationView?.drawsBorder = true - UIMenuController.sharedMenuController().setMenuVisible(false, animated: true) + UIMenuController.shared.setMenuVisible(false, animated: true) currentTextAnnotationView?.textView.selectedRange = NSRange() } - private func handleUpdateAnnotationGestureRecognizerChanged(gestureRecognizer: UIPanGestureRecognizer) { - let currentLocation = gestureRecognizer.locationInView(gestureRecognizer.view) - let previousLocation = previousUpdateAnnotationPanGestureRecognizerLocation + private func handleUpdateAnnotationGestureRecognizerChanged(_ gestureRecognizer: UIPanGestureRecognizer) { + let currentLocation = gestureRecognizer.location(in: gestureRecognizer.view) + let previousLocation: CGPoint = previousUpdateAnnotationPanGestureRecognizerLocation let offset = CGPoint(x: currentLocation.x - previousLocation.x, y: currentLocation.y - previousLocation.y) - currentAnnotationView?.moveControlPoints(offset) - previousUpdateAnnotationPanGestureRecognizerLocation = gestureRecognizer.locationInView(gestureRecognizer.view) + currentAnnotationView?.move(controlPointsBy: offset) + previousUpdateAnnotationPanGestureRecognizerLocation = gestureRecognizer.location(in: gestureRecognizer.view) } - @objc private func handleUpdateAnnotationTapGestureRecognizer(gestureRecognizer: UITapGestureRecognizer) { + @objc private func handleUpdateAnnotationTapGestureRecognizer(_ gestureRecognizer: UITapGestureRecognizer) { switch gestureRecognizer.state { - case .Ended: - if let annotationView = annotationViewWithGestureRecognizer(gestureRecognizer) { + case .ended: + if let annotationView = annotationView(with: gestureRecognizer) { currentAnnotationView = annotationView as? TextAnnotationView beginEditingTextView() - } else if currentTool == .Text { + } else if currentTool == .text { handleCreateAnnotationGestureRecognizerBegan(gestureRecognizer) } default: @@ -540,28 +567,35 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } } - @objc private func handleUpdateAnnotationPinchGestureRecognizer(gestureRecognizer: UIPinchGestureRecognizer) { + @objc private func handleUpdateAnnotationPinchGestureRecognizer(_ gestureRecognizer: UIPinchGestureRecognizer) { switch gestureRecognizer.state { - case .Began: + case .began: handleUpdateAnnotationPinchGestureRecognizerBegan(gestureRecognizer) - case .Changed: + case .changed: handleUpdateAnnotationPinchGestureRecognizerChanged(gestureRecognizer) - case .Cancelled, .Failed, .Ended: + case .ended: + // Ensure we’re actually editing an annotation before notifying the delegate. + // Also, text annotations are not resizable. + if currentTextAnnotationView == nil && currentAnnotationView != nil { + informDelegate(of: .resized) + } + handleGestureRecognizerFinished() + case .cancelled, .failed: handleGestureRecognizerFinished() default: break } } - private func handleUpdateAnnotationPinchGestureRecognizerBegan(gestureRecognizer: UIPinchGestureRecognizer) { - currentAnnotationView = annotationViewWithGestureRecognizer(gestureRecognizer) + private func handleUpdateAnnotationPinchGestureRecognizerBegan(_ gestureRecognizer: UIPinchGestureRecognizer) { + currentAnnotationView = annotationView(with: gestureRecognizer) previousUpdateAnnotationPinchScale = 1 currentBlurAnnotationView?.drawsBorder = true } - private func handleUpdateAnnotationPinchGestureRecognizerChanged(gestureRecognizer: UIPinchGestureRecognizer) { + private func handleUpdateAnnotationPinchGestureRecognizerChanged(_ gestureRecognizer: UIPinchGestureRecognizer) { if previousUpdateAnnotationPinchScale != 0 { - currentAnnotationView?.scaleControlPoints(gestureRecognizer.scale / previousUpdateAnnotationPinchScale) + currentAnnotationView?.scale(controlPointsBy: (gestureRecognizer.scale / previousUpdateAnnotationPinchScale)) } previousUpdateAnnotationPinchScale = gestureRecognizer.scale @@ -569,27 +603,27 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize // MARK: - Delete annotations - @objc private func handleDoubleTapGestureRecognizer(gestureRecognizer: UITapGestureRecognizer) { - if let view = annotationViewWithGestureRecognizer(gestureRecognizer) { + @objc private func handleDoubleTapGestureRecognizer(_ gestureRecognizer: UITapGestureRecognizer) { + if let view = annotationView(with: gestureRecognizer) { deleteAnnotationView(view, animated: true) } } - @objc private func handleLongPressGestureRecognizer(gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state != UIGestureRecognizerState.Began { + @objc private func handleLongPressGestureRecognizer(_ gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state != UIGestureRecognizerState.began { return } - guard let view = annotationViewWithGestureRecognizer(gestureRecognizer) else { return } + guard let view = annotationView(with: gestureRecognizer) else { return } selectedAnnotationView = view becomeFirstResponder() - let point = gestureRecognizer.locationInView(gestureRecognizer.view) + let point = gestureRecognizer.location(in: gestureRecognizer.view) let targetRect = CGRect(origin: point, size: CGSize()) - let controller = UIMenuController.sharedMenuController() - controller.setTargetRect(targetRect, inView: view) + let controller = UIMenuController.shared + controller.setTargetRect(targetRect, in: view) controller.menuItems = [ UIMenuItem(title: "Delete", action: #selector(EditImageViewController.deleteSelectedAnnotationView)) ] @@ -597,19 +631,22 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize controller.setMenuVisible(true, animated: true) } - private func deleteAnnotationView(annotationView: UIView, animated: Bool) { + private func deleteAnnotationView(_ annotationView: UIView, animated: Bool) { let removeAnnotationView = { self.endEditingTextView() annotationView.removeFromSuperview() } if animated { - UIView.performSystemAnimation(.Delete, onViews: [annotationView], options: [], animations: nil) { finished in + informDelegate(of: .deleted(animated: true)) + + UIView.perform(.delete, on: [annotationView], options: [], animations: nil) { finished in removeAnnotationView() } } else { removeAnnotationView() + informDelegate(of: .deleted(animated: false)) } } @@ -619,32 +656,39 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } } + private func informDelegate(of change: AnnotationChange) { + guard let delegate = delegate else { return } + guard let image = imageView.image else { assertionFailure(); return } + + delegate.editor(self, didMake: change, to: image) + } + // MARK: - UIGestureRecognizerDelegate - public func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer == createAnnotationPanGestureRecognizer { - return annotationViewWithGestureRecognizer(gestureRecognizer) == nil + return annotationView(with: gestureRecognizer) == nil } if gestureRecognizer == updateAnnotationPanGestureRecognizer { - return annotationViewWithGestureRecognizer(gestureRecognizer) != nil + return annotationView(with: gestureRecognizer) != nil } if gestureRecognizer == createOrUpdateAnnotationTapGestureRecognizer { - let annotationViewExists = annotationViewWithGestureRecognizer(gestureRecognizer) != nil - return currentTool == .Text ? true : annotationViewExists + let annotationViewExists = annotationView(with: gestureRecognizer) != nil + return currentTool == .text ? true : annotationViewExists } if gestureRecognizer == touchDownGestureRecognizer { return true } - let isEditingText = currentTextAnnotationView?.textView.isFirstResponder() ?? false + let isEditingText = currentTextAnnotationView?.textView.isFirstResponder ?? false return !isEditingText } - public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { let isTouchDown = gestureRecognizer == touchDownGestureRecognizer || otherGestureRecognizer == touchDownGestureRecognizer let isPinch = gestureRecognizer == updateAnnotationPinchGestureRecognizer || otherGestureRecognizer == updateAnnotationPinchGestureRecognizer @@ -653,19 +697,40 @@ public final class EditImageViewController: UIViewController, UIGestureRecognize } extension EditImageViewController: Editor { - public func setScreenshot(screenshot: UIImage) { - let oldScreenshot = imageView.image - - imageView.image = screenshot - - if screenshot != oldScreenshot { - clearAllAnnotations() + + /// The screenshot, without annotations. Note that setting a new image will clear all annotations in the editor. + public var screenshot: UIImage? { + get { + return imageView.image + } + set { + let oldScreenshot = imageView.image + + imageView.image = newValue + + if newValue != oldScreenshot { + clearAllAnnotations() + } } } - private func clearAllAnnotations() { + public var numberOfAnnotations: Int { + return annotationsView.subviews.count + } + + public func clearAllAnnotations() { for annotationView in annotationsView.subviews where annotationView is AnnotationView { annotationView.removeFromSuperview() } } } + +private class DefaultBarButtonItemProvider: EditImageViewControllerBarButtonItemProviding { + public let leftBarButtonItem: UIBarButtonItem? = nil + public let rightBarButtonItem: UIBarButtonItem? + public let hidesBarButtonItemsWhileEditingTextAnnotations = true + + public init(interfaceCustomization: InterfaceCustomization, rightBarButtonItemTarget: AnyObject?, rightBarButtonItemSelector: Selector) { + rightBarButtonItem = UIBarButtonItem(doneButtonWithTarget: rightBarButtonItemTarget, title: interfaceCustomization.interfaceText.editorDoneButtonTitle, font: interfaceCustomization.appearance.editorDoneButtonFont, action: rightBarButtonItemSelector) + } +} diff --git a/PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewControllerBarButtonItemProviding.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewControllerBarButtonItemProviding.swift new file mode 100644 index 0000000..f3ee757 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/EditImageViewControllerBarButtonItemProviding.swift @@ -0,0 +1,22 @@ +// +// EditImageViewControllerBarButtonItemProviding.swift +// Pods +// +// Created by Michael Liberatore on 11/4/16. +// +// + +import UIKit + +/// Describes a type that specifies the bar button items of an `EditImageViewController` and their behaviors. +public protocol EditImageViewControllerBarButtonItemProviding { + + /// The left bar button item. + var leftBarButtonItem: UIBarButtonItem? { get } + + /// The right bar button item. + var rightBarButtonItem: UIBarButtonItem? { get } + + /// Whether the `EditImageViewController` hides the bar button items while editing a text annotation to display its own dismiss button for ending the editing of text. + var hidesBarButtonItemsWhileEditingTextAnnotations: Bool { get } +} diff --git a/PinpointKit/PinpointKit/Sources/Editing/Editor.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/Editor.swift similarity index 69% rename from PinpointKit/PinpointKit/Sources/Editing/Editor.swift rename to PinpointKit/PinpointKit/Sources/Core/Editing/Editor.swift index 3137399..26ffca8 100644 --- a/PinpointKit/PinpointKit/Sources/Editing/Editor.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/Editor.swift @@ -15,12 +15,16 @@ public protocol Editor: class, InterfaceCustomizable { /// The view controller that displays the image being edited. var viewController: UIViewController { get } + /// The number of annotations added to the editor. + var numberOfAnnotations: Int { get } + + /// The screenshot, without annotations. + var screenshot: UIImage? { get set } + /** - Sets the screenshot to be edited. - - - parameter screenshot: The screenshot to be edited. + Removes all annotations added to the editor. */ - func setScreenshot(screenshot: UIImage) + func clearAllAnnotations() } extension Editor where Self: UIViewController { diff --git a/PinpointKit/PinpointKit/Sources/Core/Editing/EditorDelegate.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/EditorDelegate.swift new file mode 100644 index 0000000..cb48b55 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/EditorDelegate.swift @@ -0,0 +1,108 @@ +// +// EditorDelegate.swift +// PinpointKit +// +// Created by Matthew Bischoff on 2/19/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +/// A delegate for the Editor. +public protocol EditorDelegate: class { + + /** + A method that is called when the tool selection changes in the editor. + + - parameter editor: The editor responsible for editing the image. + - parameter tool: The tool that was selected. + + - note: The default implementation of this method does nothing. + */ + func editor(_editor: Editor, didSelect tool: Tool) + + /** + A method that is called any time the editor makes a modification to the screenshot. + + - parameter editor: The editor resonsible for editing the image. + - parameter change: The change that was made to the screenshot. + - parameter screenshot: The edited image of a screenshot. + + - note: The default implementation of this method does nothing. + */ + func editor(_ editor: Editor, didMake change: AnnotationChange, to screenshot: UIImage) + + /** + A method that is called with an image to ask if the editor should be dismissed. + + - parameter editor: The editor resonsible for editing the image. + - parameter screenshot: The edited image of a screenshot, after editing is complete. + + - returns: A bool value that defines if the editor dismisses or not. The default implementation of this method returns `true`. + */ + func editorShouldDismiss(_ editor: Editor, with screenshot: UIImage) -> Bool + + /** + A method that is called with an image just before the editor is dismissed. + + - parameter editor: The editor resonsible for editing the image. + - parameter screenshot: The edited image of a screenshot, after editing is complete. + + - note: The default implementation of this method does nothing. + */ + func editorWillDismiss(_ editor: Editor, with screenshot: UIImage) + + /** + A method that is called with an image just after the editor was dismissed. + + - parameter editor: The editor resonsible for editing the image. + - parameter screenshot: The edited image of a screenshot, after editing is complete. + + - note: The default implementation of this method does nothing. + */ + func editorDidDismiss(_ editor: Editor, with screenshot: UIImage) +} + +/// Extends editor delegate with base implementation for functions. +extension EditorDelegate { + + public func editor(_editor: Editor, didSelect tool: Tool) { + // Do nothing + } + + public func editor(_ editor: Editor, didMake change: AnnotationChange, to screenshot: UIImage) { + // Do nothing + } + + public func editorShouldDismiss(_ editor: Editor, with screenshot: UIImage) -> Bool { + return true + } + + func editorWillDismiss(_ editor: Editor, with screenshot: UIImage) { + // Do nothing + } + + public func editorDidDismiss(_ editor: Editor, with screenshot: UIImage) { + // Do nothing + } +} + +/// Represents a change made using the editor. +public enum AnnotationChange { + + /// An annotation was added. + case added + + /// An annotation was moved. + case moved + + /// An annotation was brought to front. + case broughtToFront + + /// An annotation was resized. + case resized + + /// A text annotation was edited. + case textEdited + + /// An annotation was deleted. `animated` represents whether the deletion is animated. + case deleted(animated: Bool) +} diff --git a/PinpointKit/PinpointKit/Sources/Editing/Tool.swift b/PinpointKit/PinpointKit/Sources/Core/Editing/Tool.swift similarity index 60% rename from PinpointKit/PinpointKit/Sources/Editing/Tool.swift rename to PinpointKit/PinpointKit/Sources/Core/Editing/Tool.swift index a95f33e..718104e 100644 --- a/PinpointKit/PinpointKit/Sources/Editing/Tool.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Editing/Tool.swift @@ -9,48 +9,48 @@ import UIKit /// Represents an editing tool. -enum Tool: Int { +public enum Tool: Int { /// The arrow tool. - case Arrow + case arrow /// The box tool. - case Box + case box /// The text tool. - case Text + case text /// The blur tool. - case Blur + case blur /// The name of the tool. var name: String { switch self { - case .Arrow: + case .arrow: return "Arrow Tool" - case .Box: + case .box: return "Box Tool" - case .Text: + case .text: return "Text Tool" - case .Blur: + case .blur: return "Blur Tool" } } /// The image for the tool. var image: UIImage { - let bundle = NSBundle.pinpointKitBundle() + let bundle = Bundle.pinpointKitBundle() func loadImage() -> UIImage? { switch self { - case .Arrow: - return UIImage(named: "ArrowIcon", inBundle: bundle, compatibleWithTraitCollection: nil) - case .Box: - return UIImage(named: "BoxIcon", inBundle: bundle, compatibleWithTraitCollection: nil) - case .Text: + case .arrow: + return UIImage(named: "ArrowIcon", in: bundle, compatibleWith: nil) + case .box: + return UIImage(named: "BoxIcon", in: bundle, compatibleWith: nil) + case .text: return UIImage() - case .Blur: - return UIImage(named: "BlurIcon", inBundle: bundle, compatibleWithTraitCollection: nil) + case .blur: + return UIImage(named: "BlurIcon", in: bundle, compatibleWith: nil) } } @@ -60,12 +60,12 @@ enum Tool: Int { /// The item to use for a segmented control. var segmentedControlItem: AnyObject { switch self { - case .Arrow, .Box, .Blur: + case .arrow, .box, .blur: let image = self.image image.accessibilityLabel = self.name return image - case .Text: - return NSLocalizedString("Aa", comment: "The text tool’s button label.") + case .text: + return NSLocalizedString("Aa", comment: "The text tool’s button label.") as AnyObject } } } diff --git a/PinpointKit/PinpointKit/Sources/Core/Feedback.swift b/PinpointKit/PinpointKit/Sources/Core/Feedback.swift new file mode 100644 index 0000000..6d6a99d --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/Feedback.swift @@ -0,0 +1,102 @@ +// +// Feedback.swift +// PinpointKit +// +// Created by Matthew Bischoff on 2/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import UIKit + +/// A struct containing user feedback on an application. +public struct Feedback { + + /// An enum with assocated values that represents the screenshot. + public enum ScreenshotType { + /// The original, un-annotated screenshot. + case original(image: UIImage) + + /// An annotated screenshot. + case annotated(image: UIImage) + + /// Both the original and annotated screenshot. + case combined(originalImage: UIImage, annotatedImage: UIImage) + + /// Returns an image of the screenshot preferring the annotated image. + public var preferredImage: UIImage { + switch self { + case let .original(image): + return image + case let .annotated(image): + return image + case let .combined(_, annotatedImage): + return annotatedImage + } + } + } + + /// A substructure containing information about the application and its environment. + public struct ApplicationInformation { + /// The application’s marketing version. + public let version: String? + + /// The application’s build number. + public let build: String? + + /// The application’s display name. + public let name: String? + + /// The application’s bundle identifier. + public let bundleIdentifier: String? + + /// The operating system version of the OS in which the application is running. + public let operatingSystemVersion: OperatingSystemVersion? + + /** + Initializes a new `ApplicationInformation`. + + - parameter version: The application’s marketing version. + - parameter build: The application’s build number. + - parameter name: The application’s display name. + - parameter bundleIdentifier: The application’s bundle identifier. + - parameter operatingSystemVersion: The operating system version of the OS in which the application is running. + */ + public init(version: String?, build: String?, name: String?, bundleIdentifier: String?, operatingSystemVersion: OperatingSystemVersion?) { + self.version = version + self.build = build + self.name = name + self.bundleIdentifier = bundleIdentifier + self.operatingSystemVersion = operatingSystemVersion + } + } + + /// A screenshot of the screen the feedback relates to. + public let screenshot: ScreenshotType + + /// An optional collection of log strings. + public let logs: [String]? + + /// A struct containing information about the application and its environment. + public let applicationInformation: ApplicationInformation? + + /// Specifies configurable properties for feedback. + public var configuration: FeedbackConfiguration + + /** + Initializes a `Feedback` with optional default values. + + - parameter screenshot: The type of screenshot in the feedback. + - parameter logs: The logs to include in the feedback, if any. + - parameter applicationInformation: Information about the application to be captured. + - parameter configuration: Configurable properties for feedback. + */ + public init(screenshot: ScreenshotType, + logs: [String]? = nil, + applicationInformation: ApplicationInformation? = nil, + configuration: FeedbackConfiguration) { + self.screenshot = screenshot + self.logs = logs + self.applicationInformation = applicationInformation + self.configuration = configuration + } +} diff --git a/PinpointKit/PinpointKit/Sources/FeedbackCollector.swift b/PinpointKit/PinpointKit/Sources/Core/FeedbackCollector.swift similarity index 80% rename from PinpointKit/PinpointKit/Sources/FeedbackCollector.swift rename to PinpointKit/PinpointKit/Sources/Core/FeedbackCollector.swift index 21fd974..6d4e3a6 100644 --- a/PinpointKit/PinpointKit/Sources/FeedbackCollector.swift +++ b/PinpointKit/PinpointKit/Sources/Core/FeedbackCollector.swift @@ -12,8 +12,8 @@ public protocol FeedbackCollector: class, LogSupporting, InterfaceCustomizable { /// A delegate that is informed of significant events in feedback collection. weak var feedbackDelegate: FeedbackCollectorDelegate? { get set } - /// The recipients of the feedback submission. Suitable for email recipients in the "To:" field. - var feedbackRecipients: [String]? { get set } + /// Configuration properties for all feedback to be sent. + var feedbackConfiguration: FeedbackConfiguration? { get set } /// The view controller that displays the feedback to collect. var viewController: UIViewController { get } @@ -27,7 +27,7 @@ public protocol FeedbackCollector: class, LogSupporting, InterfaceCustomizable { - parameter screenshot: The screenshot the user will be providing feedback on. - parameter viewController: The view controller from which to present. */ - func collectFeedbackWithScreenshot(screenshot: UIImage, fromViewController viewController: UIViewController) + func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) } extension FeedbackCollector where Self: UIViewController { @@ -45,5 +45,5 @@ public protocol FeedbackCollectorDelegate: class { - parameter feedbackCollector: The collector which collected the feedback. - parameter feedback: The feedback that was collected by the collector. */ - func feedbackCollector(feedbackCollector: FeedbackCollector, didCollectFeedback feedback: Feedback) + func feedbackCollector(_ feedbackCollector: FeedbackCollector, didCollect feedback: Feedback) } diff --git a/PinpointKit/PinpointKit/Sources/Core/FeedbackConfiguration.swift b/PinpointKit/PinpointKit/Sources/Core/FeedbackConfiguration.swift new file mode 100644 index 0000000..02a7d2e --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/FeedbackConfiguration.swift @@ -0,0 +1,56 @@ +// +// FeedbackConfiguration.swift +// Pods +// +// Created by Michael Liberatore on 7/8/16. +// +// + +/// Encapsulates configuration properties for all feedback to be sent. +public struct FeedbackConfiguration { + + /// The value of the default parameter for `title` in the initializer. + public static let DefaultTitle = "Bug Report" + + /// A file name without an extension for the screenshot or annotated screenshot. + public var screenshotFileName: String + + /// The recipients of the feedback submission. Suitable for email recipients in the "To:" field. + public var recipients: [String]? + + /// A short, optional title of the feedback submission. Suitable for an email subject. + public var title: String? + + /// An optional plain-text body of the feedback submission. Suitable for an email body. + public var body: String? + + /// A file name without an extension for the logs text file. + public var logsFileName: String + + /// A dictionary of additional information provided by the application developer. + public var additionalInformation: [String: AnyObject]? + + /** + Initializes a `FeedbackConfiguration` with optional default values. + + - parameter screenshotFileName: The file name of the screenshot. + - parameter recipients: The recipients of the feedback submission. + - parameter title: The title of the feedback. + - parameter body: The default body text. + - parameter logsFileName: The file name of the logs text file. + - parameter additionalInformation: Any additional information you want to capture. + */ + public init(screenshotFileName: String = "Screenshot", + recipients: [String], + title: String? = FeedbackConfiguration.DefaultTitle, + body: String? = nil, + logsFileName: String = "logs", + additionalInformation: [String: AnyObject]? = nil) { + self.screenshotFileName = screenshotFileName + self.recipients = recipients + self.title = title + self.body = body + self.logsFileName = logsFileName + self.additionalInformation = additionalInformation + } +} diff --git a/PinpointKit/PinpointKit/Sources/FeedbackNavigationController.swift b/PinpointKit/PinpointKit/Sources/Core/FeedbackNavigationController.swift similarity index 89% rename from PinpointKit/PinpointKit/Sources/FeedbackNavigationController.swift rename to PinpointKit/PinpointKit/Sources/Core/FeedbackNavigationController.swift index 025db1e..fd218fb 100644 --- a/PinpointKit/PinpointKit/Sources/FeedbackNavigationController.swift +++ b/PinpointKit/PinpointKit/Sources/Core/FeedbackNavigationController.swift @@ -54,12 +54,12 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac } } - public var feedbackRecipients: [String]? { + public var feedbackConfiguration: FeedbackConfiguration? { get { - return feedbackViewController.feedbackRecipients + return feedbackViewController.feedbackConfiguration } set { - feedbackViewController.feedbackRecipients = newValue + feedbackViewController.feedbackConfiguration = newValue } } @@ -90,7 +90,7 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac } @available(*, unavailable) - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { feedbackViewController = FeedbackViewController() super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) @@ -114,7 +114,7 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac // MARK: - FeedbackCollector - public func collectFeedbackWithScreenshot(screenshot: UIImage, fromViewController viewController: UIViewController) { + public func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) { guard presentingViewController == nil else { NSLog("Unable to present FeedbackNavigationController because it is already being presetned") return @@ -123,6 +123,6 @@ public final class FeedbackNavigationController: UINavigationController, Feedbac feedbackViewController.screenshot = screenshot feedbackViewController.annotatedScreenshot = screenshot - viewController.presentViewController(self, animated: true, completion: nil) + viewController.present(self, animated: true, completion: nil) } } diff --git a/PinpointKit/PinpointKit/Sources/Core/FeedbackTableViewDataSource.swift b/PinpointKit/PinpointKit/Sources/Core/FeedbackTableViewDataSource.swift new file mode 100644 index 0000000..5596c5e --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/FeedbackTableViewDataSource.swift @@ -0,0 +1,137 @@ +// +// FeedbackTableViewDataSource.swift +// PinpointKit +// +// Created by Matthew Bischoff on 2/19/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import UIKit + +/// An object conforming to `UITableViewDataSource` that acts as the data source for a `FeedbackViewController`. +final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource { + + private let sections: [Section] + private weak var delegate: FeedbackTableViewDataSourceDelegate? + + /** + Initializes the data source with a configuration and a boolean value indicating whether the user has enabled log collection. + + - parameter interfaceCustomization: The interface customization used to set up the data source. + - parameter screenshot: The screenshot to display for annotating. + - parameter logSupporting: The object the controls the support of logging. + - parameter userEnabledLogCollection: A boolean value indicating whether the user has enabled log collection. + - parameter delegate: The object informed when a screenshot is tapped. + */ + init(interfaceCustomization: InterfaceCustomization, screenshot: UIImage, logSupporting: LogSupporting, userEnabledLogCollection: Bool, delegate: FeedbackTableViewDataSourceDelegate? = nil) { + sections = type(of: self).sectionsFromConfiguration(interfaceCustomization, screenshot: screenshot, logSupporting: logSupporting, userEnabledLogCollection: userEnabledLogCollection) + self.delegate = delegate + } + + private enum Section { + case feedback(rows: [Row]) + + var numberOfRows: Int { + switch self { + case let .feedback(rows): + return rows.count + } + } + } + + private enum Row { + case screenshot(screensot: UIImage, hintText: String?, hintFont: UIFont) + case collectLogs(enabled: Bool, title: String, font: UIFont, canView: Bool) + } + + // MARK: - FeedbackTableViewDataSource + + private static func sectionsFromConfiguration(_ interfaceCustomization: InterfaceCustomization, screenshot: UIImage, logSupporting: LogSupporting, userEnabledLogCollection: Bool) -> [Section] { + var sections: [Section] = [] + + let screenshotRow = Row.screenshot(screensot: screenshot, hintText: interfaceCustomization.interfaceText.feedbackEditHint, hintFont: interfaceCustomization.appearance.feedbackEditHintFont) + let screenshotSection = Section.feedback(rows: [screenshotRow]) + + sections.append(screenshotSection) + + if logSupporting.logCollector != nil { + let collectLogsRow = Row.collectLogs(enabled: userEnabledLogCollection, title: interfaceCustomization.interfaceText.logCollectionPermissionTitle, font: interfaceCustomization.appearance.logCollectionPermissionFont, canView: logSupporting.logViewer != nil) + let collectLogsSection = Section.feedback(rows: [collectLogsRow]) + + sections.append(collectLogsSection) + } + + return sections + } + + private func checkmarkCell(for row: Row) -> CheckmarkCell { + let cell = CheckmarkCell() + + guard case let .collectLogs(enabled, title, font, canView) = row else { + assertionFailure("Found unexpected row type when creating checkmark cell.") + return cell + } + + cell.textLabel?.text = title + cell.textLabel?.font = font + cell.accessoryType = canView ? .detailButton : .none + cell.isChecked = enabled + + return cell + } + + private func screenshotCell(for row: Row) -> ScreenshotCell { + let cell = ScreenshotCell() + + guard case let .screenshot(screenshot, hintText, hintFont) = row else { + assertionFailure("Found unexpected row type when creating screenshot cell.") + return cell + } + + cell.viewModel = ScreenshotCell.ViewModel(screenshot: screenshot, hintText: hintText, hintFont: hintFont) + cell.screenshotButtonTapHandler = { [weak self] button in + guard let strongSelf = self else { return } + + strongSelf.delegate?.feedbackTableViewDataSource(feedbackTableViewDataSource: strongSelf, didTapScreenshot: screenshot) + } + + return cell + } + + // MARK: - UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].numberOfRows + } + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = sections[indexPath.section] + + switch section { + case let .feedback(rows): + let row = rows[indexPath.row] + switch row { + case .screenshot: + return screenshotCell(for: row) + case .collectLogs: + return checkmarkCell(for: row) + } + } + } +} + +/// Delegate protocol describing a type that is informed of screenshot tapping events. +protocol FeedbackTableViewDataSourceDelegate: class { + + /** + Notifies the delegate when a screenshot is tapped. + + - parameter feedbackTableViewDataSource: The feedback table view data source that sent the message. + - parameter screenshot: The screenshot that was tapped. + */ + func feedbackTableViewDataSource(feedbackTableViewDataSource: FeedbackTableViewDataSource, didTapScreenshot screenshot: UIImage) +} diff --git a/PinpointKit/PinpointKit/Sources/Core/FeedbackViewController.swift b/PinpointKit/PinpointKit/Sources/Core/FeedbackViewController.swift new file mode 100644 index 0000000..15ff023 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/FeedbackViewController.swift @@ -0,0 +1,241 @@ +// +// FeedbackViewController.swift +// PinpointKit +// +// Created by Brian Capps on 2/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import UIKit + +/// A `UITableViewController` that conforms to `FeedbackCollector` in order to display an interface that allows the user to see, change, and send feedback. +public final class FeedbackViewController: UITableViewController { + + // MARK: - InterfaceCustomizable + + public var interfaceCustomization: InterfaceCustomization? { + didSet { + guard isViewLoaded else { return } + + updateInterfaceCustomization() + } + } + + // MARK: - LogSupporting + + public var logViewer: LogViewer? + public var logCollector: LogCollector? + public var editor: Editor? + + // MARK: - FeedbackCollector + + public weak var feedbackDelegate: FeedbackCollectorDelegate? + public var feedbackConfiguration: FeedbackConfiguration? + + // MARK: - FeedbackViewController + + /// The screenshot the feedback describes. + public var screenshot: UIImage? { + didSet { + guard isViewLoaded else { return } + updateDataSource() + } + } + + /// The annotated screenshot the feedback describes. + var annotatedScreenshot: UIImage? { + didSet { + guard isViewLoaded else { return } + updateDataSource() + } + } + + private var dataSource: FeedbackTableViewDataSource? { + didSet { + guard isViewLoaded else { return } + tableView.dataSource = dataSource + } + } + + fileprivate var userEnabledLogCollection = true { + didSet { + updateDataSource() + } + } + + public required init() { + super.init(style: .grouped) + } + + @available(*, unavailable) + override init(style: UITableViewStyle) { + super.init(style: .grouped) + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UIViewController + + public override func viewDidLoad() { + super.viewDidLoad() + + tableView.estimatedRowHeight = 100.0 + tableView.rowHeight = UITableViewAutomaticDimension + + // Helps to prevent extra spacing from appearing at the top of the table. + tableView.tableHeaderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: .leastNormalMagnitude)) + tableView.sectionHeaderHeight = .leastNormalMagnitude + + editor?.delegate = self + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + updateInterfaceCustomization() + } + + // MARK: - FeedbackViewController + + private func updateDataSource() { + guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return } + guard let screenshot = screenshot else { assertionFailure(); return } + let screenshotToDisplay = annotatedScreenshot ?? screenshot + + dataSource = FeedbackTableViewDataSource(interfaceCustomization: interfaceCustomization, screenshot: screenshotToDisplay, logSupporting: self, userEnabledLogCollection: userEnabledLogCollection, delegate: self) + } + + private func updateInterfaceCustomization() { + guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return } + let interfaceText = interfaceCustomization.interfaceText + let appearance = interfaceCustomization.appearance + + title = interfaceText.feedbackCollectorTitle + navigationController?.navigationBar.titleTextAttributes = [NSFontAttributeName: appearance.navigationTitleFont] + + let sendBarButtonItem = UIBarButtonItem(title: interfaceText.feedbackSendButtonTitle, style: .done, target: self, action: #selector(FeedbackViewController.sendButtonTapped)) + sendBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackSendButtonFont], for: UIControlState()) + navigationItem.rightBarButtonItem = sendBarButtonItem + + let backBarButtonItem = UIBarButtonItem(title: interfaceText.feedbackBackButtonTitle, style: .plain, target: nil, action: nil) + backBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackBackButtonFont], for: UIControlState()) + navigationItem.backBarButtonItem = backBarButtonItem + + let cancelBarButtonItem: UIBarButtonItem + let cancelAction = #selector(FeedbackViewController.cancelButtonTapped) + if let cancelButtonTitle = interfaceText.feedbackCancelButtonTitle { + cancelBarButtonItem = UIBarButtonItem(title: cancelButtonTitle, style: .plain, target: self, action: cancelAction) + } else { + cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: cancelAction) + } + + cancelBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackCancelButtonFont], for: UIControlState()) + + if presentingViewController != nil { + navigationItem.leftBarButtonItem = cancelBarButtonItem + } else { + navigationItem.leftBarButtonItem = nil + } + + view.tintColor = appearance.tintColor + updateDataSource() + } + + @objc private func sendButtonTapped() { + + guard let feedbackConfiguration = feedbackConfiguration else { + assertionFailure("You must set `feedbackConfiguration` before attempting to send feedback.") + return + } + + let logs = userEnabledLogCollection ? logCollector?.retrieveLogs() : nil + + let feedback: Feedback? + + if let screenshot = annotatedScreenshot { + feedback = Feedback(screenshot: .annotated(image: screenshot), logs: logs, configuration: feedbackConfiguration) + } else if let screenshot = screenshot { + feedback = Feedback(screenshot: .original(image: screenshot), logs: logs, configuration: feedbackConfiguration) + } else { + feedback = nil + } + + guard let feedbackToSend = feedback else { return assertionFailure("We must have either a screenshot or an edited screenshot!") } + + feedbackDelegate?.feedbackCollector(self, didCollect: feedbackToSend) + } + + @objc private func cancelButtonTapped() { + guard presentingViewController != nil else { + assertionFailure("Attempting to dismiss `FeedbackViewController` in unexpected presentation context.") + return + } + + dismiss(animated: true, completion: nil) + } +} + +// MARK: - FeedbackCollector + +extension FeedbackViewController: FeedbackCollector { + public func collectFeedback(with screenshot: UIImage, from viewController: UIViewController) { + self.screenshot = screenshot + annotatedScreenshot = nil + viewController.showDetailViewController(self, sender: viewController) + } +} + +// MARK: - EditorDelegate + +extension FeedbackViewController: EditorDelegate { + public func editorWillDismiss(_ editor: Editor, with screenshot: UIImage) { + annotatedScreenshot = screenshot + } +} + +// MARK: - UITableViewDelegate + +extension FeedbackViewController { + public override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + guard let logCollector = logCollector else { + assertionFailure("No log collector exists.") + return + } + + logViewer?.viewLog(in: logCollector, from: self) + } + + public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + userEnabledLogCollection = !userEnabledLogCollection + tableView.reloadRows(at: [indexPath], with: .automatic) + } + + public override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + + // Only leave space under the last section. + if section == tableView.numberOfSections - 1 { + return tableView.sectionFooterHeight + } + + return .leastNormalMagnitude + } +} + +// MARK: - FeedbackTableViewDataSourceDelegate + +extension FeedbackViewController: FeedbackTableViewDataSourceDelegate { + + func feedbackTableViewDataSource(feedbackTableViewDataSource: FeedbackTableViewDataSource, didTapScreenshot screenshot: UIImage) { + guard let editor = editor else { return } + guard let screenshotToEdit = self.screenshot else { return } + + editor.screenshot = screenshotToEdit + + let editImageViewController = NavigationController(rootViewController: editor.viewController) + editImageViewController.view.tintColor = interfaceCustomization?.appearance.tintColor + present(editImageViewController, animated: true, completion: nil) + } +} diff --git a/PinpointKit/PinpointKit/Sources/Fonts.swift b/PinpointKit/PinpointKit/Sources/Core/Fonts.swift similarity index 65% rename from PinpointKit/PinpointKit/Sources/Fonts.swift rename to PinpointKit/PinpointKit/Sources/Core/Fonts.swift index 40cd95a..4edaf9e 100644 --- a/PinpointKit/PinpointKit/Sources/Fonts.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Fonts.swift @@ -13,13 +13,13 @@ import UIKit public enum FontWeight: Int { /// Regular weight. - case Regular + case regular /// Semibold weight. - case Semibold + case semibold /// Bold weight. - case Bold + case bold } public extension UIFont { @@ -32,23 +32,23 @@ public extension UIFont { - returns: A Source Sans Pro `UIFont` at the specified size and weight. */ - public static func sourceSansProFontOfSize(fontSize: CGFloat, weight: FontWeight = .Regular) -> UIFont { + public static func sourceSansProFont(ofSize fontSize: CGFloat, weight: FontWeight = .regular) -> UIFont { let fontName: String = { switch weight { - case .Regular: + case .regular: return "SourceSansPro-Regular" - case .Semibold: + case .semibold: return "SourceSansPro-Semibold" - case .Bold: + case .bold: return "SourceSansPro-Bold" } }() - if let fontURL = NSBundle.pinpointKitBundle().URLForResource(fontName, withExtension: "ttf") { - CTFontManagerRegisterFontsForURL(fontURL, .Process, nil) + if let fontURL = Bundle.pinpointKitBundle().url(forResource: fontName, withExtension: "ttf") { + CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, nil) } - return UIFont(name: fontName, size: fontSize) ?? UIFont.systemFontOfSize(fontSize) + return UIFont(name: fontName, size: fontSize) ?? .systemFont(ofSize: fontSize) } /** @@ -58,7 +58,7 @@ public extension UIFont { - returns: A `UIFont` representing Menlo Regular at the specified size. */ - public static func menloRegularFontOfSize(fontSize: CGFloat) -> UIFont { - return UIFont(name: "Menlo-Regular", size: fontSize) ?? UIFont.systemFontOfSize(fontSize) + public static func menloRegularFont(ofSize fontSize: CGFloat) -> UIFont { + return UIFont(name: "Menlo-Regular", size: fontSize) ?? .systemFont(ofSize: fontSize) } } diff --git a/PinpointKit/PinpointKit/Sources/InterfaceCustomizable.swift b/PinpointKit/PinpointKit/Sources/Core/InterfaceCustomizable.swift similarity index 100% rename from PinpointKit/PinpointKit/Sources/InterfaceCustomizable.swift rename to PinpointKit/PinpointKit/Sources/Core/InterfaceCustomizable.swift diff --git a/PinpointKit/PinpointKit/Sources/InterfaceCustomization.swift b/PinpointKit/PinpointKit/Sources/Core/InterfaceCustomization.swift similarity index 60% rename from PinpointKit/PinpointKit/Sources/InterfaceCustomization.swift rename to PinpointKit/PinpointKit/Sources/Core/InterfaceCustomization.swift index 3828d84..80e2d04 100644 --- a/PinpointKit/PinpointKit/Sources/InterfaceCustomization.swift +++ b/PinpointKit/PinpointKit/Sources/Core/InterfaceCustomization.swift @@ -33,6 +33,9 @@ public struct InterfaceCustomization { /// The fill color for annotations. If none is supplied, the `tintColor` of the relevant view will be used. let annotationFillColor: UIColor? + /// The text attributes for annotations. Note that `NSForegroundColorAttributeName` can only be customized using `annotationFillColor`. + let annotationTextAttributes: [String: AnyObject] + /// The stroke color for annotations. let annotationStrokeColor: UIColor @@ -60,45 +63,59 @@ public struct InterfaceCustomization { /// The font used for the text annotation tool segment in the editor. let editorTextAnnotationSegmentFont: UIFont - /// The font used for text annotations in the editor. - let editorTextAnnotationFont: UIFont + /// The font used for the dismiss button in the editor displayed while editing a text annotation. + let editorTextAnnotationDismissButtonFont: UIFont - /// The font used for the done button in the editor displayed while editing a text annotation. - let editorTextAnnotationDoneButtonFont: UIFont + /// The font used for the done button in the editor to finish editing the image. + let editorDoneButtonFont: UIFont /** Initializes an `Appearance` object with a optional annotation color properties. - - parameter tintColor: The tint color of the interface. - - parameter annotationFillColor: The fill color for annotations. If none is supplied, the `tintColor` of the relevant view will be used. - - parameter annotationStrokeColor: The stroke color for annotations. - - parameter navigationTitleFont: The font used for navigation titles. - - parameter feedbackSendButtonFont: The font used for the button that sends feedback. - - parameter feedbackCancelButtonFont: The font used for the button that cancels feedback collection. - - parameter feedbackEditHintFont: The font used for the hint to the user on how to edit the screenshot from the feedback screen. - - parameter feedbackBackButtonFont: The font used for the back button that takes the user back to the initial feedback collection screen. - - parameter logCollectionPermissionFont: The font used for the title of the cell that allows the user to toggle log collection. - - parameter logFont: The font used for displaying logs. - - parameter editorTextAnnotationSegmentFont: The font used for the text annotation tool segment in the editor. - - parameter editorTextAnnotationFont: The font used for text annotations in the editor. - - parameter editorTextAnnotationDoneButtonFont: The font used for the done button in the editor displayed while editing a text annotation. + - parameter tintColor: The tint color of the interface. + - parameter annotationFillColor: The fill color for annotations. If none is supplied, the `tintColor` of the relevant view will be used. + - parameter annotationStrokeColor: The stroke color for annotations. + - parameter annotationTextAttributes: The text attributes for annotations. + - parameter navigationTitleFont: The font used for navigation titles. + - parameter feedbackSendButtonFont: The font used for the button that sends feedback. + - parameter feedbackCancelButtonFont: The font used for the button that cancels feedback collection. + - parameter feedbackEditHintFont: The font used for the hint to the user on how to edit the screenshot from the feedback screen. + - parameter feedbackBackButtonFont: The font used for the back button that takes the user back to the initial feedback collection screen. + - parameter logCollectionPermissionFont: The font used for the title of the cell that allows the user to toggle log collection. + - parameter logFont: The font used for displaying logs. + - parameter editorTextAnnotationSegmentFont: The font used for the text annotation tool segment in the editor. + - parameter editorTextAnnotationDismissButtonFont: The font used for the dismiss button in the editor displayed while editing a text annotation. + - parameter editorDoneButtonFont: The font used for the done button in the editor to finish editing the image. */ - public init(tintColor: UIColor? = UIColor.pinpointOrangeColor(), + public init(tintColor: UIColor? = .pinpointOrange(), annotationFillColor: UIColor? = nil, - annotationStrokeColor: UIColor = .whiteColor(), - navigationTitleFont: UIFont = .sourceSansProFontOfSize(19, weight: .Semibold), - feedbackSendButtonFont: UIFont = .sourceSansProFontOfSize(19, weight: .Semibold), - feedbackCancelButtonFont: UIFont = .sourceSansProFontOfSize(19), - feedbackEditHintFont: UIFont = .sourceSansProFontOfSize(14), - feedbackBackButtonFont: UIFont = .sourceSansProFontOfSize(19), - logCollectionPermissionFont: UIFont = .sourceSansProFontOfSize(19), - logFont: UIFont = .menloRegularFontOfSize(10), - editorTextAnnotationSegmentFont: UIFont = .sourceSansProFontOfSize(18), - editorTextAnnotationFont: UIFont = .sourceSansProFontOfSize(32, weight: .Semibold), - editorTextAnnotationDoneButtonFont: UIFont = .sourceSansProFontOfSize(19, weight: .Semibold)) { + annotationStrokeColor: UIColor = .white, + annotationTextAttributes: [String: AnyObject]? = nil, + navigationTitleFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold), + feedbackSendButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold), + feedbackCancelButtonFont: UIFont = .sourceSansProFont(ofSize: 19), + feedbackEditHintFont: UIFont = .sourceSansProFont(ofSize: 14), + feedbackBackButtonFont: UIFont = .sourceSansProFont(ofSize: 19), + logCollectionPermissionFont: UIFont = .sourceSansProFont(ofSize: 19), + logFont: UIFont = .menloRegularFont(ofSize: 10), + editorTextAnnotationSegmentFont: UIFont = .sourceSansProFont(ofSize: 18), + editorTextAnnotationDismissButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold), + editorDoneButtonFont: UIFont = .sourceSansProFont(ofSize: 19, weight: .semibold)) { self.tintColor = tintColor self.annotationFillColor = annotationFillColor self.annotationStrokeColor = annotationStrokeColor + + // Custom annotation text attributes + if var customAnnotationTextAttributes = annotationTextAttributes { + // Ensure annotation font is set, if not use default font + if customAnnotationTextAttributes[NSFontAttributeName] == nil { + customAnnotationTextAttributes[NSFontAttributeName] = type(of: self).DefaultAnnotationTextFont + } + self.annotationTextAttributes = customAnnotationTextAttributes + } else { + self.annotationTextAttributes = type(of: self).defaultTextAnnotationAttributes + } + self.logFont = logFont self.navigationTitleFont = navigationTitleFont self.feedbackSendButtonFont = feedbackSendButtonFont @@ -107,8 +124,8 @@ public struct InterfaceCustomization { self.feedbackBackButtonFont = feedbackBackButtonFont self.logCollectionPermissionFont = logCollectionPermissionFont self.editorTextAnnotationSegmentFont = editorTextAnnotationSegmentFont - self.editorTextAnnotationFont = editorTextAnnotationFont - self.editorTextAnnotationDoneButtonFont = editorTextAnnotationDoneButtonFont + self.editorTextAnnotationDismissButtonFont = editorTextAnnotationDismissButtonFont + self.editorDoneButtonFont = editorDoneButtonFont } } @@ -141,6 +158,9 @@ public struct InterfaceCustomization { /// The title of a button that cancels text editing. let textEditingDismissButtonTitle: String + /// The title of a button that ends editing of the image. + let editorDoneButtonTitle: String + /** Initializes an `InterfaceText` with custom values, using a default if a particular property is unspecified. @@ -152,6 +172,7 @@ public struct InterfaceCustomization { - parameter logCollectorTitle: The title of the log collector. - parameter logCollectionPermissionTitle: The title of the permission button. - parameter textEditingDismissButtonTitle: The title of the text editing dismiss button. + - parameter editorDoneButtonTitle: The title of a button that ends editing of the image. */ public init(feedbackCollectorTitle: String? = NSLocalizedString("Report a Bug", comment: "Title of a view that reports a bug"), feedbackSendButtonTitle: String = NSLocalizedString("Send", comment: "A button that sends feedback."), @@ -160,7 +181,8 @@ public struct InterfaceCustomization { feedbackEditHint: String? = NSLocalizedString("Tap the screenshot to annotate.", comment: "A hint on how to edit the screenshot"), logCollectorTitle: String? = NSLocalizedString("Console Log", comment: "Title of a view that collects logs"), logCollectionPermissionTitle: String = NSLocalizedString("Include Console Log", comment: "Title of a button asking the user to include system logs"), - textEditingDismissButtonTitle: String = NSLocalizedString("Dismiss", comment: "Title of a button that dismisses text editing")) { + textEditingDismissButtonTitle: String = NSLocalizedString("Dismiss", comment: "Title of a button that dismisses text editing"), + editorDoneButtonTitle: String = NSLocalizedString("Done", comment: "Title of a button that finishes editing")) { self.feedbackCollectorTitle = feedbackCollectorTitle self.feedbackSendButtonTitle = feedbackSendButtonTitle self.feedbackCancelButtonTitle = feedbackCancelButtonTitle @@ -169,6 +191,23 @@ public struct InterfaceCustomization { self.logCollectorTitle = logCollectorTitle self.logCollectionPermissionTitle = logCollectionPermissionTitle self.textEditingDismissButtonTitle = textEditingDismissButtonTitle + self.editorDoneButtonTitle = editorDoneButtonTitle } } } + +private extension InterfaceCustomization.Appearance { + + static var defaultTextAnnotationAttributes: [String: AnyObject] { + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = UIColor.black + shadow.shadowOffset = .zero + + return [NSFontAttributeName: DefaultAnnotationTextFont, + NSShadowAttributeName: shadow, + NSKernAttributeName: 1.3 as NSNumber] + } + + static let DefaultAnnotationTextFont = UIFont.sourceSansProFont(ofSize: 32, weight: .semibold) +} diff --git a/PinpointKit/PinpointKit/Sources/KeyboardAvoider.swift b/PinpointKit/PinpointKit/Sources/Core/KeyboardAvoider.swift similarity index 61% rename from PinpointKit/PinpointKit/Sources/KeyboardAvoider.swift rename to PinpointKit/PinpointKit/Sources/Core/KeyboardAvoider.swift index a8ee55b..258319c 100644 --- a/PinpointKit/PinpointKit/Sources/KeyboardAvoider.swift +++ b/PinpointKit/PinpointKit/Sources/Core/KeyboardAvoider.swift @@ -23,18 +23,18 @@ final class KeyboardAvoider { init(window: UIWindow) { self.window = window - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(KeyboardAvoider.keyboardWillChangeFrame(_:)), name: UIKeyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(KeyboardAvoider.keyboardWillChangeFrame(_:)), name: .UIKeyboardWillChangeFrame, object: nil) } deinit { - NSNotificationCenter.defaultCenter().removeObserver(self) + NotificationCenter.default.removeObserver(self) } - @objc private func keyboardWillChangeFrame(notification: NSNotification) { - let frameEndValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue - let animationDurationValue = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSValue + @objc private func keyboardWillChangeFrame(_ notification: Notification) { + let frameEndValue = (notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue + let animationDurationValue = (notification as NSNotification).userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSValue - guard let keyboardEndFrame = frameEndValue?.CGRectValue() else { return } + guard let keyboardEndFrame = frameEndValue?.cgRectValue else { return } let animationDurationNumber = animationDurationValue as? NSNumber let animationDuration = animationDurationNumber?.doubleValue ?? 0.0 @@ -42,7 +42,7 @@ final class KeyboardAvoider { var difference: CGFloat = 0 for triggerView in triggerViews { - let triggerViewFrameInWindow = triggerView.superview?.convertRect(triggerView.frame, toView: window) ?? CGRect.zero + let triggerViewFrameInWindow = triggerView.superview?.convert(triggerView.frame, to: window) ?? CGRect.zero let intersectsKeyboard = triggerViewFrameInWindow.intersects(keyboardEndFrame) let triggerKeyboardDifference = intersectsKeyboard ? triggerViewFrameInWindow.maxY - keyboardEndFrame.minY : 0 @@ -56,43 +56,43 @@ final class KeyboardAvoider { for avoidingView in viewsToAvoidKeyboard { let constraints = avoidingView.superview?.constraints ?? [] - updateAndStoreConstraints(constraints, onView: avoidingView, withDifference: difference, isDismissing: isDismissing) + updateAndStore(constraints, on: avoidingView, withDifference: difference, isDismissing: isDismissing) avoidingView.superview?.layoutIfNeeded() } - UIView.animateWithDuration(animationDuration, animations: {}) { finished in + UIView.animate(withDuration: animationDuration, animations: {}) { finished in if isDismissing { - self.originalConstraintConstants.removeAll(keepCapacity: false) + self.originalConstraintConstants.removeAll(keepingCapacity: false) } } } - private func updateAndStoreConstraints(constraints: [NSLayoutConstraint], onView view: UIView, withDifference difference: CGFloat, isDismissing: Bool) { + private func updateAndStore(_ constraints: [NSLayoutConstraint], on view: UIView, withDifference difference: CGFloat, isDismissing: Bool) { for constraint in constraints { let originalConstant = originalConstraintConstants[constraint] - if let originalConstant = originalConstant where isDismissing { + if let originalConstant = originalConstant, isDismissing { constraint.constant = originalConstant - originalConstraintConstants.removeValueForKey(constraint) + originalConstraintConstants.removeValue(forKey: constraint) - } else if !isDismissing && firstOrSecondItemForConstraint(constraint, isEqualToView: view) { + } else if !isDismissing && firstOrSecondItem(forConstraint: constraint, isEqualTo: view) { // Only replace contraints that don't already exist. if originalConstant == nil { originalConstraintConstants[constraint] = constraint.constant } - if constraint.secondAttribute == .Bottom { + if constraint.secondAttribute == .bottom { constraint.constant += difference - } else if constraint.secondAttribute == .Top { + } else if constraint.secondAttribute == .top { constraint.constant -= difference } } } } - private func firstOrSecondItemForConstraint(constraint: NSLayoutConstraint, isEqualToView view: UIView) -> Bool { + private func firstOrSecondItem(forConstraint constraint: NSLayoutConstraint, isEqualTo view: UIView) -> Bool { return constraint.secondItem as? UIView == view || constraint.firstItem as? UIView == view } } diff --git a/PinpointKit/PinpointKit/Sources/LogCollector.swift b/PinpointKit/PinpointKit/Sources/Core/LogCollector.swift similarity index 100% rename from PinpointKit/PinpointKit/Sources/LogCollector.swift rename to PinpointKit/PinpointKit/Sources/Core/LogCollector.swift diff --git a/PinpointKit/PinpointKit/Sources/LogSupporting.swift b/PinpointKit/PinpointKit/Sources/Core/LogSupporting.swift similarity index 100% rename from PinpointKit/PinpointKit/Sources/LogSupporting.swift rename to PinpointKit/PinpointKit/Sources/Core/LogSupporting.swift diff --git a/PinpointKit/PinpointKit/Sources/LogViewer.swift b/PinpointKit/PinpointKit/Sources/Core/LogViewer.swift similarity index 84% rename from PinpointKit/PinpointKit/Sources/LogViewer.swift rename to PinpointKit/PinpointKit/Sources/Core/LogViewer.swift index bed0a89..bb263a8 100644 --- a/PinpointKit/PinpointKit/Sources/LogViewer.swift +++ b/PinpointKit/PinpointKit/Sources/Core/LogViewer.swift @@ -15,5 +15,5 @@ public protocol LogViewer: InterfaceCustomizable { - parameter collector: The collector which has the logs to view. - parameter viewController: A view controller from which to present an interface for log viewing. */ - func viewLog(collector: LogCollector, fromViewController viewController: UIViewController) + func viewLog(in collector: LogCollector, from viewController: UIViewController) } diff --git a/PinpointKit/PinpointKit/Sources/MIMEType.swift b/PinpointKit/PinpointKit/Sources/Core/MIMEType.swift similarity index 89% rename from PinpointKit/PinpointKit/Sources/MIMEType.swift rename to PinpointKit/PinpointKit/Sources/Core/MIMEType.swift index ec65e41..bcc89ca 100644 --- a/PinpointKit/PinpointKit/Sources/MIMEType.swift +++ b/PinpointKit/PinpointKit/Sources/Core/MIMEType.swift @@ -7,7 +7,7 @@ // /// An enumeration of MIME types used in PinpointKit. -enum MIMEType: String { +public enum MIMEType: String { /// The MIME type used to represent a Portable Network Graphics image. case PNG = "image/png" @@ -19,9 +19,9 @@ enum MIMEType: String { case JSON = "application/json" /// The file extension associated with the MIME type including the leading `.`. - var fileExtension: String { + public var fileExtension: String { switch self { - case PNG: + case .PNG: return ".png" case .JSON: return ".json" diff --git a/PinpointKit/PinpointKit/Sources/Core/MailSender.swift b/PinpointKit/PinpointKit/Sources/Core/MailSender.swift new file mode 100644 index 0000000..42bac8b --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/MailSender.swift @@ -0,0 +1,171 @@ +// +// MailSender.swift +// PinpointKit +// +// Created by Brian Capps on 2/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import MessageUI + +/// A `Sender` that uses `MessageUI` to send an email containing the feedback. +open class MailSender: NSObject, Sender { + + /// An error in sending feedback. + enum Error: Swift.Error { + + /// An unknown error occured. + case unknown + + /// No view controller was provided for presentation. + case noViewControllerProvided + + /// The screenshot failed to encode. + case imageEncoding + + /// The text failed to encode. + case textEncoding + + /// `MFMailComposeViewController.canSendMail()` returned `false`. + case mailCannotSend + + /// Email composing was canceled by the user. + case mailCanceled(underlyingError: Swift.Error?) + + /// Email sending failed. + case mailFailed(underlyingError: Swift.Error?) + } + + /// A success in sending feedback. + enum Success: SuccessType { + + /// The email was saved as a draft. + case saved + + /// The email was sent. + case sent + } + + private var feedback: Feedback? + + // MARK: - Sender + + /// A delegate that is informed of successful or failed feedback sending. + weak open var delegate: SenderDelegate? + + /** + Sends the feedback using the provided view controller as a presenting view controller. + + - parameter feedback: The feedback to send. + - parameter viewController: The view controller from which to present any of the sender’s necessary views. + */ + open func send(_ feedback: Feedback, from viewController: UIViewController?) { + guard let viewController = viewController else { fail(with: .noViewControllerProvided); return } + + guard MFMailComposeViewController.canSendMail() else { fail(with: .mailCannotSend); return } + + let mailComposer = MFMailComposeViewController() + mailComposer.mailComposeDelegate = self + + self.feedback = feedback + + do { + try mailComposer.attach(feedback) + } catch let error as Error { + fail(with: error) + } catch { + fail(with: .unknown) + } + + viewController.present(mailComposer, animated: true, completion: nil) + } + + // MARK: - MailSender + + fileprivate func fail(with error: Error) { + delegate?.sender(self, didFailToSend: feedback, error: error) + feedback = nil + } + + fileprivate func succeed(with success: Success) { + delegate?.sender(self, didSend: feedback, success: success) + feedback = nil + } +} + +private extension MFMailComposeViewController { + + func attach(_ feedback: Feedback) throws { + setToRecipients(feedback.configuration.recipients) + + if let subject = feedback.configuration.title { + setSubject(subject) + } + + if let body = feedback.configuration.body { + setMessageBody(body, isHTML: false) + } + + try attach(feedback.screenshot, screenshotFileName: feedback.configuration.screenshotFileName) + + if let logs = feedback.logs { + try attach(logs, logsFileName: feedback.configuration.logsFileName) + } + + if let additionalInformation = feedback.configuration.additionalInformation { + attach(additionalInformation) + } + } + + func attach(_ screenshot: Feedback.ScreenshotType, screenshotFileName: String) throws { + try attach(screenshot.preferredImage, filename: screenshotFileName + MIMEType.PNG.fileExtension) + } + + func attach(_ logs: [String], logsFileName: String) throws { + let logsText = logs.joined(separator: "\n\n") + try attach(logsText, filename: logsFileName + MIMEType.PlainText.fileExtension) + } + + func attach(_ image: UIImage, filename: String) throws { + guard let PNGData = UIImagePNGRepresentation(image) else { throw MailSender.Error.imageEncoding } + + addAttachmentData(PNGData, mimeType: MIMEType.PNG.rawValue, fileName: filename) + } + + func attach(_ text: String, filename: String) throws { + guard let textData = text.data(using: String.Encoding.utf8) else { throw MailSender.Error.textEncoding } + + addAttachmentData(textData, mimeType: MIMEType.PlainText.rawValue, fileName: filename) + } + + func attach(_ additionalInformation: [String: AnyObject]) { + let data = try? JSONSerialization.data(withJSONObject: additionalInformation, options: .prettyPrinted) + + if let data = data { + addAttachmentData(data, mimeType: MIMEType.JSON.rawValue, fileName: "info.json") + } else { + NSLog("PinpointKit could not attach Feedback.additionalInformation because it was not valid JSON.") + } + } +} + +extension MailSender: MFMailComposeViewControllerDelegate { + public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Swift.Error?) { + controller.dismiss(animated: true) { + self.completeWithResult(result, error: error) + } + } + + private func completeWithResult(_ result: MFMailComposeResult, error: Swift.Error?) { + switch result { + case .cancelled: + fail(with: .mailCanceled(underlyingError: error)) + case .failed: + fail(with: .mailFailed(underlyingError: error)) + case .saved: + succeed(with: .saved) + case .sent: + succeed(with: .sent) + } + } +} diff --git a/PinpointKit/PinpointKit/Sources/NSBundle+PinpointKit.swift b/PinpointKit/PinpointKit/Sources/Core/NSBundle+PinpointKit.swift similarity index 71% rename from PinpointKit/PinpointKit/Sources/NSBundle+PinpointKit.swift rename to PinpointKit/PinpointKit/Sources/Core/NSBundle+PinpointKit.swift index 25159cf..00f1990 100644 --- a/PinpointKit/PinpointKit/Sources/NSBundle+PinpointKit.swift +++ b/PinpointKit/PinpointKit/Sources/Core/NSBundle+PinpointKit.swift @@ -9,14 +9,14 @@ import Foundation /// Extends `NSBundle` to provide bundles from PinpointKit. -extension NSBundle { +extension Bundle { /** The main PinpointKit bundle. - returns: Returns the bundle associated with PinpointKit. */ - static func pinpointKitBundle() -> NSBundle { - return NSBundle(forClass: PinpointKit.self) + static func pinpointKitBundle() -> Bundle { + return Bundle(for: PinpointKit.self) } } diff --git a/PinpointKit/PinpointKit/Sources/NavigationController.swift b/PinpointKit/PinpointKit/Sources/Core/NavigationController.swift similarity index 65% rename from PinpointKit/PinpointKit/Sources/NavigationController.swift rename to PinpointKit/PinpointKit/Sources/Core/NavigationController.swift index 35f1c39..572afc7 100644 --- a/PinpointKit/PinpointKit/Sources/NavigationController.swift +++ b/PinpointKit/PinpointKit/Sources/Core/NavigationController.swift @@ -13,7 +13,7 @@ final class NavigationController: UINavigationController, UINavigationController // MARK: - Initializers - override init(nibName: String?, bundle nibBundle: NSBundle?) { + override init(nibName: String?, bundle nibBundle: Bundle?) { super.init(nibName: nibName, bundle: nibBundle) delegate = self } @@ -21,7 +21,7 @@ final class NavigationController: UINavigationController, UINavigationController override init(rootViewController: UIViewController) { super.init(rootViewController: rootViewController) delegate = self - modalPresentationStyle = .FullScreen // Necessary for proper transition rotation. + modalPresentationStyle = .fullScreen // Necessary for proper transition rotation. modalPresentationCapturesStatusBarAppearance = true } @@ -32,23 +32,23 @@ final class NavigationController: UINavigationController, UINavigationController // MARK: - UIViewController - override func shouldAutorotate() -> Bool { - return topViewController?.shouldAutorotate() ?? false + override var shouldAutorotate: Bool { + return topViewController?.shouldAutorotate ?? false } - override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { - return topViewController?.supportedInterfaceOrientations() ?? .All + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return topViewController?.supportedInterfaceOrientations ?? .all } - override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation { - return topViewController?.preferredInterfaceOrientationForPresentation() ?? .Unknown + override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { + return topViewController?.preferredInterfaceOrientationForPresentation ?? .unknown } - override func childViewControllerForStatusBarHidden() -> UIViewController? { + override var childViewControllerForStatusBarHidden: UIViewController? { return topViewController } - override func childViewControllerForStatusBarStyle() -> UIViewController? { + override var childViewControllerForStatusBarStyle: UIViewController? { return topViewController } } diff --git a/PinpointKit/PinpointKit/Sources/PinpointKit+ShakePresentation.swift b/PinpointKit/PinpointKit/Sources/Core/PinpointKit+ShakePresentation.swift similarity index 88% rename from PinpointKit/PinpointKit/Sources/PinpointKit+ShakePresentation.swift rename to PinpointKit/PinpointKit/Sources/Core/PinpointKit+ShakePresentation.swift index e98b33d..e4823d3 100644 --- a/PinpointKit/PinpointKit/Sources/PinpointKit+ShakePresentation.swift +++ b/PinpointKit/PinpointKit/Sources/Core/PinpointKit+ShakePresentation.swift @@ -14,13 +14,13 @@ extension PinpointKit: ShakeDetectingWindowDelegate { // MARK: - ShakeDetectingWindowDelegate - public func shakeDetectingWindowDidDetectShake(shakeDetectingWindow: ShakeDetectingWindow) { + public func shakeDetectingWindowDidDetectShake(_ shakeDetectingWindow: ShakeDetectingWindow) { guard let viewController = shakeDetectingWindow.rootViewController?.pinpointTopModalViewController() else { NSLog("PinpointPresentingShakeDetectingWindowDelegate couldn't find a root view controller to present on.") return } - show(fromViewController: viewController) + show(from: viewController) } } diff --git a/PinpointKit/PinpointKit/Sources/PinpointKit.h b/PinpointKit/PinpointKit/Sources/Core/PinpointKit.h similarity index 100% rename from PinpointKit/PinpointKit/Sources/PinpointKit.h rename to PinpointKit/PinpointKit/Sources/Core/PinpointKit.h diff --git a/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift b/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift new file mode 100644 index 0000000..847e3b8 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift @@ -0,0 +1,118 @@ +// +// PinpointKit.swift +// PinpointKit +// +// Created by Paul Rehkugler on 1/22/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import Foundation + + +/// `PinpointKit` is an object that can be used to collect feedback from application users. +open class PinpointKit { + + /// The configuration struct that specifies how PinpointKit should be configured. + fileprivate let configuration: Configuration + + /// A delegate that is notified of significant events. + fileprivate weak var delegate: PinpointKitDelegate? + + fileprivate weak var displayingViewController: UIViewController? + + /** + Initializes a `PinpointKit` object with a configuration and an optional delegate. + + - parameter configuration: The configuration struct that specifies how PinpointKit should be configured. + - parameter delegate: A delegate that is notified of significant events. + */ + public init(configuration: Configuration, delegate: PinpointKitDelegate? = nil) { + self.configuration = configuration + self.delegate = delegate + + self.configuration.feedbackCollector.feedbackDelegate = self + self.configuration.sender.delegate = self + } + + /** + Initializes a `PinpointKit` with a default configuration supplied with feedback recipients and an optional delegate. + + - parameter feedbackRecipients: The recipients of the feedback submission. Suitable for email recipients in the "To:" field. + - parameter title: The default title of the feedback. + - parameter body: The default body text of the feedback. + - parameter delegate: A delegate that is notified of significant events. + */ + public convenience init(feedbackRecipients: [String], title: String? = FeedbackConfiguration.DefaultTitle, body: String? = nil, delegate: PinpointKitDelegate? = nil) { + let feedbackConfiguration = FeedbackConfiguration(recipients: feedbackRecipients, title: title, body: body) + let configuration = Configuration(feedbackConfiguration: feedbackConfiguration) + + self.init(configuration: configuration, delegate: delegate) + } + + /** + Shows PinpointKit’s feedback collection UI from a given view controller. + + - parameter viewController: The view controller from which to present. + - parameter screenshot: The screenshot to be annotated. The default value is a screenshot taken at the time this method is called. This image is intended to match the device’s screen size in points. + */ + open func show(from viewController: UIViewController, screenshot: UIImage = Screenshotter.takeScreenshot()) { + displayingViewController = viewController + configuration.editor.clearAllAnnotations() + configuration.feedbackCollector.collectFeedback(with: screenshot, from: viewController) + } +} + +// MARK: - FeedbackCollectorDelegate + +extension PinpointKit: FeedbackCollectorDelegate { + + public func feedbackCollector(_ feedbackCollector: FeedbackCollector, didCollect feedback: Feedback) { + delegate?.pinpointKit(self, willSend: feedback) + configuration.sender.send(feedback, from: feedbackCollector.viewController) + } +} + +// MARK: - SenderDelegate + +extension PinpointKit: SenderDelegate { + + public func sender(_ sender: Sender, didSend feedback: Feedback?, success: SuccessType?) { + guard let feedback = feedback else { return } + + delegate?.pinpointKit(self, didSend: feedback) + displayingViewController?.dismiss(animated: true, completion: nil) + } + + public func sender(_ sender: Sender, didFailToSend feedback: Feedback?, error: Error) { + if case MailSender.Error.mailCanceled = error { return } + + NSLog("An error occurred sending mail: \(error)") + } +} + +/// A protocol describing an object that can be notified of events from PinpointKit. +public protocol PinpointKitDelegate: class { + + /** + Notifies the delegate that PinpointKit is about to send user feedback. + + - parameter pinpointKit: The `PinpointKit` instance responsible for the feedback. + - parameter feedback: The feedback that’s about to be sent. + */ + func pinpointKit(_ pinpointKit: PinpointKit, willSend feedback: Feedback) + + /** + Notifies the delegate that PinpointKit has just sent user feedback. + + - parameter pinpointKit: The `PinpointKit` instance responsible for the feedback. + - parameter feedback: The feedback that’s just been sent. + */ + func pinpointKit(_ pinpointKit: PinpointKit, didSend feedback: Feedback) +} + +/// An extension on PinpointKitDelegate that makes all delegate methods optional by giving them empty implementations by default. +public extension PinpointKitDelegate { + + func pinpointKit(_ pinpointKit: PinpointKit, willSend feedback: Feedback) {} + func pinpointKit(_ pinpointKit: PinpointKit, didSend feedback: Feedback) {} +} diff --git a/PinpointKit/PinpointKit/Sources/Screen.swift b/PinpointKit/PinpointKit/Sources/Core/Screen.swift similarity index 56% rename from PinpointKit/PinpointKit/Sources/Screen.swift rename to PinpointKit/PinpointKit/Sources/Core/Screen.swift index 17a95cf..cd63422 100644 --- a/PinpointKit/PinpointKit/Sources/Screen.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Screen.swift @@ -8,15 +8,16 @@ import UIKit -/// Extends `UIScreen` to identify the pixel size for a screen. +/// Extends `UIScreen` to add convenience properties. extension UIScreen { - /** - Identifies the size of the pixel when the screen is in portrait. - - - returns: The size of a pixel for the screen's portrait dimensions. - */ - func portraitPixelSize() -> CGSize { + /// The height of a single pixel on the screen, in points. + var pixelHeight: CGFloat { + return 1.0 / scale + } + + /// The size of the receiver when in portrait orientation. + var portraitPixelSize: CGSize { let coordinateSpaceBounds = fixedCoordinateSpace.bounds return CGSize(width: coordinateSpaceBounds.width * scale, height: coordinateSpaceBounds.height * scale) diff --git a/PinpointKit/PinpointKit/Sources/Core/ScreenshotCell.swift b/PinpointKit/PinpointKit/Sources/Core/ScreenshotCell.swift new file mode 100644 index 0000000..72224a5 --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/ScreenshotCell.swift @@ -0,0 +1,153 @@ +// +// ScreenshotCell.swift +// PinpointKit +// +// Created by Matthew Bischoff on 2/19/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import UIKit + +/// A view that displays a screenshot and hint text about how to edit it. +class ScreenshotCell: UITableViewCell { + + /// A type of closure that is invoked when a button is tapped. + typealias TapHandler = (_ button: UIButton) -> Void + + /** + * A struct encapsulating the information necessary for this view to be displayed. + */ + struct ViewModel { + let screenshot: UIImage + let hintText: String? + let hintFont: UIFont? + } + + private enum DesignConstants: CGFloat { + case defaultMargin = 15 + case minimumScreenshotPadding = 50 + } + + /// Set the `viewData` in order to update the receiver’s content. + var viewModel: ViewModel? { + didSet { + screenshotButton.setImage(viewModel?.screenshot.withRenderingMode(.alwaysOriginal), for: UIControlState()) + + if let screenshot = viewModel?.screenshot { + screenshotButtonHeightConstraint = screenshotButton.heightAnchor.constraint(equalTo: screenshotButton.widthAnchor, multiplier: 1.0 / screenshot.aspectRatio) + } + + hintLabel.text = viewModel?.hintText + hintLabel.isHidden = viewModel?.hintText == nil || viewModel?.hintText?.isEmpty == true + hintLabel.font = viewModel?.hintFont + } + } + + /// A closure that is invoked when the user taps on the screenshot. + var screenshotButtonTapHandler: TapHandler? + + private let stackView: UIStackView = { + let stackView = UIStackView() + + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 10 + stackView.translatesAutoresizingMaskIntoConstraints = false + + stackView.layoutMargins = UIEdgeInsets(top: DesignConstants.defaultMargin.rawValue, left: DesignConstants.defaultMargin.rawValue, bottom: DesignConstants.defaultMargin.rawValue, right: DesignConstants.defaultMargin.rawValue) + stackView.isLayoutMarginsRelativeArrangement = true + + return stackView + }() + + private lazy var screenshotButton: UIButton = { + let button = UIButton(type: .system) + button.layer.borderColor = self.tintColor.cgColor + button.layer.borderWidth = 1 + + return button + }() + + private let hintLabel: UILabel = { + let label = UILabel() + label.textColor = .lightGray + return label + }() + + private var screenshotButtonHeightConstraint: NSLayoutConstraint? { + didSet { + oldValue?.isActive = false + screenshotButtonHeightConstraint?.isActive = true + } + } + + public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setUp() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + setUp() + } + + // MARK: - UIView + + override func tintColorDidChange() { + super.tintColorDidChange() + + screenshotButton.layer.borderColor = tintColor.cgColor + } + + override func addSubview(_ view: UIView) { + // Prevents the adding of separators to this cell. + let separatorHeight = UIScreen.main.pixelHeight + guard view.frame.height != separatorHeight else { + return + } + + super.addSubview(view) + } + + // MARK: - ScreenshotCell + + private func setUp() { + backgroundColor = .clear + selectionStyle = .none + + addSubview(stackView) + + stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + + stackView.addArrangedSubview(screenshotButton) + stackView.addArrangedSubview(hintLabel) + + setUpScreenshotButton() + } + + private func setUpScreenshotButton() { + screenshotButton.leadingAnchor.constraint(greaterThanOrEqualTo: stackView.leadingAnchor, constant: DesignConstants.minimumScreenshotPadding.rawValue).isActive = true + screenshotButton.trailingAnchor.constraint(lessThanOrEqualTo: stackView.trailingAnchor, constant: -DesignConstants.minimumScreenshotPadding.rawValue).isActive = true + + screenshotButtonHeightConstraint = screenshotButton.heightAnchor.constraint(equalTo: screenshotButton.widthAnchor, multiplier: 1.0) + + screenshotButton.addTarget(self, action: #selector(ScreenshotCell.screenshotButtonTapped(_:)), for: .touchUpInside) + } + + @objc private func screenshotButtonTapped(_ sender: UIButton) { + screenshotButtonTapHandler?(sender) + } +} + +private extension UIImage { + var aspectRatio: CGFloat { + guard size.height > 0 else { return 0 } + + return size.width / size.height + } +} diff --git a/PinpointKit/PinpointKit/Sources/Screenshotter.swift b/PinpointKit/PinpointKit/Sources/Core/Screenshotter.swift similarity index 53% rename from PinpointKit/PinpointKit/Sources/Screenshotter.swift rename to PinpointKit/PinpointKit/Sources/Core/Screenshotter.swift index c4de1ca..2c1e227 100644 --- a/PinpointKit/PinpointKit/Sources/Screenshotter.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Screenshotter.swift @@ -9,26 +9,29 @@ import Foundation /// A class responsible for generating a screenshot image of all windows shown by a `UIApplication` on a given `UIScreen`. -public class Screenshotter { +open class Screenshotter { /** - Takes and returns a screenshot of all of an `application`’s windows displayed on a given screen. + Takes and returns a screenshot of all of an application’s windows displayed on a given screen. - - parameter screen: The screen to determine the screenshot size. - parameter application: The application to screenshot. + - parameter screen: The screen to determine the screenshot size. - returns: A screenshot as a `UIImage`. */ - public static func takeScreenshot(screen: UIScreen = UIScreen.mainScreen(), application: UIApplication = UIApplication.sharedApplication()) -> UIImage { + open static func takeScreenshot(of application: UIApplication = UIApplication.shared, on screen: UIScreen = UIScreen.main) -> UIImage { UIGraphicsBeginImageContextWithOptions(screen.bounds.size, true, 0) application.windows.forEach { window in guard window.screen == screen else { return } - window.drawViewHierarchyInRect(window.bounds, afterScreenUpdates: false) + window.drawHierarchy(in: window.bounds, afterScreenUpdates: false) + } + + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { + preconditionFailure("`UIGraphicsGetImageFromCurrentImageContext()` should never return `nil` as we satisify the requirements of having a bitmap-based current context created with `UIGraphicsBeginImageContextWithOptions(_:_:_:)`") } - let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image diff --git a/PinpointKit/PinpointKit/Sources/Sender.swift b/PinpointKit/PinpointKit/Sources/Core/Sender.swift similarity index 81% rename from PinpointKit/PinpointKit/Sources/Sender.swift rename to PinpointKit/PinpointKit/Sources/Core/Sender.swift index 5387041..064d097 100644 --- a/PinpointKit/PinpointKit/Sources/Sender.swift +++ b/PinpointKit/PinpointKit/Sources/Core/Sender.swift @@ -20,11 +20,12 @@ public protocol Sender: class { - parameter feedback: The feedback to send. - parameter viewController: The view controller from which to present any of the sender’s necessary views. */ - func sendFeedback(feedback: Feedback, fromViewController viewController: UIViewController?) + func send(_ feedback: Feedback, from viewController: UIViewController?) } /// A delegate protocol describing an object that receives success and failure events from a `Sender`. public protocol SenderDelegate: class { + /** Notifies the receiver that the sender successfully sent the feedback with a given type of success. @@ -32,7 +33,7 @@ public protocol SenderDelegate: class { - parameter feedback: The feedback that was sent. - parameter success: The optional type of success. */ - func sender(sender: Sender, didSendFeedback feedback: Feedback?, success: SuccessType?) + func sender(_ sender: Sender, didSend feedback: Feedback?, success: SuccessType?) /** Notifies the receiver that the sender failed to send the feedback with a given error. @@ -41,11 +42,11 @@ public protocol SenderDelegate: class { - parameter feedback: The feedback that failed to send. - parameter error: The error that caused the failure. */ - func sender(sender: Sender, didFailToSendFeedback feedback: Feedback?, error: ErrorType) + func sender(_ sender: Sender, didFailToSend feedback: Feedback?, error: Error) } /// An extension on PinpointKitDelegate that makes some of the delegate methods optional by giving them empty implementations by default. public extension SenderDelegate { - func sender(sender: Sender, didFailToSendFeedback feedback: Feedback?, error: ErrorType) { } + func sender(_ sender: Sender, didFailToSend feedback: Feedback?, error: Error) { } } diff --git a/PinpointKit/PinpointKit/Sources/ShakeDetectingWindow.swift b/PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindow.swift similarity index 69% rename from PinpointKit/PinpointKit/Sources/ShakeDetectingWindow.swift rename to PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindow.swift index 2465a94..41121f8 100644 --- a/PinpointKit/PinpointKit/Sources/ShakeDetectingWindow.swift +++ b/PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindow.swift @@ -10,20 +10,18 @@ import UIKit /// `ShakeDetectingWindow` is a `UIWindow` subclass that notifies a `ShakeDetectingWindowDelegate` any time a shake motion event occurs. -public class ShakeDetectingWindow: UIWindow { +open class ShakeDetectingWindow: UIWindow { /// A `ShakeDetectingWindowDelegate` to notify when a shake motion event occurs. - public weak var delegate: ShakeDetectingWindowDelegate? + open weak var delegate: ShakeDetectingWindowDelegate? /** Initializes a `ShakeDetectingWindow`. - parameter frame: The frame rectangle for the view. - - parameter delegate: An object to notify when a shake motion event occurs. Defaults to `PinpointKit.defaultPinpointKit`. + - parameter delegate: An object to notify when a shake motion event occurs. */ - required public init( - frame: CGRect, - delegate: ShakeDetectingWindowDelegate = PinpointKit.defaultPinpointKit) { + required public init(frame: CGRect, delegate: ShakeDetectingWindowDelegate) { self.delegate = delegate super.init(frame: frame) } @@ -32,13 +30,12 @@ public class ShakeDetectingWindow: UIWindow { required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - delegate = PinpointKit.defaultPinpointKit } // MARK: - UIResponder - override public func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) { - if motion == .MotionShake { + override open func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) { + if motion == .motionShake { guard let delegate = delegate else { NSLog(#file + "- There is no ShakeDetectingWindowDelegate registered to handle this shake.") return diff --git a/PinpointKit/PinpointKit/Sources/ShakeDetectingWindowDelegate.swift b/PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindowDelegate.swift similarity index 86% rename from PinpointKit/PinpointKit/Sources/ShakeDetectingWindowDelegate.swift rename to PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindowDelegate.swift index ed2aeec..29c3f0d 100644 --- a/PinpointKit/PinpointKit/Sources/ShakeDetectingWindowDelegate.swift +++ b/PinpointKit/PinpointKit/Sources/Core/ShakeDetectingWindowDelegate.swift @@ -17,5 +17,5 @@ public protocol ShakeDetectingWindowDelegate: class { - parameter shakeDetectingWindow: The `ShakeDetectingWindow` in which the shake motion event occurred. */ - func shakeDetectingWindowDidDetectShake(shakeDetectingWindow: ShakeDetectingWindow) + func shakeDetectingWindowDidDetectShake(_ shakeDetectingWindow: ShakeDetectingWindow) } diff --git a/PinpointKit/PinpointKit/Sources/SourceSansPro-Bold.ttf b/PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Bold.ttf similarity index 100% rename from PinpointKit/PinpointKit/Sources/SourceSansPro-Bold.ttf rename to PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Bold.ttf diff --git a/PinpointKit/PinpointKit/Sources/SourceSansPro-Regular.ttf b/PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Regular.ttf similarity index 100% rename from PinpointKit/PinpointKit/Sources/SourceSansPro-Regular.ttf rename to PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Regular.ttf diff --git a/PinpointKit/PinpointKit/Sources/SourceSansPro-Semibold.ttf b/PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Semibold.ttf similarity index 100% rename from PinpointKit/PinpointKit/Sources/SourceSansPro-Semibold.ttf rename to PinpointKit/PinpointKit/Sources/Core/SourceSansPro-Semibold.ttf diff --git a/PinpointKit/PinpointKit/Sources/Core/StrokeLayoutManager.swift b/PinpointKit/PinpointKit/Sources/Core/StrokeLayoutManager.swift new file mode 100644 index 0000000..beb172e --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/StrokeLayoutManager.swift @@ -0,0 +1,68 @@ +// +// StrokeLayoutManager.swift +// Pinpoint +// +// Created by Brian Capps on 4/23/15. +// Copyright (c) 2015 Lickability. All rights reserved. +// + +import UIKit + +/// A subclass of `NSLayoutManager` that handles drawing the stroke. +final class StrokeLayoutManager: NSLayoutManager { + + /// The color to display as a stroke around the text. + var strokeColor: UIColor? + + /// The width of the stroke to display around the text. + var strokeWidth: CGFloat? + + override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { + let context = UIGraphicsGetCurrentContext() + + let firstIndex = characterIndexForGlyph(at: glyphsToShow.location) + let attributes = textStorage?.attributes(at: firstIndex, effectiveRange: nil) + let shadow = attributes?[NSShadowAttributeName] as? NSShadow + let shouldRenderTransparencyLayer = strokeColor != nil && strokeWidth != nil && shadow != nil + + if let shadow = shadow, shouldRenderTransparencyLayer { + // Applies the shadow to the entire stroke as one layer, insead of overlapping per-character. + context?.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: (shadow.shadowColor as? UIColor)?.cgColor) + context?.beginTransparencyLayer(auxiliaryInfo: nil) + } + + super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) + + if shouldRenderTransparencyLayer { + context?.endTransparencyLayer() + } + } + + override func showCGGlyphs(_ glyphs: UnsafePointer, positions: UnsafePointer, count glyphCount: Int, font: UIFont, matrix textMatrix: CGAffineTransform, attributes: [String : Any], in graphicsContext: CGContext) { + var textAttributes = attributes + + if let strokeColor = strokeColor, let strokeWidth = strokeWidth { + // Remove the shadow. It'll all be drawn at once afterwards. + textAttributes[NSShadowAttributeName] = nil + graphicsContext.setShadow(offset: CGSize.zero, blur: 0, color: nil) + + graphicsContext.saveGState() + + strokeColor.setStroke() + + graphicsContext.setLineWidth(strokeWidth) + graphicsContext.setLineJoin(.miter) + + graphicsContext.setTextDrawingMode(.fillStroke) + + super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: textAttributes, in: graphicsContext) + + // Due to a bug introduced in iOS 7, kCGTextFillStroke will never have the correct fill color, so we must draw the string twice: once for stroke and once for fill. http://stackoverflow.com/questions/18894907/why-cgcontextsetrgbstrokecolor-isnt-working-on-ios7 + + graphicsContext.restoreGState() + graphicsContext.setTextDrawingMode(.fill) + } + + super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: textAttributes, in: graphicsContext) + } +} diff --git a/PinpointKit/PinpointKit/Sources/SuccessType.swift b/PinpointKit/PinpointKit/Sources/Core/SuccessType.swift similarity index 100% rename from PinpointKit/PinpointKit/Sources/SuccessType.swift rename to PinpointKit/PinpointKit/Sources/Core/SuccessType.swift diff --git a/PinpointKit/PinpointKit/Sources/Core/SystemLogCollector.swift b/PinpointKit/PinpointKit/Sources/Core/SystemLogCollector.swift new file mode 100644 index 0000000..7214c0d --- /dev/null +++ b/PinpointKit/PinpointKit/Sources/Core/SystemLogCollector.swift @@ -0,0 +1,56 @@ +// +// SystemLogCollector.swift +// PinpointKit +// +// Created by Brian Capps on 2/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +/// A log collector that uses [Apple System Logger](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/LoggingErrorsAndWarnings.html) API to retrieve messages logged to the console with `NSLog`. +open class SystemLogCollector: LogCollector { + + /** + The type of logs to collect. + */ + public enum LoggingType { + /// Logs from the application target. + case application + + /// Logs from the testing target. + case testing + } + + private let logger: ASLLogger + + /** + Creates a new system logger. + + - parameter loggingType: Specifies the type of logs to collect. + + - warning: This initializer returns `nil` on iOS 10.0+. When running on iOS 10.0+, ASL is superseded by unified logging, for which there are no APIs to search or read log messages. + - seealso: https://developer.apple.com/reference/os/logging + */ + public init?(loggingType: LoggingType = .application) { + if #available(iOS 10.0, *), loggingType == .application { + return nil + } + + switch loggingType { + case .application: + logger = ASLLogger(bundleIdentifier: Bundle.main.bundleIdentifier ?? "") + case .testing: + logger = ASLLogger(senderName: "xctest") + } + } + + // MARK: - LogCollector + + /** + Retrieves and returns logs as an ordered list of strings. + + - returns: Logs as an ordered list of strings, sorted by descending recency. + */ + open func retrieveLogs() -> [String] { + return logger.retrieveLogs() + } +} diff --git a/PinpointKit/PinpointKit/Sources/TextAnnotationView.swift b/PinpointKit/PinpointKit/Sources/Core/TextAnnotationView.swift similarity index 72% rename from PinpointKit/PinpointKit/Sources/TextAnnotationView.swift rename to PinpointKit/PinpointKit/Sources/Core/TextAnnotationView.swift index 33fb3e4..6d0ce95 100644 --- a/PinpointKit/PinpointKit/Sources/TextAnnotationView.swift +++ b/PinpointKit/PinpointKit/Sources/Core/TextAnnotationView.swift @@ -9,7 +9,7 @@ import UIKit /// The default text annotation view. -public class TextAnnotationView: AnnotationView, UITextViewDelegate { +open class TextAnnotationView: AnnotationView, UITextViewDelegate { private static let TextViewInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) private static let TextViewLineFragmentPadding: CGFloat = 5.0 @@ -19,7 +19,7 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { let manager = StrokeLayoutManager() manager.strokeWidth = 4.5 - let container = NSTextContainer(size: CGSize(width: 0, height: CGFloat.max)) + let container = NSTextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude)) container.widthTracksTextView = true manager.addTextContainer(container) @@ -27,8 +27,8 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { let textView = UITextView(frame: CGRect.zero, textContainer: container) - textView.spellCheckingType = .No - textView.scrollEnabled = false + textView.spellCheckingType = .no + textView.isScrollEnabled = false textView.backgroundColor = UIColor(white: 1.0, alpha: 0.3) textView.textContainerInset = TextViewInset textView.textContainer.lineFragmentPadding = TextViewLineFragmentPadding @@ -66,7 +66,7 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { // MARK: - UIView - override public func tintColorDidChange() { + override open func tintColorDidChange() { textView.typingAttributes = { var attributes = self.textView.typingAttributes attributes[NSForegroundColorAttributeName] = self.tintColor @@ -74,16 +74,16 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { }() } - override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return annotationFrame?.contains(point) ?? false } // MARK: - AnnotationView - override func moveControlPoints(translation: CGPoint) { + override func move(controlPointsBy translationAmount: CGPoint) { textView.frame = { var textViewFrame = self.textView.frame - textViewFrame.origin = CGPoint(x: textViewFrame.minX + translation.x, y: textViewFrame.minY + translation.y) + textViewFrame.origin = CGPoint(x: textViewFrame.minX + translationAmount.x, y: textViewFrame.minY + translationAmount.y) return textViewFrame }() } @@ -91,22 +91,15 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { // MARK: - TextAnnotationView /// The attributes of the text to use for an `NSAttributedString`. - var textAttributes: [String: AnyObject] { - let shadow = NSShadow() - shadow.shadowBlurRadius = 5 - shadow.shadowColor = UIColor(white: 0.0, alpha: 1.0) - shadow.shadowOffset = CGSize.zero - - return [ - NSFontAttributeName: font, - NSForegroundColorAttributeName: tintColor, - NSShadowAttributeName: shadow, - NSKernAttributeName: 1.3 - ] + var textAttributes: [String: AnyObject] = [:] { + didSet { + textAttributes[NSFontAttributeName] = font + textView.typingAttributes = textAttributes + } } private var font: UIFont { - return UITextView.appearanceWhenContainedInInstancesOfClasses([TextAnnotationView.self]).font ?? UIFont.systemFontOfSize(32) + return UITextView.appearance(whenContainedInInstancesOf: [TextAnnotationView.self]).font ?? .systemFont(ofSize: 32) } /// The minimum text size for the annotation view. @@ -115,14 +108,14 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { let character = "." as NSString let textFont = textAttributes[NSFontAttributeName] ?? font - let size = character.boundingRectWithSize(CGSize(width: width, height: CGFloat.max), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: textFont], context: nil) + let size = character.boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: textFont], context: nil) return CGSize(width: width, height: size.height + TextAnnotationView.TextViewInset.top + TextAnnotationView.TextViewInset.bottom + TextAnnotationView.TextViewLineFragmentPadding) } private func updateTextViewFrame() { textView.frame = { var textViewFrame = self.textView.frame - textViewFrame.size = self.textView.intrinsicContentSize() + textViewFrame.size = self.textView.intrinsicContentSize let distanceToEdgeOfView = self.bounds.maxX - textViewFrame.minX textViewFrame.size.width = min(textViewFrame.width, distanceToEdgeOfView) @@ -136,7 +129,7 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { minHeight = minimumTextSize.height } - let size = CGSize(width: textViewFrame.width, height: CGFloat.max) + let size = CGSize(width: textViewFrame.width, height: CGFloat.greatestFiniteMagnitude) textViewFrame.size.height = max(textView.sizeThatFits(size).height, minHeight) @@ -148,28 +141,28 @@ public class TextAnnotationView: AnnotationView, UITextViewDelegate { Tells the internal text view to begin editing. */ func beginEditing() { - textView.selectable = true - textView.editable = true + textView.isSelectable = true + textView.isEditable = true textView.becomeFirstResponder() } // MARK: - UITextViewDelegate - public func textViewDidChange(textView: UITextView) { + open func textViewDidChange(_ textView: UITextView) { updateTextViewFrame() } - public func textViewDidBeginEditing(textView: UITextView) { + open func textViewDidBeginEditing(_ textView: UITextView) { textView.backgroundColor = UIColor(white: 1.0, alpha: 0.3) textView.layer.borderWidth = 1 - textView.layer.borderColor = tintColor.colorWithAlphaComponent(self.dynamicType.BorderAlpha).CGColor + textView.layer.borderColor = tintColor.withAlphaComponent(type(of: self).BorderAlpha).cgColor } - public func textViewDidEndEditing(textView: UITextView) { - textView.selectable = false - textView.editable = false + open func textViewDidEndEditing(_ textView: UITextView) { + textView.isSelectable = false + textView.isEditable = false - textView.backgroundColor = UIColor.clearColor() + textView.backgroundColor = .clear textView.layer.borderWidth = 0 textView.layer.borderColor = nil } diff --git a/PinpointKit/PinpointKit/Sources/UIColor+Palette.swift b/PinpointKit/PinpointKit/Sources/Core/UIColor+Palette.swift similarity index 89% rename from PinpointKit/PinpointKit/Sources/UIColor+Palette.swift rename to PinpointKit/PinpointKit/Sources/Core/UIColor+Palette.swift index fc0f43c..df3c96e 100644 --- a/PinpointKit/PinpointKit/Sources/UIColor+Palette.swift +++ b/PinpointKit/PinpointKit/Sources/Core/UIColor+Palette.swift @@ -16,7 +16,7 @@ public extension UIColor { - returns: The Pinpoint specific orange color. */ - static func pinpointOrangeColor() -> UIColor { + static func pinpointOrange() -> UIColor { return UIColor(red: 1, green: 0.2196, blue: 0.0392, alpha: 1) } } diff --git a/PinpointKit/PinpointKit/Sources/UIGestureRecognizer+FailRecognizing.swift b/PinpointKit/PinpointKit/Sources/Core/UIGestureRecognizer+FailRecognizing.swift similarity index 87% rename from PinpointKit/PinpointKit/Sources/UIGestureRecognizer+FailRecognizing.swift rename to PinpointKit/PinpointKit/Sources/Core/UIGestureRecognizer+FailRecognizing.swift index e207a10..cf2a150 100644 --- a/PinpointKit/PinpointKit/Sources/UIGestureRecognizer+FailRecognizing.swift +++ b/PinpointKit/PinpointKit/Sources/Core/UIGestureRecognizer+FailRecognizing.swift @@ -15,12 +15,12 @@ extension UIGestureRecognizer { Function that forces a gesture recognizer to fail */ func failRecognizing() { - if !enabled { + if !isEnabled { return } // Disabling and enabling causes recognizing to fail. - enabled = false - enabled = true + isEnabled = false + isEnabled = true } } diff --git a/PinpointKit/PinpointKit/Sources/UIView+PinpointKit.swift b/PinpointKit/PinpointKit/Sources/Core/UIView+PinpointKit.swift similarity index 50% rename from PinpointKit/PinpointKit/Sources/UIView+PinpointKit.swift rename to PinpointKit/PinpointKit/Sources/Core/UIView+PinpointKit.swift index 58afeb2..f716e48 100644 --- a/PinpointKit/PinpointKit/Sources/UIView+PinpointKit.swift +++ b/PinpointKit/PinpointKit/Sources/Core/UIView+PinpointKit.swift @@ -12,9 +12,14 @@ extension UIView { /// The UIImage representation of this view at the time of access. var pinpoint_screenshot: UIImage { UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0) - drawViewHierarchyInRect(bounds, afterScreenUpdates: true) - let image = UIGraphicsGetImageFromCurrentImageContext() + drawHierarchy(in: bounds, afterScreenUpdates: true) + + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { + preconditionFailure("`UIGraphicsGetImageFromCurrentImageContext()` should never return `nil` as we satisify the requirements of having a bitmap-based current context created with `UIGraphicsBeginImageContextWithOptions(_:_:_:)`") + } + UIGraphicsEndImageContext() + return image } } diff --git a/PinpointKit/PinpointKit/Sources/Editing/EditorDelegate.swift b/PinpointKit/PinpointKit/Sources/Editing/EditorDelegate.swift deleted file mode 100644 index a233f9e..0000000 --- a/PinpointKit/PinpointKit/Sources/Editing/EditorDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// EditorDelegate.swift -// PinpointKit -// -// Created by Matthew Bischoff on 2/19/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -/// A delegate for the Editor. -public protocol EditorDelegate: class { - - /** - A function that is called with an image just before the editor is dismissed. - - - parameter editor: The editor resonsible for editing the image. - - parameter screenshot: The edited image of a screenshot, after editing is complete. - */ - func editorWillDismiss(editor: Editor, screenshot: UIImage) - - /** - A function that is called with an image to ask if the editor should be dismissed. - - - parameter editor: The editor resonsible for editing the image. - - parameter screenshot: The edited image of a screenshot, after editing is complete. - - - returns: A bool value that defines if the editor dismisses or not. - */ - func editorShouldDismiss(editor: Editor, screenshot: UIImage) -> Bool -} - -/// Extends editor delegate with base implementation for functions. -extension EditorDelegate { - public func editorShouldDismiss(editor: Editor, screenshot: UIImage) -> Bool { - return true - } -} diff --git a/PinpointKit/PinpointKit/Sources/Feedback.swift b/PinpointKit/PinpointKit/Sources/Feedback.swift deleted file mode 100644 index 8f4933f..0000000 --- a/PinpointKit/PinpointKit/Sources/Feedback.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// Feedback.swift -// PinpointKit -// -// Created by Matthew Bischoff on 2/5/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import UIKit - -/// A struct containing user feedback on an application. -public struct Feedback { - - /// An enum with assocated values that represents the screenshot. - enum ScreenshotType { - /// The original, un-annotated screenshot. - case Original(image: UIImage) - - /// An annotated screenshot. - case Annotated(image: UIImage) - - /// Both the original and annotated screenshot. - case Combined(originalImage: UIImage, annotatedImage: UIImage) - - /// Returns an image of the screenshot preferring the annotated image. - var preferredImage: UIImage { - switch self { - case let Original(image): - return image - case let Annotated(image): - return image - case let Combined(_, annotatedImage): - return annotatedImage - } - } - } - - /// A substructure containing information about the application and its environment. - struct ApplicationInformation { - /// The application’s marketing version. - let version: String? - - /// The application’s build number. - let build: String? - - /// The application’s display name. - let name: String? - - /// The application’s bundle identifer. - let bundleIdentifer: String? - - /// The operating system version of the OS in which the application is running. - let operatingSystemVersion: NSOperatingSystemVersion? - } - - /// A screenshot of the screen the feedback relates to. - let screenshot: ScreenshotType - - /// A file name without an extension for the screenshot or annotated screenshot. - let screenshotFileName: String - - /// The recipients of the feedback submission. Suitable for email recipients in the "To:" field. - let recipients: [String]? - - /// A short, optional title of the feedback submission. Suitable for an email subject. - let title: String? - - /// An optional plain-text body of the feedback submission. Suitable for an email body. - let body: String? - - /// An optional collection of log strings. - let logs: [String]? - - /// A file name without an extension for the logs text file. - let logsFileName: String - - /// A dictionary of additional information provided by the application developer. - let additionalInformation: [String: AnyObject]? - - /// A struct containing information about the application and its environment. - let applicationInformation: ApplicationInformation? - - /** - Initializes a `Feedback` with optional default values. - - - parameter screenshot: The type of screenshot in the feedback. - - parameter screenshotFileName: The file name of the screenshot. - - parameter recipients: The recipients of the feedback submission. - - parameter title: The title of the feedback. - - parameter body: The default body text. - - parameter logs: The logs to include in the feedback, if any. - - parameter logsFileName: The file name of the logs text file. - - parameter additionalInformation: Any additional information you want to capture. - - parameter applicationInformation: Information about the application to be captured. - */ - init(screenshot: ScreenshotType, - screenshotFileName: String = "Screenshot", - recipients: [String]? = nil, - title: String? = "Bug Report", - body: String? = nil, - logs: [String]? = nil, - logsFileName: String = "logs", - additionalInformation: [String: AnyObject]? = nil, - applicationInformation: ApplicationInformation? = nil) { - self.screenshot = screenshot - self.screenshotFileName = screenshotFileName - self.recipients = recipients - self.title = title - self.body = body - self.logs = logs - self.logsFileName = logsFileName - self.additionalInformation = additionalInformation - self.applicationInformation = applicationInformation - } -} diff --git a/PinpointKit/PinpointKit/Sources/FeedbackTableViewDataSource.swift b/PinpointKit/PinpointKit/Sources/FeedbackTableViewDataSource.swift deleted file mode 100644 index 06fa508..0000000 --- a/PinpointKit/PinpointKit/Sources/FeedbackTableViewDataSource.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// FeedbackTableViewDataSource.swift -// PinpointKit -// -// Created by Matthew Bischoff on 2/19/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import UIKit - -/// An object conforming to `UITableViewDataSource` that acts as the data source for a `FeedbackViewController`. -final class FeedbackTableViewDataSource: NSObject, UITableViewDataSource { - - private let sections: [Section] - - /** - Initializes the data source with a configuration and a boolean value indicating whether the user has enabled log collection. - - - parameter interfaceCustomization: The interface customization used to set up the data source. - - parameter logSupporting: The object the controls the support of logging. - - parameter userEnabledLogCollection: A boolean value indicating whether the user has enabled log collection. - */ - init(interfaceCustomization: InterfaceCustomization, logSupporting: LogSupporting, userEnabledLogCollection: Bool) { - sections = self.dynamicType.sectionsFromConfiguration(interfaceCustomization, logSupporting: logSupporting, userEnabledLogCollection: userEnabledLogCollection) - } - - private enum Section { - case Feedback(rows: [Row]) - - var numberOfRows: Int { - switch self { - case let .Feedback(rows): - return rows.count - } - } - } - - private enum Row { - case CollectLogs(enabled: Bool, title: String, font: UIFont, canView: Bool) - } - - // MARK: - FeedbackTableViewDataSource - - private static func sectionsFromConfiguration(interfaceCustomization: InterfaceCustomization, logSupporting: LogSupporting, userEnabledLogCollection: Bool) -> [Section] { - guard logSupporting.logCollector != nil else { return [] } - - let collectLogsRow = Row.CollectLogs(enabled: userEnabledLogCollection, title: interfaceCustomization.interfaceText.logCollectionPermissionTitle, font: interfaceCustomization.appearance.logCollectionPermissionFont, canView: logSupporting.logViewer != nil) - let feedbackSection = Section.Feedback(rows: [collectLogsRow]) - - return [feedbackSection] - } - - // MARK: - UITableViewDataSource - - func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return sections[section].numberOfRows - } - - func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return sections.count - } - - func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - let cell = CheckmarkCell() - let section = sections[indexPath.section] - - switch section { - case let .Feedback(rows): - let row = rows[indexPath.row] - - switch row { - case let .CollectLogs(enabled, title, font, canView): - cell.textLabel?.text = title - cell.textLabel?.font = font - cell.accessoryType = canView ? .DetailButton : .None - cell.isChecked = enabled - } - } - - return cell - } -} diff --git a/PinpointKit/PinpointKit/Sources/FeedbackViewController.swift b/PinpointKit/PinpointKit/Sources/FeedbackViewController.swift deleted file mode 100644 index a8fad72..0000000 --- a/PinpointKit/PinpointKit/Sources/FeedbackViewController.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// FeedbackViewController.swift -// PinpointKit -// -// Created by Brian Capps on 2/5/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import UIKit - -/// A `UITableViewController` that conforms to `FeedbackCollector` in order to display an interface that allows the user to see, change, and send feedback. -public final class FeedbackViewController: UITableViewController { - - // MARK: - InterfaceCustomizable - - public var interfaceCustomization: InterfaceCustomization? { - didSet { - guard isViewLoaded() else { return } - - updateInterfaceCustomization() - } - } - - // MARK: - LogSupporting - - public var logViewer: LogViewer? - public var logCollector: LogCollector? - public var editor: Editor? - - // MARK: - FeedbackCollector - - public weak var feedbackDelegate: FeedbackCollectorDelegate? - public var feedbackRecipients: [String]? - - // MARK: - FeedbackViewController - - /// The screenshot the feedback describes. - public var screenshot: UIImage? { - didSet { - guard isViewLoaded() else { return } - updateTableHeaderView() - } - } - - /// The annotated screenshot the feedback describes. - var annotatedScreenshot: UIImage? { - didSet { - guard isViewLoaded() else { return } - updateTableHeaderView() - } - } - - private var dataSource: FeedbackTableViewDataSource? { - didSet { - guard isViewLoaded() else { return } - tableView.dataSource = dataSource - } - } - - private var userEnabledLogCollection = true { - didSet { - updateDataSource() - } - } - - public required init() { - super.init(style: .Grouped) - } - - @available(*, unavailable) - override init(style: UITableViewStyle) { - super.init(style: .Grouped) - } - - @available(*, unavailable) - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - UIViewController - - public override func viewDidLoad() { - super.viewDidLoad() - editor?.delegate = self - updateInterfaceCustomization() - } - - public override func viewWillAppear(animated: Bool) { - super.viewWillAppear(animated) - - // Since this view controller could be reused in another orientation, update the table header view on every appearance to reflect the current orientation sizing. - updateTableHeaderView() - } - - public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { - - coordinator.animateAlongsideTransition({ context in - // Layout and adjust the height of the table header view by setting the property once more to alert the table view of a layout change. - self.tableView.tableHeaderView?.layoutIfNeeded() - self.tableView.tableHeaderView = self.tableView.tableHeaderView - }, completion: nil) - } - - // MARK: - FeedbackViewController - - private func updateDataSource() { - guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return } - - dataSource = FeedbackTableViewDataSource(interfaceCustomization: interfaceCustomization, logSupporting: self, userEnabledLogCollection: userEnabledLogCollection) - } - - private func updateTableHeaderView() { - guard let screenshot = screenshot, editor = editor else { return } - let screenshotToDisplay = annotatedScreenshot ?? screenshot - - // We must set the screenshot before showing the view controller. - editor.setScreenshot(screenshot) - let header = ScreenshotHeaderView() - - header.viewModel = ScreenshotHeaderView.ViewModel(screenshot: screenshotToDisplay, hintText: interfaceCustomization?.interfaceText.feedbackEditHint, hintFont: interfaceCustomization?.appearance.feedbackEditHintFont) - header.screenshotButtonTapHandler = { [weak self] button in - let editImageViewController = NavigationController(rootViewController: editor.viewController) - editImageViewController.view.tintColor = self?.interfaceCustomization?.appearance.tintColor - self?.presentViewController(editImageViewController, animated: true, completion: nil) - } - - tableView.tableHeaderView = header - tableView.enableTableHeaderViewDynamicHeight() - } - - private func updateInterfaceCustomization() { - guard let interfaceCustomization = interfaceCustomization else { assertionFailure(); return } - let interfaceText = interfaceCustomization.interfaceText - let appearance = interfaceCustomization.appearance - - title = interfaceText.feedbackCollectorTitle - navigationController?.navigationBar.titleTextAttributes = [NSFontAttributeName: appearance.navigationTitleFont] - - let sendBarButtonItem = UIBarButtonItem(title: interfaceText.feedbackSendButtonTitle, style: .Done, target: self, action: #selector(FeedbackViewController.sendButtonTapped)) - sendBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackSendButtonFont], forState: .Normal) - navigationItem.rightBarButtonItem = sendBarButtonItem - - let backBarButtonItem = UIBarButtonItem(title: interfaceText.feedbackBackButtonTitle, style: .Plain, target: nil, action: nil) - backBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackBackButtonFont], forState: .Normal) - navigationItem.backBarButtonItem = backBarButtonItem - - let cancelBarButtonItem: UIBarButtonItem - let cancelAction = #selector(FeedbackViewController.cancelButtonTapped) - if let cancelButtonTitle = interfaceText.feedbackCancelButtonTitle { - cancelBarButtonItem = UIBarButtonItem(title: cancelButtonTitle, style: .Plain, target: self, action: cancelAction) - } else { - cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Cancel, target: self, action: cancelAction) - } - - cancelBarButtonItem.setTitleTextAttributes([NSFontAttributeName: appearance.feedbackCancelButtonFont], forState: .Normal) - navigationItem.leftBarButtonItem = cancelBarButtonItem - - view.tintColor = appearance.tintColor - updateTableHeaderView() - updateDataSource() - } - - @objc private func sendButtonTapped() { - - let logs = userEnabledLogCollection ? logCollector?.retrieveLogs() : nil - - let feedback: Feedback? - - if let screenshot = annotatedScreenshot { - feedback = Feedback(screenshot: .Annotated(image: screenshot), recipients: feedbackRecipients, logs: logs) - } else if let screenshot = screenshot { - feedback = Feedback(screenshot: .Original(image: screenshot), recipients: feedbackRecipients, logs: logs) - } else { - feedback = nil - } - - guard let feedbackToSend = feedback else { return assertionFailure("We must have either a screenshot or an edited screenshot!") } - - feedbackDelegate?.feedbackCollector(self, didCollectFeedback: feedbackToSend) - } - - @objc private func cancelButtonTapped() { - dismissViewControllerAnimated(true, completion: nil) - } -} - -// MARK: - FeedbackCollector - -extension FeedbackViewController: FeedbackCollector { - public func collectFeedbackWithScreenshot(screenshot: UIImage, fromViewController viewController: UIViewController) { - self.screenshot = screenshot - annotatedScreenshot = nil - viewController.showDetailViewController(self, sender: viewController) - } -} - -// MARK: - EditorDelegate - -extension FeedbackViewController: EditorDelegate { - public func editorWillDismiss(editor: Editor, screenshot: UIImage) { - annotatedScreenshot = screenshot - updateTableHeaderView() - } -} - -// MARK: - UITableViewDelegate - -extension FeedbackViewController { - public override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) { - guard let logCollector = logCollector else { - assertionFailure("No log collector exists.") - return - } - - logViewer?.viewLog(logCollector, fromViewController: self) - } - - public override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { - userEnabledLogCollection = !userEnabledLogCollection - tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) - } -} - -private extension UITableView { - - /** - A workaround to make table header views created in nibs able to use their intrinsic content size to size the header. Removes the autoresizing constraints that constrain the height, and instead adds width contraints to the table header view. - */ - func enableTableHeaderViewDynamicHeight() { - tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false - - if let headerView = tableHeaderView { - let leadingConstraint = headerView.leadingAnchor.constraintEqualToAnchor(leadingAnchor) - let trailingContraint = headerView.trailingAnchor.constraintEqualToAnchor(trailingAnchor) - let topConstraint = headerView.topAnchor.constraintEqualToAnchor(topAnchor) - let widthConstraint = headerView.widthAnchor.constraintEqualToAnchor(widthAnchor) - - NSLayoutConstraint.activateConstraints([leadingConstraint, trailingContraint, topConstraint, widthConstraint]) - - headerView.layoutIfNeeded() - tableHeaderView = headerView - } - } -} diff --git a/PinpointKit/PinpointKit/Sources/MailSender.swift b/PinpointKit/PinpointKit/Sources/MailSender.swift deleted file mode 100644 index db4a2d4..0000000 --- a/PinpointKit/PinpointKit/Sources/MailSender.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// MailSender.swift -// PinpointKit -// -// Created by Brian Capps on 2/5/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import MessageUI - -/// A `Sender` that uses `MessageUI` to send an email containing the feedback. -public class MailSender: NSObject, Sender { - - /// An error in sending feedback. - enum Error: ErrorType { - - /// An unknown error occured. - case Unknown - - /// No view controller was provided for presentation. - case NoViewControllerProvided - - /// The screenshot failed to encode. - case ImageEncoding - - /// The text failed to encode. - case TextEncoding - - /// `MFMailComposeViewController.canSendMail()` returned `false`. - case MailCannotSend - - /// Email composing was canceled by the user. - case MailCanceled(underlyingError: NSError?) - - /// Email sending failed. - case MailFailed(underlyingError: NSError?) - } - - /// A success in sending feedback. - enum Success: SuccessType { - - /// The email was saved as a draft. - case Saved - - /// The email was sent. - case Sent - } - - private var feedback: Feedback? - - // MARK: - Sender - - /// A delegate that is informed of successful or failed feedback sending. - weak public var delegate: SenderDelegate? - - /** - Sends the feedback using the provided view controller as a presenting view controller. - - - parameter feedback: The feedback to send. - - parameter viewController: The view controller from which to present any of the sender’s necessary views. - */ - public func sendFeedback(feedback: Feedback, fromViewController viewController: UIViewController?) { - guard let viewController = viewController else { fail(.NoViewControllerProvided); return } - - guard MFMailComposeViewController.canSendMail() else { fail(.MailCannotSend); return } - - let mailComposer = MFMailComposeViewController() - mailComposer.mailComposeDelegate = self - - self.feedback = feedback - - do { - try mailComposer.attachFeedback(feedback) - } catch let error as Error { - fail(error) - } catch { - fail(.Unknown) - } - - viewController.presentViewController(mailComposer, animated: true, completion: nil) - } - - // MARK: - MailSender - - private func fail(error: Error) { - delegate?.sender(self, didFailToSendFeedback: feedback, error: error) - feedback = nil - } - - private func succeed(success: Success) { - delegate?.sender(self, didSendFeedback: feedback, success: success) - feedback = nil - } -} - -private extension MFMailComposeViewController { - - func attachFeedback(feedback: Feedback) throws { - setToRecipients(feedback.recipients) - - if let subject = feedback.title { - setSubject(subject) - } - - if let body = feedback.body { - setMessageBody(body, isHTML: false) - } - - try attachScreenshot(feedback.screenshot, screenshotFileName: feedback.screenshotFileName) - - if let logs = feedback.logs { - try attachLogs(logs, logsFileName: feedback.logsFileName) - } - - if let additionalInformation = feedback.additionalInformation { - attachAdditionalInformation(additionalInformation) - } - } - - func attachScreenshot(screenshot: Feedback.ScreenshotType, screenshotFileName: String) throws { - try attachImage(screenshot.preferredImage, filename: screenshotFileName + MIMEType.PNG.fileExtension) - } - - func attachLogs(logs: [String], logsFileName: String) throws { - let logsText = logs.joinWithSeparator("\n\n") - try attachText(logsText, filename: logsFileName + MIMEType.PlainText.fileExtension) - } - - func attachImage(image: UIImage, filename: String) throws { - guard let PNGData = UIImagePNGRepresentation(image) else { throw MailSender.Error.ImageEncoding } - - addAttachmentData(PNGData, mimeType: MIMEType.PNG.rawValue, fileName: filename) - } - - func attachText(text: String, filename: String) throws { - guard let textData = text.dataUsingEncoding(NSUTF8StringEncoding) else { throw MailSender.Error.TextEncoding } - - addAttachmentData(textData, mimeType: MIMEType.PlainText.rawValue, fileName: filename) - } - - func attachAdditionalInformation(additionalInformation: [String: AnyObject]) { - let data = try? NSJSONSerialization.dataWithJSONObject(additionalInformation, options: .PrettyPrinted) - - if let data = data { - addAttachmentData(data, mimeType: MIMEType.JSON.rawValue, fileName: "info.json") - } else { - NSLog("PinpointKit could not attach Feedback.additionalInformation because it was not valid JSON.") - } - } -} - -extension MailSender: MFMailComposeViewControllerDelegate { - public func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) { - controller.dismissViewControllerAnimated(true) { - self.completeWithResult(result, error: error) - } - } - - private func completeWithResult(result: MFMailComposeResult, error: NSError?) { - switch result { - case MFMailComposeResultCancelled: - fail(.MailCanceled(underlyingError: error)) - case MFMailComposeResultFailed: - fail(.MailFailed(underlyingError: error)) - case MFMailComposeResultSaved: - succeed(.Saved) - case MFMailComposeResultSent: - succeed(.Sent) - default: - fail(.Unknown) - } - } -} diff --git a/PinpointKit/PinpointKit/Sources/PinpointKit.swift b/PinpointKit/PinpointKit/Sources/PinpointKit.swift deleted file mode 100644 index 670e162..0000000 --- a/PinpointKit/PinpointKit/Sources/PinpointKit.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// PinpointKit.swift -// PinpointKit -// -// Created by Paul Rehkugler on 1/22/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import Foundation - - -/// `PinpointKit` is an object that can be used to collect feedback from application users. -public class PinpointKit { - - /// Returns a `PinpointKit` instance with a default configuration. - public static let defaultPinpointKit = PinpointKit() - - /// The configuration struct that specifies how PinpointKit should be configured. - private let configuration: Configuration - - /// A delegate that is notified of significant events. - private weak var delegate: PinpointKitDelegate? - - private weak var displayingViewController: UIViewController? - - /** - Initializes a `PinpointKit` object with a configuration and an optional delegate. - - - parameter configuration: The configuration struct that specifies how PinpointKit should be configured. - - parameter delegate: A delegate that is notified of significant events. - */ - public init(configuration: Configuration = Configuration(), delegate: PinpointKitDelegate? = nil) { - self.configuration = configuration - self.delegate = delegate - - self.configuration.feedbackCollector.feedbackDelegate = self - self.configuration.sender.delegate = self - } - - /** - Shows PinpointKit’s feedback collection UI from a given view controller. - - - parameter viewController: The view controller from which to present. - */ - public func show(fromViewController viewController: UIViewController) { - let screenshot = Screenshotter.takeScreenshot() - displayingViewController = viewController - - configuration.feedbackCollector.collectFeedbackWithScreenshot(screenshot, fromViewController: viewController) - } -} - -// MARK: - FeedbackCollectorDelegate - -extension PinpointKit: FeedbackCollectorDelegate { - - public func feedbackCollector(feedbackCollector: FeedbackCollector, didCollectFeedback feedback: Feedback) { - delegate?.pinpointKit(self, willSendFeedback: feedback) - configuration.sender.sendFeedback(feedback, fromViewController: feedbackCollector.viewController) - } -} - -// MARK: - SenderDelegate - -extension PinpointKit: SenderDelegate { - - public func sender(sender: Sender, didSendFeedback feedback: Feedback?, success: SuccessType?) { - guard let feedback = feedback else { return } - - delegate?.pinpointKit(self, didSendFeedback: feedback) - displayingViewController?.dismissViewControllerAnimated(true, completion: nil) - } - - public func sender(sender: Sender, didFailToSendFeedback feedback: Feedback?, error: ErrorType) { - if case MailSender.Error.MailCanceled = error { return } - - NSLog("An error occurred sending mail: \(error)") - } -} - -/// A protocol describing an object that can be notified of events from PinpointKit. -public protocol PinpointKitDelegate: class { - - /** - Notifies the delegate that PinpointKit is about to send user feedback. - - - parameter pinpointKit: The `PinpointKit` instance responsible for the feedback. - - parameter feedback: The feedback that’s about to be sent. - */ - func pinpointKit(pinpointKit: PinpointKit, willSendFeedback feedback: Feedback) - - /** - Notifies the delegate that PinpointKit has just sent user feedback. - - - parameter pinpointKit: The `PinpointKit` instance responsible for the feedback. - - parameter feedback: The feedback that’s just been sent. - */ - func pinpointKit(pinpointKit: PinpointKit, didSendFeedback feedback: Feedback) -} - -/// An extension on PinpointKitDelegate that makes all delegate methods optional by giving them empty implementations by default. -public extension PinpointKitDelegate { - - func pinpointKit(pinpointKit: PinpointKit, willSendFeedback feedback: Feedback) {} - func pinpointKit(pinpointKit: PinpointKit, didSendFeedback feedback: Feedback) {} -} diff --git a/PinpointKit/PinpointKit/Sources/ScreenshotDetector.swift b/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift similarity index 62% rename from PinpointKit/PinpointKit/Sources/ScreenshotDetector.swift rename to PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift index 3a9494a..319e2d9 100644 --- a/PinpointKit/PinpointKit/Sources/ScreenshotDetector.swift +++ b/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift @@ -12,25 +12,25 @@ import Photos /// A class that detects when the user has taken a screenshot and provides it via a delegate callback. @available(iOS 9.0, *) -public class ScreenshotDetector: NSObject { +open class ScreenshotDetector: NSObject { /// An error encountered when detecting and retreiving a screenshot. - enum Error: ErrorType { + public enum Error: Swift.Error { /// The user did not give authorization to this application to their Photo Library. - case Unauthorized(status: PHAuthorizationStatus) + case unauthorized(status: PHAuthorizationStatus) /// The screenshot metadata could not be fetched from the library. - case FetchFailure + case fetchFailure /// The screenshot image data could not be loaded from the library. - case LoadFailure + case loadFailure } /// A boolean value indicating whether the detector is enabled. When set to true, the detector will request photo access whenever a screenshot is taken by the user and deliver screenshots to its delegate. - public var detectionEnabled: Bool = true + open var detectionEnabled: Bool = true private weak var delegate: ScreenshotDetectorDelegate? - private let notificationCenter: NSNotificationCenter + private let notificationCenter: NotificationCenter private let application: UIApplication private let imageManager: PHImageManager @@ -42,7 +42,7 @@ public class ScreenshotDetector: NSObject { - parameter application: An application that will be the `object` of the notification observer. - parameter imageManager: An image manager used to fetch the image data of the screenshot. */ - init(delegate: ScreenshotDetectorDelegate, notificationCenter: NSNotificationCenter = .defaultCenter(), application: UIApplication = .sharedApplication(), imageManager: PHImageManager = .defaultManager()) { + public init(delegate: ScreenshotDetectorDelegate, notificationCenter: NotificationCenter = .default, application: UIApplication = .shared, imageManager: PHImageManager = .default()) { self.delegate = delegate self.notificationCenter = notificationCenter self.application = application @@ -50,10 +50,10 @@ public class ScreenshotDetector: NSObject { super.init() - notificationCenter.addObserver(self, selector: #selector(ScreenshotDetector.userTookScreenshot(_:)), name: UIApplicationUserDidTakeScreenshotNotification, object: application) + notificationCenter.addObserver(self, selector: #selector(ScreenshotDetector.userTookScreenshot(_:)), name: .UIApplicationUserDidTakeScreenshot, object: application) } - @objc private func userTookScreenshot(notification: NSNotification) { + @objc private func userTookScreenshot(_ notification: Notification) { guard detectionEnabled else { return } requestPhotosAuthorization() @@ -61,62 +61,62 @@ public class ScreenshotDetector: NSObject { private func requestPhotosAuthorization() { PHPhotoLibrary.requestAuthorization { authorizationStatus in - NSOperationQueue.mainQueue().addOperationWithBlock { + OperationQueue.main.addOperation { switch authorizationStatus { - case .Authorized: + case .authorized: self.findScreenshot() - case .Denied, .NotDetermined, .Restricted: - self.fail(.Unauthorized(status: authorizationStatus)) + case .denied, .notDetermined, .restricted: + self.fail(with: .unauthorized(status: authorizationStatus)) } } } } private func findScreenshot() { - guard let screenshot = PHAsset.fetchLastScreenshot() else { fail(.FetchFailure); return } + guard let screenshot = PHAsset.fetchLastScreenshot() else { fail(with: .fetchFailure); return } - imageManager.requestImageForAsset(screenshot, + imageManager.requestImage(for: screenshot, targetSize: PHImageManagerMaximumSize, - contentMode: PHImageContentMode.Default, + contentMode: .default, options: PHImageRequestOptions.highQualitySynchronousLocalOptions()) { [weak self] image, info in - NSOperationQueue.mainQueue().addOperationWithBlock { + OperationQueue.main.addOperation { guard let strongSelf = self else { return } - guard let image = image else { strongSelf.fail(.LoadFailure); return } + guard let image = image else { strongSelf.fail(with: .loadFailure); return } - strongSelf.succeed(image) + strongSelf.succeed(with: image) } } } - private func succeed(image: UIImage) { - delegate?.screenshotDetector(self, didDetectScreenshot: image) + private func succeed(with image: UIImage) { + delegate?.screenshotDetector(self, didDetect: image) } - private func fail(error: Error) { - delegate?.screenshotDetector(self, didFailWithError: error) + private func fail(with error: Error) { + delegate?.screenshotDetector(self, didFailWith: error) } } -/// A protocol that `ScreenshotDetector` uses to inform its delegate of sucessful and failed screenshot detection events. +/// A protocol that `ScreenshotDetector` uses to inform its delegate of successful and failed screenshot detection events. @available(iOS 9.0, *) -protocol ScreenshotDetectorDelegate: class { +public protocol ScreenshotDetectorDelegate: class { /** - Notifies the delegate that the detector did sucessfully detect a screenshot. + Notifies the delegate that the detector did successfully detect a screenshot. - parameter screenshotDetector: The detector responsible for the message. - - parameter screenshot: The screeenshot that was detected. + - parameter screenshot: The screenshot that was detected. */ - func screenshotDetector(screenshotDetector: ScreenshotDetector, didDetectScreenshot screenshot: UIImage) + func screenshotDetector(_ screenshotDetector: ScreenshotDetector, didDetect screenshot: UIImage) /** Notifies the delegate that the detector failed to detect a screenshot. - parameter screenshotDetector: The detector responsible for the message. - - parameter error: The error that occurred while attempting to detecting the screenshot. + - parameter error: The error that occurred while attempting to detect the screenshot. */ - func screenshotDetector(screenshotDetector: ScreenshotDetector, didFailWithError error: ScreenshotDetector.Error) + func screenshotDetector(_ screenshotDetector: ScreenshotDetector, didFailWith error: ScreenshotDetector.Error) } @available(iOS 9.0, *) @@ -126,12 +126,12 @@ private extension PHAsset { let options = PHFetchOptions() options.fetchLimit = 1 - options.includeAssetSourceTypes = [.TypeUserLibrary] + options.includeAssetSourceTypes = [.typeUserLibrary] options.wantsIncrementalChangeDetails = false - options.predicate = NSPredicate(format: "(mediaSubtype & %d) != 0", PHAssetMediaSubtype.PhotoScreenshot.rawValue) + options.predicate = NSPredicate(format: "(mediaSubtype & %d) != 0", PHAssetMediaSubtype.photoScreenshot.rawValue) options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] - return PHAsset.fetchAssetsWithMediaType(.Image, options: options).firstObject as? PHAsset + return PHAsset.fetchAssets(with: .image, options: options).firstObject } } @@ -140,9 +140,9 @@ private extension PHImageRequestOptions { static func highQualitySynchronousLocalOptions() -> PHImageRequestOptions { let options = PHImageRequestOptions() - options.deliveryMode = .HighQualityFormat - options.networkAccessAllowed = false - options.synchronous = true + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = false + options.isSynchronous = true return options } diff --git a/PinpointKit/PinpointKit/Sources/ScreenshotHeaderView.swift b/PinpointKit/PinpointKit/Sources/ScreenshotHeaderView.swift deleted file mode 100644 index de9c549..0000000 --- a/PinpointKit/PinpointKit/Sources/ScreenshotHeaderView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// ScreenshotHeaderView.swift -// PinpointKit -// -// Created by Matthew Bischoff on 2/19/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import UIKit - -/// A view that displays a screenshot and hint text about how to edit it. -class ScreenshotHeaderView: UIView { - - /// A type of closure that is invoked when a button is tapped. - typealias TapHandler = (button: UIButton) -> Void - - /** - * A struct encapsulating the information necessary for this view to be displayed. - */ - struct ViewModel { - let screenshot: UIImage - let hintText: String? - let hintFont: UIFont? - } - - private enum DesignConstants: CGFloat { - case DefaultMargin = 15 - case MinimumScreenshotPadding = 50 - } - - /// Set the `viewData` in order to update the receiver’s content. - var viewModel: ViewModel? { - didSet { - screenshotButton.setImage(viewModel?.screenshot.imageWithRenderingMode(.AlwaysOriginal), forState: .Normal) - - if let screenshot = viewModel?.screenshot { - screenshotButtonHeightConstraint = screenshotButton.heightAnchor.constraintEqualToAnchor(screenshotButton.widthAnchor, multiplier: 1.0 / screenshot.aspectRatio) - } - - hintLabel.text = viewModel?.hintText - hintLabel.hidden = viewModel?.hintText == nil || viewModel?.hintText?.isEmpty == true - hintLabel.font = viewModel?.hintFont - } - } - - /// A closure that is invoked when the user taps on the screenshot. - var screenshotButtonTapHandler: TapHandler? - - private let stackView: UIStackView = { - let stackView = UIStackView() - - stackView.axis = .Vertical - stackView.alignment = .Center - stackView.spacing = 10 - stackView.translatesAutoresizingMaskIntoConstraints = false - - stackView.layoutMargins = UIEdgeInsets(top: DesignConstants.DefaultMargin.rawValue, left: DesignConstants.DefaultMargin.rawValue, bottom: DesignConstants.DefaultMargin.rawValue, right: DesignConstants.DefaultMargin.rawValue) - stackView.layoutMarginsRelativeArrangement = true - - return stackView - }() - - private lazy var screenshotButton: UIButton = { - let button = UIButton(type: .System) - button.layer.borderColor = self.tintColor.CGColor - button.layer.borderWidth = 1 - - return button - }() - - private let hintLabel: UILabel = { - let label = UILabel() - label.textColor = UIColor.lightGrayColor() - return label - }() - - private var screenshotButtonHeightConstraint: NSLayoutConstraint? { - didSet { - oldValue?.active = false - screenshotButtonHeightConstraint?.active = true - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - setUp() - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - UIView - - override func tintColorDidChange() { - super.tintColorDidChange() - - screenshotButton.layer.borderColor = tintColor.CGColor - } - - // MARK: - ScreenshotHeaderView - - private func setUp() { - addSubview(stackView) - - stackView.topAnchor.constraintEqualToAnchor(topAnchor).active = true - stackView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true - stackView.leadingAnchor.constraintEqualToAnchor(leadingAnchor).active = true - stackView.trailingAnchor.constraintEqualToAnchor(trailingAnchor).active = true - - stackView.addArrangedSubview(screenshotButton) - stackView.addArrangedSubview(hintLabel) - - setUpScreenshotButton() - } - - private func setUpScreenshotButton() { - screenshotButton.leadingAnchor.constraintGreaterThanOrEqualToAnchor(stackView.leadingAnchor, constant: DesignConstants.MinimumScreenshotPadding.rawValue).active = true - screenshotButton.trailingAnchor.constraintLessThanOrEqualToAnchor(stackView.trailingAnchor, constant: -DesignConstants.MinimumScreenshotPadding.rawValue).active = true - - screenshotButtonHeightConstraint = screenshotButton.heightAnchor.constraintEqualToAnchor(screenshotButton.widthAnchor, multiplier: 1.0) - - screenshotButton.addTarget(self, action: #selector(ScreenshotHeaderView.screenshotButtonTapped(_:)), forControlEvents: .TouchUpInside) - } - - @objc private func screenshotButtonTapped(sender: UIButton) { - screenshotButtonTapHandler?(button: sender) - } -} - -private extension UIImage { - var aspectRatio: CGFloat { - guard size.height > 0 else { return 0 } - - return size.width / size.height - } -} diff --git a/PinpointKit/PinpointKit/Sources/StrokeLayoutManager.swift b/PinpointKit/PinpointKit/Sources/StrokeLayoutManager.swift deleted file mode 100644 index 6e29711..0000000 --- a/PinpointKit/PinpointKit/Sources/StrokeLayoutManager.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// StrokeLayoutManager.swift -// Pinpoint -// -// Created by Brian Capps on 4/23/15. -// Copyright (c) 2015 Lickability. All rights reserved. -// - -import UIKit - -/// A subclass of `NSLayoutManager` that handles drawing the stroke. -final class StrokeLayoutManager: NSLayoutManager { - - /// The color to display as a stroke around the text. - var strokeColor: UIColor? - - /// The width of the stroke to display around the text. - var strokeWidth: CGFloat? - - override func drawGlyphsForGlyphRange(glyphsToShow: NSRange, atPoint origin: CGPoint) { - let context = UIGraphicsGetCurrentContext() - - let firstIndex = characterIndexForGlyphAtIndex(glyphsToShow.location) - let attributes = textStorage?.attributesAtIndex(firstIndex, effectiveRange: nil) - let shadow = attributes?[NSShadowAttributeName] as? NSShadow - let shouldRenderTransparencyLayer = strokeColor != nil && strokeWidth != nil && shadow != nil - - if let shadow = shadow where shouldRenderTransparencyLayer { - // Applies the shadow to the entire stroke as one layer, insead of overlapping per-character. - CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, shadow.shadowColor?.CGColor) - CGContextBeginTransparencyLayer(context, nil) - } - - super.drawGlyphsForGlyphRange(glyphsToShow, atPoint: origin) - - if shouldRenderTransparencyLayer { - CGContextEndTransparencyLayer(context) - } - } - - override func showCGGlyphs(glyphs: UnsafePointer, positions: UnsafePointer, count glyphCount: Int, font: UIFont, matrix textMatrix: CGAffineTransform, attributes: [String : AnyObject], inContext graphicsContext: CGContext) { - var textAttributes = attributes - - if let strokeColor = strokeColor, strokeWidth = strokeWidth { - // Remove the shadow. It'll all be drawn at once afterwards. - textAttributes[NSShadowAttributeName] = nil - CGContextSetShadowWithColor(graphicsContext, CGSize.zero, 0, nil) - - CGContextSaveGState(graphicsContext) - - strokeColor.setStroke() - - CGContextSetLineWidth(graphicsContext, strokeWidth) - CGContextSetLineJoin(graphicsContext, .Miter) - - CGContextSetTextDrawingMode(graphicsContext, .FillStroke) - - super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: textAttributes, inContext: graphicsContext) - - // Due to a bug in iOS 7, kCGTextFillStroke will never have the correct fill color, so we must draw the string twice: once for stroke and once for fill. http://stackoverflow.com/questions/18894907/why-cgcontextsetrgbstrokecolor-isnt-working-on-ios7 - - CGContextRestoreGState(graphicsContext) - CGContextSetTextDrawingMode(graphicsContext, .Fill) - } - - super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: textAttributes, inContext: graphicsContext) - } -} diff --git a/PinpointKit/PinpointKit/Sources/SystemLogCollector.swift b/PinpointKit/PinpointKit/Sources/SystemLogCollector.swift deleted file mode 100644 index 5c4ea4e..0000000 --- a/PinpointKit/PinpointKit/Sources/SystemLogCollector.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// SystemLogCollector.swift -// PinpointKit -// -// Created by Brian Capps on 2/5/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -/// A log collector that uses [Apple System Logger](https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/LoggingErrorsAndWarnings.html) API to retrieve messages logged to the console with `NSLog`. -public class SystemLogCollector: LogCollector { - - private let logger = ASLLogger() - - public init() { } - - // MARK: - LogCollector - - /** - Retrieves and returns logs as an ordered list of strings. - - - returns: Logs as an ordered list of strings, sorted by descending recency. - */ - public func retrieveLogs() -> [String] { - return logger.retrieveLogs() - } -} diff --git a/PinpointKit/PinpointKitTests/PinpointKitTests.swift b/PinpointKit/PinpointKitTests/PinpointKitTests.swift deleted file mode 100644 index d3de3b2..0000000 --- a/PinpointKit/PinpointKitTests/PinpointKitTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// PinpointKitTests.swift -// PinpointKitTests -// -// Created by Paul Rehkugler on 1/22/16. -// Copyright © 2016 Lickability. All rights reserved. -// - -import XCTest -@testable import PinpointKit - -class PinpointKitTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measureBlock { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/PinpointKit/PinpointKitTests/SystemLogCollectorTests.swift b/PinpointKit/PinpointKitTests/SystemLogCollectorTests.swift new file mode 100644 index 0000000..468dcb0 --- /dev/null +++ b/PinpointKit/PinpointKitTests/SystemLogCollectorTests.swift @@ -0,0 +1,87 @@ +// +// SystemLogCollectorTests.swift +// PinpointKit +// +// Created by Andrew Harrison on 8/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import XCTest +@testable import PinpointKit + +class SystemLogCollectorTests: XCTestCase { + + func testLogCollectorCollectsLogs() { + let testString = "TestLog" + let systemLogCollector = SystemLogCollector(loggingType: .testing) + + NSLog(testString) + NSLog(testString) + NSLog(testString) + + let expectation = defaultExpectation() + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) { + + let systemLogs = systemLogCollector?.retrieveLogs() + + guard let logs = systemLogs, let firstLog = logs.first else { return XCTFail("There should be at least 1 log.") } + + XCTAssertEqual(logs.count, 3) + XCTAssertTrue(firstLog.contains(testString)) + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testLogCollectorCollecsLogInOrder() { + let testString1 = "First" + let testString2 = "Second" + let testString3 = "Third" + + let systemLogCollector = SystemLogCollector(loggingType: .testing) + + NSLog(testString1) + NSLog(testString2) + NSLog(testString3) + + let expectation = defaultExpectation() + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) { + let systemLogs = systemLogCollector?.retrieveLogs() + + guard let logs = systemLogs, logs.count == 3 else { return XCTFail("Count should be 3.") } + + let firstLog = logs[0] + let secondLog = logs[1] + let thirdLog = logs[2] + + XCTAssertEqual(logs.count, 3) + XCTAssertTrue(firstLog.contains(testString1)) + XCTAssertTrue(secondLog.contains(testString2)) + XCTAssertTrue(thirdLog.contains(testString3)) + + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testLogCollectorDoesNotCollectPreviousLogs() { + NSLog("Hey") + NSLog("I'm a log!") + + let systemLogCollector = SystemLogCollector(loggingType: .testing) + + XCTAssertEqual(systemLogCollector?.retrieveLogs().count, 0) + } + + func testLogCollectorHasNoLogsInitially() { + let systemLogCollector = SystemLogCollector(loggingType: .testing) + + let logs = systemLogCollector?.retrieveLogs() + + XCTAssertEqual(logs?.count, 0) + } +} diff --git a/PinpointKit/PinpointKitTests/XCTestCaseExpectationExtensions.swift b/PinpointKit/PinpointKitTests/XCTestCaseExpectationExtensions.swift new file mode 100644 index 0000000..671b1f8 --- /dev/null +++ b/PinpointKit/PinpointKitTests/XCTestCaseExpectationExtensions.swift @@ -0,0 +1,23 @@ +// +// XCTestCaseExpectationExtensions.swift +// PinpointKit +// +// Created by Andrew Harrison on 8/5/16. +// Copyright © 2016 Lickability. All rights reserved. +// + +import XCTest + +extension XCTestCase { + + /** + Creates a default expectation for the current function. + + - parameter description: The description for the expectation. By default this will be the name of the function. + + - returns: A new XCTestExpectation with the given description. + */ + func defaultExpectation(description: String = #function) -> XCTestExpectation { + return expectation(description: description) + } +} diff --git a/README.md b/README.md index 1da80f0..38f957f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,35 @@ ![PinpointKit Logo](Assets/logo.png) -**PinpointKit** is an open-source iOS library in Swift that lets your testers and users send feedback with annotated screenshots and logs using a simple gesture. +[![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=588239a8711a3f0100b86836&branch=master&build=latest)](https://dashboard.buddybuild.com/apps/588239a8711a3f0100b86836/build/latest?branch=master) +[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/PinpointKit.svg)](https://img.shields.io/cocoapods/v/PinpointKit.svg) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) + +**PinpointKit** is an open-source iOS library in Swift that lets your testers and users send feedback with annotated screenshots using a simple gesture. + +![Screenshots](Assets/screenshots.png) + + + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) + - [CocoaPods](#cocoapods) + - [Carthage](#carthage) + - [Manually](#manually) + - [Embedded Framework](#embedded-framework) +- [Usage](#usage) +- [Customization](#customization) +- [License](#license) +- [About](#about) + + ## Features - [x] Shake to trigger feedback collection -- [x] Automatic, opt-in system log collection - [x] Add arrows, boxes, and text to screenshots to point out problems. - [x] Blur our sensitive information before sending screenshots +- [x] Automatic, opt-in system log collection ([iOS 9.x only](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/SystemLogCollector.swift#L30)) - [x] Customize everything - [x] The color of the arrows, and boxes - [x] The text in the interface @@ -18,7 +40,11 @@ ## Requirements * iOS 9.0+ -* Xcode 7.3+ +* Xcode 8.0+ +* Swift 3.0 + * Looking for a Swift 2.3 version? See the [README](https://github.com/Lickability/PinpointKit/blob/swift-2.3/README.md) on the [`swift-2.3`](https://github.com/Lickability/PinpointKit/tree/swift-2.3) branch. + +> **Note:** [`ScreenshotDetector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift) depends on the `Photos` framework to access the user’s photo library. This requires you to add an entry for the [`NSPhotoLibraryUsageDescription`](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW17) key in your `Info.plist` file describing your app’s use of the user’s photo library. As of iOS 10, failure to provide a value for this key could cause your submission to the App Store to be rejected by Apple, or cause your app to exit upon attempting to access the user’s photo library. [`ScreenshotDetector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift) is excluded by default when installing via Cocoapods, but is included otherwise. ## Installation @@ -39,7 +65,10 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! -pod 'PinpointKit', '~> 0.9' +target 'YOUR_TARGET_NAME' do + pod 'PinpointKit', '~> 1.0' +end + ``` Then, run the following command: @@ -48,6 +77,12 @@ Then, run the following command: $ pod install ``` +We also offer a convenience class, [`ScreenshotDetector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift) that is available via the `ScreenshotDetector` subspec. This class provides delegate callbacks when the user takes a screenshot while using your app. Please see the [Requirements](#requirements) section regarding inclusion of [`ScreenshotDetector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/ScreenshotDetector/ScreenshotDetector.swift). You can add this to your project by adding the following line in your `Podfile`, in addition to the one for `PinpointKit` above: + +```ruby +pod 'PinpointKit/ScreenshotDetector', '~> 1.0' +``` + ### Carthage [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. @@ -62,7 +97,7 @@ $ brew install carthage To integrate PinpointKit into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "Lickability/PinpointKit" ~> 0.9 +github "Lickability/PinpointKit" ~> 1.0 ``` - Run `carthage update` to build the framework. @@ -107,31 +142,39 @@ The `PinpointKit.framework` is automatically added as a target dependency, linke Once PinpointKit is installed, it’s simple to use. -To display a feedback view controller, add the following code where you want the feedback to display, passing the view controller from which PinpointKit should present: +Initialize an instance of [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift), specifying an array of feedback recipients used to pre-populate email addresses to which feedback can be sent: + +```swift +let pinpointKit = PinpointKit(feedbackRecipients: ["feedback@example.com"]) +``` + +To display a feedback view controller, add the following code where you want the feedback to display, passing the view controller from which [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift) should present: ```swift -PinpointKit.defaultPinpointKit.show(fromViewController: viewController) +pinpointKit.show(from: viewController) ``` +> **Note:** Be sure to keep a strong reference to your instance of [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift) for the duration of its use. -If you want to have the feedback view display from a shake gesture, simply do the following in your [`UIApplicationDelegate`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/index.html) class: +If you want to have the feedback view display from a shake gesture, simply add the following to your application delegate, replacing `["feedback@example.com"]` with your array of email recipients and `AppDelegate` with your application delegate’s name: ```swift -lazy var window: UIWindow? = return ShakeDetectingWindow(frame: UIScreen.mainScreen().bounds) +private static let pinpointKit = PinpointKit(feedbackRecipients: ["feedback@example.com"]) +var window: UIWindow? = ShakeDetectingWindow(frame: UIScreen.main.bounds, delegate: AppDelegate.pinpointKit) ``` -If you don't want to use `defaultPinpointKit` you can specify both [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Configuration.swift) and [`PinpointKitDelegate`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/PinpointKit.swift) instances on initialization of [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/PinpointKit.swift). +If you don’t want to use [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift)’s default configuration, you can specify both [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/Configuration.swift) and [`PinpointKitDelegate`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift) instances on initialization of [`PinpointKit`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift). -The [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Configuration.swift) struct allows you to specify how the feedback view looks and behaves, while the [`PinpointKitDelegate`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/PinpointKit.swift) instance provides hooks into the state of the feedback being sent. +The [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/Configuration.swift) struct allows you to specify how the feedback view looks and behaves, while the [`PinpointKitDelegate`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/PinpointKit.swift) instance provides hooks into the state of the feedback being sent. ## Customization PinpointKit uses a protocol-oriented architecture which allows almost everything to be customized. Here are some examples of what’s possible: -* Implement a `JIRASender` that conforms to [`Sender`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Sender.swift), allowing users to send feedback directly into your bug tracker. -* Supply your own console log collector that aggregates messages from your third-party logging framework of choice by conforming to [`LogCollector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/LogCollector.swift) -* Change how logs are viewed by creating your own view controller conforming to [`LogViewer`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/LogViewer.swift). +* Implement a `JIRASender` that conforms to [`Sender`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/Sender.swift), allowing users to send feedback directly into your bug tracker. +* Supply your own console log collector that aggregates messages from your third-party logging framework of choice by conforming to [`LogCollector`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/LogCollector.swift) +* Change how logs are viewed by creating your own view controller conforming to [`LogViewer`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/LogViewer.swift). -For more information on what you can customize, take a peek the documentation of [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Configuration.swift). +For more information on what you can customize, take a peek at the documentation of [`Configuration`](https://github.com/Lickability/PinpointKit/blob/master/PinpointKit/PinpointKit/Sources/Core/Configuration.swift). ## License