Repository: rileytestut/Clip Branch: main Commit: fc0c861b82e1 Files: 64 Total size: 230.4 KB Directory structure: gitextract_7pamx728/ ├── .gitignore ├── .gitmodules ├── Clip/ │ ├── AppDelegate.swift │ ├── ApplicationMonitor.swift │ ├── Base.lproj/ │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Clip.entitlements │ ├── Components/ │ │ ├── ForwardingNavigationController.swift │ │ └── GradientView.swift │ ├── Extensions/ │ │ ├── PasteboardItem+ActivityItemSource.swift │ │ └── UIDevice+Vibration.swift │ ├── History/ │ │ ├── ClippingTableViewCell.swift │ │ ├── ClippingTableViewCell.xib │ │ └── HistoryViewController.swift │ ├── Info.plist │ ├── Map View/ │ │ ├── ClippingSheet.swift │ │ └── ClippingsMapView.swift │ ├── Pasteboard/ │ │ ├── LocationManager.swift │ │ └── PasteboardMonitor.swift │ ├── Resources/ │ │ └── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── Settings.imageset/ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Settings/ │ │ └── SettingsViewController.swift │ └── Types/ │ └── PublishedPipeline.swift ├── Clip.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ ├── Clip.xcscheme │ ├── ClipKit.xcscheme │ └── ClipboardReader.xcscheme ├── ClipBoard/ │ ├── ClipBoard.entitlements │ ├── Info.plist │ └── KeyboardViewController.swift ├── ClipKit/ │ ├── ClipKit.h │ ├── Database/ │ │ ├── DatabaseManager.swift │ │ └── Model/ │ │ ├── Model.xcdatamodeld/ │ │ │ └── Model.xcdatamodel/ │ │ │ └── contents │ │ ├── PasteboardItem.swift │ │ └── PasteboardItemRepresentation.swift │ ├── Extensions/ │ │ ├── Bundle+AppGroups.swift │ │ ├── CFNotification+PasteboardListener.swift │ │ ├── Int+Bytes.swift │ │ ├── Result+Conveniences.swift │ │ ├── UIColor+Clip.swift │ │ ├── UIInputView+Click.swift │ │ ├── UIPasteboard+PasteboardItem.swift │ │ ├── UNNotification+Keys.swift │ │ └── UserDefaults+App.swift │ ├── Info.plist │ ├── Resources/ │ │ └── Colors.xcassets/ │ │ ├── Contents.json │ │ ├── LightPink.colorset/ │ │ │ └── Contents.json │ │ └── Pink.colorset/ │ │ └── Contents.json │ ├── SwiftUI/ │ │ ├── Blur.swift │ │ ├── ClippingCell.swift │ │ ├── Keyboard.swift │ │ ├── Preview.swift │ │ └── SwitchKeyboardButton.swift │ └── UTI.swift ├── ClipboardReader/ │ ├── Base.lproj/ │ │ └── MainInterface.storyboard │ ├── ClipboardReader.entitlements │ ├── Info.plist │ └── NotificationViewController.swift ├── README.md └── UNLICENSE ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # macOS # *.DS_Store # Xcode # ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap ================================================ FILE: .gitmodules ================================================ [submodule "Dependencies/Roxas"] path = Dependencies/Roxas url = https://github.com/rileytestut/Roxas.git ================================================ FILE: Clip/AppDelegate.swift ================================================ // // AppDelegate.swift // Clip // // Created by Riley Testut on 6/10/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import UserNotifications import ClipKit import Roxas @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. print(RoxasVersionNumber) self.window?.tintColor = .clipPink UserDefaults.shared.registerAppDefaults() func printError(from result: Result, title: String) { guard let error = result.error else { return } print(title, error) } DatabaseManager.shared.prepare() { printError(from: $0, title: "Database Error:") } ApplicationMonitor.shared.start() self.registerForNotifications() return true } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { #if targetEnvironment(simulator) // Audio extension hack to access pasteboard doesn't work in simulator, so for testing just start background task. RSTBeginBackgroundTask("com.rileytestut.Clip.simulatorBackgroundTask") #endif DatabaseManager.shared.purge() } func applicationWillEnterForeground(_ application: UIApplication) { DatabaseManager.shared.refresh() } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } } extension AppDelegate: UNUserNotificationCenterDelegate { private func registerForNotifications() { let category = UNNotificationCategory(identifier: UNNotificationCategory.clipboardReaderIdentifier, actions: [], intentIdentifiers: []) UNUserNotificationCenter.current().setNotificationCategories([category]) UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in } } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler(.alert) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { guard response.notification.request.content.categoryIdentifier == UNNotificationCategory.clipboardReaderIdentifier else { return } guard response.actionIdentifier == UNNotificationDefaultActionIdentifier else { return } let location = ApplicationMonitor.shared.locationManager.location // Delay until next run loop so UIPasteboard no longer returns nil items due to being in background. DispatchQueue.main.async { DatabaseManager.shared.savePasteboard(location: location) { (result) in switch result { case .success: break case .failure(PasteboardError.duplicateItem): break case .failure(let error): DispatchQueue.main.async { let alertController = UIAlertController(title: NSLocalizedString("Failed to Save Clipboard", comment: ""), message: error.localizedDescription, preferredStyle: .alert) alertController.addAction(.ok) self.window?.rootViewController?.present(alertController, animated: true, completion: nil) } } print("Save clipboard with result:", result) completionHandler() } } } } ================================================ FILE: Clip/ApplicationMonitor.swift ================================================ // // ApplicationMonitor.swift // Clip // // Created by Riley Testut on 6/27/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import AVFoundation import UserNotifications import Combine private enum UserNotification: String { case appStoppedRunning = "com.rileytestut.Clip.AppStoppedRunning" } private extension CFNotificationName { static let altstoreRequestAppState: CFNotificationName = CFNotificationName("com.altstore.RequestAppState.com.rileytestut.Clip" as CFString) static let altstoreAppIsRunning: CFNotificationName = CFNotificationName("com.altstore.AppState.Running.com.rileytestut.Clip" as CFString) } private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in ApplicationMonitor.shared.receivedApplicationStateRequest() } class ApplicationMonitor { static let shared = ApplicationMonitor() let pasteboardMonitor = PasteboardMonitor() let locationManager = LocationManager() private(set) var isMonitoring = false private var backgroundTaskID: UIBackgroundTaskIdentifier? } extension ApplicationMonitor { func start() { guard !self.isMonitoring else { return } self.isMonitoring = true self.cancelApplicationQuitNotification() // Cancel any notifications from a previous launch. self.scheduleApplicationQuitNotification() self.pasteboardMonitor.start() { (result) in switch result { case .success: self.locationManager.start() self.registerForNotifications() case .failure(let error): self.isMonitoring = false self.sendNotification(title: NSLocalizedString("Failed to Monitor Clipboard", comment: ""), message: error.localizedDescription) } } } } private extension ApplicationMonitor { func registerForNotifications() { let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterAddObserver(center, nil, ReceivedApplicationState, CFNotificationName.altstoreRequestAppState.rawValue, nil, .deliverImmediately) } func scheduleApplicationQuitNotification() { let delay = 5 as TimeInterval let content = UNMutableNotificationContent() content.title = NSLocalizedString("App Stopped Running", comment: "") content.body = NSLocalizedString("Tap this notification to resume monitoring your clipboard.", comment: "") let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false) let request = UNNotificationRequest(identifier: UserNotification.appStoppedRunning.rawValue, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) DispatchQueue.global().asyncAfter(deadline: .now() + delay) { // If app is still running at this point, we schedule another notification with same identifier. // This prevents the currently scheduled notification from displaying, and starts another countdown timer. self.scheduleApplicationQuitNotification() } } func cancelApplicationQuitNotification() { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [UserNotification.appStoppedRunning.rawValue]) } func sendNotification(title: String, message: String) { let content = UNMutableNotificationContent() content.title = title content.body = message let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } } private extension ApplicationMonitor { func receivedApplicationStateRequest() { guard UIApplication.shared.applicationState != .background else { return } let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterPostNotification(center!, CFNotificationName(CFNotificationName.altstoreAppIsRunning.rawValue), nil, nil, true) } } ================================================ FILE: Clip/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Clip/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Clip/Clip.entitlements ================================================ com.apple.security.application-groups group.com.rileytestut.Clip inter-app-audio ================================================ FILE: Clip/Components/ForwardingNavigationController.swift ================================================ // // ForwardingNavigationController.swift // AltStore // // Created by Riley Testut on 10/24/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit class ForwardingNavigationController: UINavigationController { override var childForStatusBarStyle: UIViewController? { return self.topViewController } override var childForStatusBarHidden: UIViewController? { return self.topViewController } } ================================================ FILE: Clip/Components/GradientView.swift ================================================ // // GradientView.swift // Clip // // Created by Riley Testut on 7/27/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit class GradientView: UIView { var colors: [UIColor] = [] { didSet { self.gradientLayer.colors = self.colors.map { $0.cgColor } } } override class var layerClass: AnyClass { return CAGradientLayer.self } private var gradientLayer: CAGradientLayer { return self.layer as! CAGradientLayer } override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Clip/Extensions/PasteboardItem+ActivityItemSource.swift ================================================ // // PasteboardItem+ActivityItemSource.swift // Clip // // Created by Riley Testut on 6/14/19. // Copyright © 2019 Riley Testut. All rights reserved. // import MobileCoreServices import ClipKit extension PasteboardItem: UIActivityItemSource { public func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { guard let representation = self.preferredRepresentation else { return NSNull() } switch representation.type { case .text: return "" case .attributedText: return NSAttributedString(string: "") case .url: return URL(string: "http://apple.com")! case .image: return Data() } } public func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String { return self.preferredRepresentation?.uti ?? kUTTypeImage as String } public func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { guard let representation = self.preferredRepresentation else { return nil } if activityType == UIActivity.ActivityType.copyToPasteboard { let itemProvider = NSItemProvider() for representation in self.representations { itemProvider.registerItem(forTypeIdentifier: representation.uti) { (completionHandler, expectedClass, options) in completionHandler?(representation.pasteboardValue as? NSSecureCoding, nil) } } // Add our own UTI representation to distinguish from other copies. itemProvider.registerItem(forTypeIdentifier: UTI.clipping) { (completionHandler, expectedClass, options) in completionHandler?([:] as NSDictionary, nil) } return itemProvider } else { // _Don't_ return pasteboard value, instead return the logical object value. // This way, inline data such as text won't accidentally appear as attachments in some share extensions. return representation.value } } } ================================================ FILE: Clip/Extensions/UIDevice+Vibration.swift ================================================ // // UIDevice+Vibration.swift // DeltaCore // // Created by Riley Testut on 11/28/16. // Copyright © 2016 Riley Testut. All rights reserved. // import UIKit import AudioToolbox public extension UIDevice { enum FeedbackSupportLevel: Int { case unsupported // iPhone 6 or earlier, or non-iPhone (e.g. iPad) case basic // iPhone 6s case feedbackGenerator // iPhone 7 and later } } public extension UIDevice { var feedbackSupportLevel: FeedbackSupportLevel { guard let rawValue = self.value(forKey: "_feedbackSupportLevel") as? Int else { return .unsupported } let feedbackSupportLevel = FeedbackSupportLevel(rawValue: rawValue) return feedbackSupportLevel ?? .feedbackGenerator // We'll assume raw values greater than 2 still support UIFeedbackGenerator ¯\_(ツ)_/¯ } var isVibrationSupported: Bool { #if (arch(i386) || arch(x86_64)) // Return false for iOS simulator return false #else // All iPhones support some form of vibration, and potentially future non-iPhone devices will support taptic feedback return (self.model.hasPrefix("iPhone")) || self.feedbackSupportLevel != .unsupported #endif } func vibrate() { guard self.isVibrationSupported else { return } switch self.feedbackSupportLevel { case .unsupported: break case .basic, .feedbackGenerator: AudioServicesPlaySystemSound(1519) // "peek" vibration } } } ================================================ FILE: Clip/History/ClippingTableViewCell.swift ================================================ // // ClippingTableViewCell.swift // Clip // // Created by Riley Testut on 6/13/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit @objc(ClippingTableViewCell) class ClippingTableViewCell: UITableViewCell { @IBOutlet var clippingView: UIView! @IBOutlet var titleLabel: UILabel! @IBOutlet var dateLabel: UILabel! @IBOutlet var contentLabel: UILabel! @IBOutlet var contentImageView: UIImageView! @IBOutlet var locationButton: UIButton! @IBOutlet var bottomConstraint: NSLayoutConstraint! override func awakeFromNib() { super.awakeFromNib() self.clippingView.layer.cornerRadius = 10 self.clippingView.layer.masksToBounds = true self.contentImageView.layer.cornerRadius = 10 self.contentImageView.layer.masksToBounds = true } } ================================================ FILE: Clip/History/ClippingTableViewCell.xib ================================================ ================================================ FILE: Clip/History/HistoryViewController.swift ================================================ // // HistoryViewController.swift // Clip // // Created by Riley Testut on 6/10/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import MobileCoreServices import Combine import CoreLocation import Contacts import ClipKit import Roxas class HistoryViewController: UITableViewController { private var dataSource: RSTFetchedResultsTableViewPrefetchingDataSource! private let _undoManager = UndoManager() private var prototypeCell: ClippingTableViewCell! private var navigationBarMaskView: UIView! private var navigationBarGradientView: GradientView! private var didAddInitialLayoutConstraints = false private var cachedHeights = [NSManagedObjectID: CGFloat]() private weak var selectedItem: PasteboardItem? private var updateTimer: Timer? private var fetchLimitSettingObservation: NSKeyValueObservation? private var cancellables = Set() private lazy var dateComponentsFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .abbreviated formatter.maximumUnitCount = 1 formatter.allowedUnits = [.second, .minute, .hour, .day] return formatter }() override var undoManager: UndoManager? { return _undoManager } override var canBecomeFirstResponder: Bool { return true } override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } override func viewDidLoad() { super.viewDidLoad() self.subscribe() self.extendedLayoutIncludesOpaqueBars = true self.tableView.backgroundView = self.makeGradientView() self.updateDataSource() self.tableView.contentInset.top = 8 self.tableView.estimatedRowHeight = 0 self.prototypeCell = ClippingTableViewCell.instantiate(with: ClippingTableViewCell.nib!) self.tableView.register(ClippingTableViewCell.nib, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier) DatabaseManager.shared.persistentContainer.viewContext.undoManager = self.undoManager NotificationCenter.default.addObserver(self, selector: #selector(HistoryViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(HistoryViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(HistoryViewController.settingsDidChange(_:)), name: SettingsViewController.settingsDidChangeNotification, object: nil) self.fetchLimitSettingObservation = UserDefaults.shared.observe(\.historyLimit) { [weak self] (defaults, change) in self?.updateDataSource() } self.navigationBarGradientView = self.makeGradientView() self.navigationBarGradientView.translatesAutoresizingMaskIntoConstraints = false self.navigationBarMaskView = UIView() self.navigationBarMaskView.clipsToBounds = true self.navigationBarMaskView.translatesAutoresizingMaskIntoConstraints = false self.navigationBarMaskView.addSubview(self.navigationBarGradientView) if let navigationBar = self.navigationController?.navigationBar { if #available(iOS 13.0, *) { let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white] let standardAppearance = navigationBar.standardAppearance standardAppearance.configureWithOpaqueBackground() standardAppearance.backgroundColor = .clipLightPink standardAppearance.titleTextAttributes = attributes standardAppearance.largeTitleTextAttributes = attributes standardAppearance.shadowImage = nil let scrollEdgeAppearance = navigationBar.scrollEdgeAppearance scrollEdgeAppearance?.configureWithTransparentBackground() scrollEdgeAppearance?.titleTextAttributes = attributes scrollEdgeAppearance?.largeTitleTextAttributes = attributes } else { navigationBar.shadowImage = UIImage() navigationBar.setBackgroundImage(nil, for: .default) navigationBar.insertSubview(self.navigationBarMaskView, at: 1) } } if let tabBar = self.navigationController?.tabBarController?.tabBar { let appearance = tabBar.standardAppearance tabBar.scrollEdgeAppearance = appearance } self.navigationController?.tabBarItem.image = UIImage(systemName: "list.bullet") self.startUpdating() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.becomeFirstResponder() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.resignFirstResponder() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) self.cachedHeights.removeAll() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if #available(iOS 13.0, *) {} else { if let navigationBar = self.navigationController?.navigationBar, !self.didAddInitialLayoutConstraints { self.didAddInitialLayoutConstraints = true NSLayoutConstraint.activate([self.navigationBarGradientView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.navigationBarGradientView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), self.navigationBarGradientView.topAnchor.constraint(equalTo: self.view.topAnchor), self.navigationBarGradientView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)]) NSLayoutConstraint.activate([self.navigationBarMaskView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor), self.navigationBarMaskView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor), self.navigationBarMaskView.topAnchor.constraint(equalTo: self.view.topAnchor), self.navigationBarMaskView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)]) } } } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard segue.identifier == "showSettings" else { return } guard let sender = sender as? UIBarButtonItem else { return } let navigationController = segue.destination as! UINavigationController let settingsViewController = navigationController.viewControllers[0] as! SettingsViewController settingsViewController.view.layoutIfNeeded() navigationController.preferredContentSize = CGSize(width: 375, height: settingsViewController.tableView.contentSize.height) navigationController.popoverPresentationController?.delegate = self navigationController.popoverPresentationController?.barButtonItem = sender } } extension HistoryViewController { override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { let supportedActions = [#selector(UIResponderStandardEditActions.copy(_:)), #selector(UIResponderStandardEditActions.delete(_:)), #selector(HistoryViewController._share(_:))] let isSupported = supportedActions.contains(action) return isSupported } @objc override func copy(_ sender: Any?) { guard let item = self.selectedItem else { return } UIPasteboard.general.copy(item) } @objc override func delete(_ sender: Any?) { guard let item = self.selectedItem else { return } // Use the main view context so we can undo this operation easily. // Saving a context can mess with its undo history, so we only save main context when we enter background. item.isMarkedForDeletion = true } @objc func _share(_ sender: Any?) { guard let item = self.selectedItem, let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: item) else { return } let cell = self.tableView.cellForRow(at: indexPath) let activityViewController = UIActivityViewController(activityItems: [item], applicationActivities: nil) activityViewController.popoverPresentationController?.sourceItem = cell self.present(activityViewController, animated: true, completion: nil) } } private extension HistoryViewController { func subscribe() { //TODO: Uncomment once we can tell user to enable location for background execution. //ApplicationMonitor.shared.locationManager.$status // .receive(on: RunLoop.main) // .compactMap { $0?.error } // .sink { self.present($0) } // .store(in: &self.cancellables) } func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource { let fetchRequest = PasteboardItem.historyFetchRequest() fetchRequest.returnsObjectsAsFaults = false fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(PasteboardItem.preferredRepresentation)] let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.persistentContainer.viewContext) dataSource.cellConfigurationHandler = { [weak self] (cell, item, indexPath) in let cell = cell as! ClippingTableViewCell cell.contentLabel.isHidden = false cell.contentImageView.isHidden = true self?.updateDate(for: cell, item: item) if let representation = item.preferredRepresentation { cell.titleLabel.text = representation.type.localizedName switch representation.type { case .text: cell.contentLabel.text = representation.stringValue case .attributedText: cell.contentLabel.text = representation.attributedStringValue?.string case .url: cell.contentLabel.text = representation.urlValue?.absoluteString case .image: cell.contentLabel.isHidden = true cell.contentImageView.isHidden = false cell.contentImageView.isIndicatingActivity = true } } else { cell.titleLabel.text = NSLocalizedString("Unknown", comment: "") cell.contentLabel.isHidden = true } if UserDefaults.shared.showLocationIcon { cell.locationButton.isHidden = (item.location == nil) cell.locationButton.addTarget(self, action: #selector(HistoryViewController.showLocation(_:)), for: .primaryActionTriggered) } else { cell.locationButton.isHidden = true } if indexPath.row < UserDefaults.shared.historyLimit.rawValue { cell.bottomConstraint.isActive = true } else { // Make it not active so we can collapse the cell to a height of 0 without auto layout errors. cell.bottomConstraint.isActive = false } } dataSource.prefetchHandler = { (item, indexPath, completionHandler) in guard let representation = item.preferredRepresentation, representation.type == .image else { return nil } return RSTBlockOperation() { (operation) in guard let image = representation.imageValue?.resizing(toFill: CGSize(width: 500, height: 500)) else { return completionHandler(nil, nil) } completionHandler(image, nil) } } dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in DispatchQueue.main.async { let cell = cell as! ClippingTableViewCell if let image = image { cell.contentImageView.image = image } else { cell.contentImageView.image = nil } cell.contentImageView.isIndicatingActivity = false } } let placeholderView = RSTPlaceholderView() placeholderView.textLabel.text = NSLocalizedString("No Clippings", comment: "") placeholderView.textLabel.textColor = .white placeholderView.detailTextLabel.text = NSLocalizedString("Items that you've copied to the clipboard will appear here.", comment: "") placeholderView.detailTextLabel.textColor = .white let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark))) vibrancyView.contentView.addSubview(placeholderView, pinningEdgesWith: .zero) let gradientView = self.makeGradientView() gradientView.addSubview(vibrancyView, pinningEdgesWith: .zero) dataSource.placeholderView = gradientView return dataSource } func makeGradientView() -> GradientView { let gradientView = GradientView() gradientView.colors = [.clipLightPink, .clipPink] return gradientView } func updateDataSource() { self.stopUpdating() self.dataSource = self.makeDataSource() self.tableView.dataSource = self.dataSource self.tableView.prefetchDataSource = self.dataSource self.tableView.reloadData() self.startUpdating() } func updateDate(for cell: ClippingTableViewCell, item: PasteboardItem) { if Date().timeIntervalSince(item.date) < 2 { cell.dateLabel.text = NSLocalizedString("now", comment: "") } else { cell.dateLabel.text = self.dateComponentsFormatter.string(from: item.date, to: Date()) } } func showMenu(at indexPath: IndexPath) { guard let cell = self.tableView.cellForRow(at: indexPath) as? ClippingTableViewCell else { return } let item = self.dataSource.item(at: indexPath) self.selectedItem = item let targetRect = cell.clippingView.frame self.becomeFirstResponder() UIMenuController.shared.setTargetRect(targetRect, in: cell) UIMenuController.shared.setMenuVisible(true, animated: true) } @objc func showLocation(_ sender: UIButton) { let point = self.view.convert(sender.center, from: sender.superview!) guard let indexPath = self.tableView.indexPathForRow(at: point) else { return } let item = self.dataSource.item(at: indexPath) guard let location = item.location else { return } let geocoder = CLGeocoder() geocoder.reverseGeocodeLocation(location) { (placemarks, error) in DispatchQueue.main.async { let title: String let message: String? if let placemarks, let placemark = placemarks.first, let postalAddress = placemark.postalAddress?.mutableCopy() as? CNMutablePostalAddress { // The location isn't precise, so don't pretend that it is by showing street address. postalAddress.street = "" postalAddress.subLocality = "" let formatter = CNPostalAddressFormatter() if let sublocality = placemark.subLocality { title = sublocality + "\n" + formatter.string(from: postalAddress) } else { title = formatter.string(from: postalAddress) } message = nil } else if let error { title = NSLocalizedString("Unable to Look Up Location", comment: "") message = error.localizedDescription + "\n\n" + "\(location.coordinate.latitude), \(location.coordinate.longitude)" } else { title = "\(location.coordinate.latitude), \(location.coordinate.longitude)" message = nil } let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(.ok) self.present(alertController, animated: true) } } } func startUpdating() { self.stopUpdating() self.updateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] (timer) in guard let self = self else { return } for indexPath in self.tableView.indexPathsForVisibleRows ?? [] { guard let cell = self.tableView.cellForRow(at: indexPath) as? ClippingTableViewCell else { continue } let item = self.dataSource.item(at: indexPath) self.updateDate(for: cell, item: item) } } } func stopUpdating() { self.updateTimer?.invalidate() self.updateTimer = nil } } private extension HistoryViewController { func present(_ error: Error) { let nsError = error as NSError let alertController = UIAlertController(title: nsError.localizedFailureReason ?? nsError.localizedDescription, message: nsError.localizedRecoverySuggestion, preferredStyle: .alert) if let recoverableError = error as? RecoverableError, !recoverableError.recoveryOptions.isEmpty { alertController.addAction(.cancel) for (index, title) in zip(0..., recoverableError.recoveryOptions) { let action = UIAlertAction(title: title, style: .default) { (action) in recoverableError.attemptRecovery(optionIndex: index) { (success) in print("Recovered from error with success:", success) } } alertController.addAction(action) } } else { alertController.addAction(.ok) } self.present(alertController, animated: true, completion: nil) } } private extension HistoryViewController { @objc func didEnterBackground(_ notification: Notification) { // Save any pending changes to disk. if DatabaseManager.shared.persistentContainer.viewContext.hasChanges { do { try DatabaseManager.shared.persistentContainer.viewContext.save() } catch { print("Failed to save view context.", error) } } self.undoManager?.removeAllActions() self.stopUpdating() } @objc func willEnterForeground(_ notification: Notification) { self.startUpdating() } @objc func settingsDidChange(_ notification: Notification) { self.tableView.reloadData() } @IBAction func unwindToHistoryViewController(_ segue: UIStoryboardSegue) { } } extension HistoryViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { // It's far *far* easier to simply set row height to 0 for cells beyond history limit // than to actually limit fetched results to the correct number live (with insertions and deletions). guard indexPath.row < UserDefaults.shared.historyLimit.rawValue else { return 0.0 } let item = self.dataSource.item(at: indexPath) if let height = self.cachedHeights[item.objectID] { return height } let portraitScreenHeight = UIScreen.main.coordinateSpace.convert(UIScreen.main.bounds, to: UIScreen.main.fixedCoordinateSpace).height let maximumHeight: CGFloat if item.preferredRepresentation?.type == .image { maximumHeight = portraitScreenHeight / 2 } else { maximumHeight = portraitScreenHeight / 3 } let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: tableView.bounds.width) let heightConstraint = self.prototypeCell.contentView.heightAnchor.constraint(lessThanOrEqualToConstant: maximumHeight) NSLayoutConstraint.activate([widthConstraint, heightConstraint]) defer { NSLayoutConstraint.deactivate([widthConstraint, heightConstraint]) } self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath) let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) self.cachedHeights[item.objectID] = size.height return size.height } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { self.showMenu(at: indexPath) } } extension HistoryViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } } ================================================ FILE: Clip/Info.plist ================================================ ALTAppGroups group.com.rileytestut.Clip CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Clip CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName Clip General CFBundleURLSchemes clip CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSLocationAlwaysAndWhenInUseUsageDescription Clip requires “Always” location permission to automatically tag clippings with your location. Your location data never leaves this device. NSLocationDefaultAccuracyReduced NSLocationWhenInUseUsageDescription Clip requires location permissions to automatically tag clippings with your location. Your location data never leaves this device. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UILaunchStoryboardName LaunchScreen UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main UIBackgroundModes location UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UIStatusBarStyle UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIUserInterfaceStyle Light UTExportedTypeDeclarations UTTypeConformsTo public.data UTTypeDescription Clipping UTTypeIconFiles UTTypeIdentifier com.rileytestut.Clip.Clipping ================================================ FILE: Clip/Map View/ClippingSheet.swift ================================================ // // ClippingSheet.swift // Clip // // Created by Riley Testut on 3/20/24. // Copyright © 2024 Riley Testut. All rights reserved. // import SwiftUI import ClipKit @available(iOS 16.4, *) struct ClippingSheet: View { var pasteboardItem: PasteboardItem @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { ClippingCell(pasteboardItem: pasteboardItem) .padding() .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Copy", action: copy) } ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: cancel) } } } .presentationBackground(Material.regular) .presentationDetents([.fraction(0.33)]) .tint(.init(uiColor: .clipPink)) } } @available(iOS 16.4, *) private extension ClippingSheet { func copy() { let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterPostNotification(center, .ignoreNextPasteboardChange, nil, nil, true) UIPasteboard.general.copy(self.pasteboardItem) self.dismiss() } func cancel() { self.dismiss() } } ================================================ FILE: Clip/Map View/ClippingsMapView.swift ================================================ // // HistoryMapView.swift // Clip // // Created by Riley Testut on 3/20/24. // Copyright © 2024 Riley Testut. All rights reserved. // import MapKit import UIKit import SwiftUI import ClipKit @available(iOS 17, *) class ClippingsMapViewController: UIHostingController { @MainActor required dynamic init?(coder aDecoder: NSCoder) { let view = AnyView(erasing: ClippingsMapView().environment(\.managedObjectContext, DatabaseManager.shared.persistentContainer.viewContext)) super.init(coder: aDecoder, rootView: view) self.tabBarItem.image = UIImage(systemName: "map") } } @MainActor @available(iOS 17, *) struct ClippingsMapView: View { @FetchRequest(fetchRequest: PasteboardItem.historyFetchRequest()) private var pasteboardItems: FetchedResults @State private var selectedItem: PasteboardItem? var body: some View { Map(selection: $selectedItem) { // Must use \.self as keypath for selection to work ForEach(pasteboardItems, id: \.self) { pasteboardItem in if let location = pasteboardItem.location { Marker(pasteboardItem.date.formatted(), systemImage: "paperclip", coordinate: location.coordinate) } } } .sheet(item: $selectedItem) { pasteboardItem in ClippingSheet(pasteboardItem: pasteboardItem) } } } ================================================ FILE: Clip/Pasteboard/LocationManager.swift ================================================ // // LocationManager.swift // Clip // // Created by Riley Testut on 11/6/20. // Copyright © 2020 Riley Testut. All rights reserved. // import CoreLocation import Combine import UIKit extension LocationManager { typealias Status = Result enum Error: LocalizedError, RecoverableError { case requiresAlwaysAuthorization var failureReason: String? { switch self { case .requiresAlwaysAuthorization: return NSLocalizedString("Clip requires “Always” location permission.", comment: "") } } var recoverySuggestion: String? { switch self { case .requiresAlwaysAuthorization: return NSLocalizedString("Please grant Clip “Always” location permission in Settings so it can run in the background indefinitely.", comment: "") } } var recoveryOptions: [String] { switch self { case .requiresAlwaysAuthorization: return [NSLocalizedString("Open Settings", comment: "")] } } func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool { return false } func attemptRecovery(optionIndex recoveryOptionIndex: Int, resultHandler handler: @escaping (Bool) -> Void) { switch self { case .requiresAlwaysAuthorization: let openURL = URL(string: UIApplication.openSettingsURLString)! UIApplication.shared.open(openURL, options: [:], completionHandler: handler) } } } } class LocationManager: NSObject, ObservableObject { @PublishedPipeline({ removeDuplicatesPipeline($0) }) var status: Status? = nil var location: CLLocation? { return self.locationManager.location } private let locationManager: CLLocationManager override init() { self.locationManager = CLLocationManager() self.locationManager.distanceFilter = CLLocationDistanceMax self.locationManager.pausesLocationUpdatesAutomatically = false self.locationManager.allowsBackgroundLocationUpdates = true if #available(iOS 14.0, *) { self.locationManager.desiredAccuracy = kCLLocationAccuracyReduced } else { self.locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers } super.init() self.locationManager.delegate = self } func start() { switch self.status { case .success: return case .failure, nil: break } if CLLocationManager.authorizationStatus() == .notDetermined || CLLocationManager.authorizationStatus() == .authorizedWhenInUse { self.locationManager.requestAlwaysAuthorization() return } self.locationManager.startUpdatingLocation() } func stop() { self.locationManager.stopUpdatingLocation() self.status = nil } } private extension LocationManager { static func removeDuplicatesPipeline(_ publisher: T) -> AnyPublisher where T.Output == Status?, T.Failure == Never { return publisher .removeDuplicates { (a, b) in switch (a, b) { case (nil, nil), (.success(()), .success(())): return true case (.failure(let errorA as NSError), .failure(let errorB as NSError)): return errorA.domain == errorB.domain && errorA.code == errorB.code case (nil, _), (.success, _), (.failure, _): return false } } .eraseToAnyPublisher() } } extension LocationManager: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { switch status { case .notDetermined: break case .restricted, .denied, .authorizedWhenInUse: self.status = .failure(Error.requiresAlwaysAuthorization) case .authorizedAlways: self.start() @unknown default: break } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { self.status = .success(()) } func locationManager(_ manager: CLLocationManager, didFailWithError error: Swift.Error) { if let error = error as? CLError { guard error.code != .denied else { self.status = .failure(Error.requiresAlwaysAuthorization) return } } self.status = .failure(error) } } ================================================ FILE: Clip/Pasteboard/PasteboardMonitor.swift ================================================ // // PasteboardMonitor.swift // Clip // // Created by Riley Testut on 6/11/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import AVFoundation import UserNotifications import CoreLocation import ClipKit import Roxas private let PasteboardMonitorDidChangePasteboard: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in ApplicationMonitor.shared.pasteboardMonitor.didChangePasteboard() } private let PasteboardMonitorIgnoreNextPasteboardChange: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in ApplicationMonitor.shared.pasteboardMonitor.ignoreNextPasteboardChange = true } class PasteboardMonitor { private(set) var isStarted = false fileprivate var ignoreNextPasteboardChange = false private let feedbackGenerator = UINotificationFeedbackGenerator() } extension PasteboardMonitor { func start(completionHandler: @escaping (Result) -> Void) { guard !self.isStarted else { return } self.isStarted = true self.registerForNotifications() completionHandler(.success(())) } } private extension PasteboardMonitor { func registerForNotifications() { let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterAddObserver(center, nil, PasteboardMonitorDidChangePasteboard, CFNotificationName.didChangePasteboard.rawValue, nil, .deliverImmediately) CFNotificationCenterAddObserver(center, nil, PasteboardMonitorIgnoreNextPasteboardChange, CFNotificationName.ignoreNextPasteboardChange.rawValue, nil, .deliverImmediately) #if !targetEnvironment(simulator) let beginListeningSelector = ["Notifications", "Change", "Pasteboard", "To", "Listening", "begin"].reversed().joined() let className = ["Connection", "Server", "PB"].reversed().joined() let PBServerConnection = NSClassFromString(className) as AnyObject _ = PBServerConnection.perform(NSSelectorFromString(beginListeningSelector)) #endif let changedNotification = ["changed", "pasteboard", "apple", "com"].reversed().joined(separator: ".") NotificationCenter.default.addObserver(self, selector: #selector(PasteboardMonitor.pasteboardDidUpdate), name: Notification.Name(changedNotification), object: nil) } @objc func pasteboardDidUpdate() { guard !self.ignoreNextPasteboardChange else { self.ignoreNextPasteboardChange = false return } DispatchQueue.main.async { if UIApplication.shared.applicationState != .background { // Don't present notifications for items copied from within Clip. guard !UIPasteboard.general.contains(pasteboardTypes: [UTI.clipping]) else { return } } UNUserNotificationCenter.current().getNotificationSettings { (settings) in if settings.soundSetting == .enabled { UIDevice.current.vibrate() } } let content = UNMutableNotificationContent() content.categoryIdentifier = UNNotificationCategory.clipboardReaderIdentifier content.title = NSLocalizedString("Clipboard Changed", comment: "") content.body = NSLocalizedString("Swipe down to save to Clip.", comment: "") if let location = ApplicationMonitor.shared.locationManager.location { content.userInfo = [ UNNotification.latitudeUserInfoKey: location.coordinate.latitude, UNNotification.longitudeUserInfoKey: location.coordinate.longitude ] } let request = UNNotificationRequest(identifier: "ClipboardChanged", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { (error) in if let error = error { print(error) } } } } } private extension PasteboardMonitor { func didChangePasteboard() { DatabaseManager.shared.refresh() UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["ClipboardChanged"]) } } ================================================ FILE: Clip/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "Clip1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Clip/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Clip/Resources/Assets.xcassets/Settings.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "gear_1.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Clip/SceneDelegate.swift ================================================ // // SceneDelegate.swift // Clip // // Created by Riley Testut on 10/30/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit import ClipKit import Roxas class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } if let context = connectionOptions.urlContexts.first { self.open(context) } } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. guard DatabaseManager.shared.isStarted else { return } DatabaseManager.shared.refresh() guard !UIPasteboard.general.hasImages else { // Our duplicate detection does not work for images, // so don't automatically save images upon returning to foreground. return } let location = ApplicationMonitor.shared.locationManager.location DatabaseManager.shared.savePasteboard(location: location) { (result) in do { try result.get() print("Saved clipboard upon returning to foreground!") } catch PasteboardError.noItem, PasteboardError.duplicateItem { // Ignore } catch { print("Failed to save clipboard upon returning to app.") } } } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. #if targetEnvironment(simulator) // Audio extension hack to access pasteboard doesn't work in simulator, so for testing just start background task. RSTBeginBackgroundTask("com.rileytestut.Clip.simulatorBackgroundTask") #endif DatabaseManager.shared.purge() } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first else { return } self.open(context) } } private extension SceneDelegate { func open(_ context: UIOpenURLContext) { guard context.url.scheme?.lowercased() == "clip" && context.url.host?.lowercased() == "settings" else { return } let openURL = URL(string: UIApplication.openSettingsURLString)! UIApplication.shared.open(openURL, options: [:], completionHandler: nil) } } ================================================ FILE: Clip/Settings/SettingsViewController.swift ================================================ // // SettingsViewController.swift // Clip // // Created by Riley Testut on 6/14/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import ClipKit extension SettingsViewController { private enum Section: CaseIterable { case historyLimit case location } static let settingsDidChangeNotification: Notification.Name = Notification.Name("SettingsDidChangeNotification") } class SettingsViewController: UITableViewController { @IBOutlet private var showLocationIconSwitch: UISwitch! override func viewDidLoad() { super.viewDidLoad() self.showLocationIconSwitch.isOn = UserDefaults.shared.showLocationIcon } } private extension SettingsViewController { @IBAction func toggleShowLocationIcon(_ sender: UISwitch) { UserDefaults.shared.showLocationIcon = sender.isOn NotificationCenter.default.post(name: SettingsViewController.settingsDidChangeNotification, object: nil) } } extension SettingsViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) switch Section.allCases[indexPath.section] { case .historyLimit: let limit = HistoryLimit.allCases[indexPath.row] cell.accessoryType = (limit == UserDefaults.shared.historyLimit) ? .checkmark : .none case .location: break } return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard Section.allCases[indexPath.section] == .historyLimit else { return } let historyLimit = HistoryLimit.allCases[indexPath.row] UserDefaults.shared.historyLimit = historyLimit tableView.reloadData() self.dismiss(animated: true, completion: nil) } } ================================================ FILE: Clip/Types/PublishedPipeline.swift ================================================ // // PublishedPipeline.swift // Clip // // Created by Riley Testut on 12/4/20. // Copyright © 2020 Riley Testut. All rights reserved. // import Combine @propertyWrapper class PublishedPipeline { @Published var wrappedValue: Value var projectedValue: AnyPublisher { return self.pipeline(self.$wrappedValue.eraseToAnyPublisher()).eraseToAnyPublisher() } private let pipeline: (AnyPublisher) -> Pipeline init(wrappedValue: Value, _ pipeline: @escaping (AnyPublisher) -> Pipeline) { self.wrappedValue = wrappedValue self.pipeline = pipeline } } ================================================ FILE: Clip.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ BF0BDE5122B4414A00E1419D /* UTI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0BDE5022B4414A00E1419D /* UTI.swift */; }; BF0BDE5622B456A600E1419D /* PasteboardItem+ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0BDE5522B456A600E1419D /* PasteboardItem+ActivityItemSource.swift */; }; BF0BDE5A22B4771300E1419D /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0BDE5922B4771300E1419D /* SettingsViewController.swift */; }; BF0BDE5E22B47D4900E1419D /* UserDefaults+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0BDE5722B4757100E1419D /* UserDefaults+App.swift */; }; BF1D5C7A22B029E70062F474 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1D5C7522B029DC0062F474 /* Roxas.framework */; platformFilter = ios; }; BF1D5C7B22B029E70062F474 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF1D5C7522B029DC0062F474 /* Roxas.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF1D5C7F22B02BF30062F474 /* PasteboardMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1D5C7E22B02BF30062F474 /* PasteboardMonitor.swift */; }; BF235BC1247D8B5300CCFCB0 /* UIPasteboard+PasteboardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF235BC0247D8B5300CCFCB0 /* UIPasteboard+PasteboardItem.swift */; }; BF50E7D922C2BF010070E17B /* Bundle+AppGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF50E7D822C2BF010070E17B /* Bundle+AppGroups.swift */; }; BF770E6522BC7688002A40FE /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6122BC767A002A40FE /* UIDevice+Vibration.swift */; }; BF7B9EE122B81C980042C873 /* Int+Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EE022B81C980042C873 /* Int+Bytes.swift */; }; BF7E6FE3247C4FCD0058F4D4 /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E6FE2247C4FCD0058F4D4 /* KeyboardViewController.swift */; }; BF7E6FE7247C4FCD0058F4D4 /* ClipBoard.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BF7E6FE0247C4FCD0058F4D4 /* ClipBoard.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BF8F76F3254CC0E0005AF18B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F76F2254CC0E0005AF18B /* SceneDelegate.swift */; }; BFA2DF60247DD21F00E31E4D /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E6FF0247C50540058F4D4 /* Keyboard.swift */; }; BFA2DF61247DD23800E31E4D /* ClippingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E6FF2247C518D0058F4D4 /* ClippingCell.swift */; }; BFA2DF62247DD23C00E31E4D /* Blur.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0AB807247C5D1F0090B43B /* Blur.swift */; }; BFA2DF65247DD2B200E31E4D /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFA2DF64247DD2B200E31E4D /* Colors.xcassets */; }; BFA2DF69247DD31500E31E4D /* UIColor+Clip.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA2DF68247DD31500E31E4D /* UIColor+Clip.swift */; }; BFA2DF6B247DE47900E31E4D /* Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA2DF6A247DE47900E31E4D /* Preview.swift */; }; BFA714BA22C53F1700EE7236 /* ApplicationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA714B922C53F1700EE7236 /* ApplicationMonitor.swift */; }; BFAC49D322B2EDA80011E7C4 /* ClippingTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFAC49D222B2EDA80011E7C4 /* ClippingTableViewCell.xib */; }; BFAC49D522B2EDB20011E7C4 /* ClippingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAC49D422B2EDB20011E7C4 /* ClippingTableViewCell.swift */; }; BFAC49E022B40EC90011E7C4 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFAC49DF22B40EC90011E7C4 /* AudioToolbox.framework */; }; BFAD2C4A257AD4D600FF9532 /* PublishedPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD2C49257AD4D600FF9532 /* PublishedPipeline.swift */; }; BFAD46C92490588800451D6F /* UIInputView+Click.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD46C82490588800451D6F /* UIInputView+Click.swift */; }; BFAD46CB249058DE00451D6F /* SwitchKeyboardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD46CA249058DE00451D6F /* SwitchKeyboardButton.swift */; }; BFC176FD2399BC4F0058AC51 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69A53F2395D44600CF838A /* UserNotifications.framework */; }; BFC176FE2399BC4F0058AC51 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69A5412395D44700CF838A /* UserNotificationsUI.framework */; }; BFC177012399BC4F0058AC51 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC177002399BC4F0058AC51 /* NotificationViewController.swift */; }; BFC177042399BC4F0058AC51 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFC177022399BC4F0058AC51 /* MainInterface.storyboard */; }; BFC177082399BC4F0058AC51 /* ClipboardReader.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BFC176FC2399BC4F0058AC51 /* ClipboardReader.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BFC1F39D22AF0D0E003AC21A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F39C22AF0D0E003AC21A /* AppDelegate.swift */; }; BFC1F39F22AF0D0E003AC21A /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F39E22AF0D0E003AC21A /* HistoryViewController.swift */; }; BFC1F3A222AF0D0E003AC21A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFC1F3A022AF0D0E003AC21A /* Main.storyboard */; }; BFC1F3A422AF0D0F003AC21A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFC1F3A322AF0D0F003AC21A /* Assets.xcassets */; }; BFC1F3A722AF0D0F003AC21A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFC1F3A522AF0D0F003AC21A /* LaunchScreen.storyboard */; }; BFC9E65422B1A22700974663 /* ClipKit.h in Headers */ = {isa = PBXBuildFile; fileRef = BFC9E65222B1A22700974663 /* ClipKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; BFC9E65722B1A22700974663 /* ClipKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC9E65022B1A22700974663 /* ClipKit.framework */; platformFilter = ios; }; BFC9E65822B1A22700974663 /* ClipKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC9E65022B1A22700974663 /* ClipKit.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BFC9E66222B1B03900974663 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1D5D4622B0869C0062F474 /* DatabaseManager.swift */; }; BFC9E66322B1B03C00974663 /* PasteboardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1D5D4A22B094DB0062F474 /* PasteboardItem.swift */; }; BFC9E66422B1B03C00974663 /* PasteboardItemRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1D5D4822B088F40062F474 /* PasteboardItemRepresentation.swift */; }; BFC9E66522B1B04000974663 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF1D5D4322B0867C0062F474 /* Model.xcdatamodeld */; }; BFC9E66622B1B1E600974663 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1D5C7522B029DC0062F474 /* Roxas.framework */; }; BFC9E67222B2C4C500974663 /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9E67122B2C4C400974663 /* Result+Conveniences.swift */; }; BFC9E67C22B2CA4400974663 /* CFNotification+PasteboardListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9E67B22B2CA4400974663 /* CFNotification+PasteboardListener.swift */; }; BFDB5B1E22EF7B5000F74113 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1D22EF7B5000F74113 /* GradientView.swift */; }; BFEF44AE2398693300095A92 /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF44AB2398693300095A92 /* ForwardingNavigationController.swift */; }; BFF9E4932555F4F40052B1B2 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF9E4922555F4F40052B1B2 /* LocationManager.swift */; }; D534428E2BAB202700E133EE /* ClippingsMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D534428D2BAB202700E133EE /* ClippingsMapView.swift */; }; D5D375152BAB330200213D84 /* ClippingSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D375142BAB330200213D84 /* ClippingSheet.swift */; }; D5D375192BAB4DF800213D84 /* UNNotification+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D375182BAB4DF800213D84 /* UNNotification+Keys.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ BF1D5C7422B029DC0062F474 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */; proxyType = 2; remoteGlobalIDString = BFADAFF819AE7BB70050CF31; remoteInfo = Roxas; }; BF1D5C7622B029DC0062F474 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */; proxyType = 2; remoteGlobalIDString = BF8624801BB742E700C12EEE; remoteInfo = RoxasTV; }; BF1D5C7822B029DC0062F474 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */; proxyType = 2; remoteGlobalIDString = BFADB00319AE7BB80050CF31; remoteInfo = RoxasTests; }; BF1D5C7C22B029E80062F474 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */; proxyType = 1; remoteGlobalIDString = BFADAFF719AE7BB70050CF31; remoteInfo = Roxas; }; BF7E6FE5247C4FCD0058F4D4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFC1F39122AF0D0E003AC21A /* Project object */; proxyType = 1; remoteGlobalIDString = BF7E6FDF247C4FCD0058F4D4; remoteInfo = ClipBoard; }; BFAD46CC24905E9400451D6F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFC1F39122AF0D0E003AC21A /* Project object */; proxyType = 1; remoteGlobalIDString = BFC9E64F22B1A22700974663; remoteInfo = ClipKit; }; BFC177062399BC4F0058AC51 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFC1F39122AF0D0E003AC21A /* Project object */; proxyType = 1; remoteGlobalIDString = BFC176FB2399BC4F0058AC51; remoteInfo = ClipboardReader; }; BFC9E65522B1A22700974663 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFC1F39122AF0D0E003AC21A /* Project object */; proxyType = 1; remoteGlobalIDString = BFC9E64F22B1A22700974663; remoteInfo = ClipKit; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ BF1D5C6D22B029190062F474 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( BF1D5C7B22B029E70062F474 /* Roxas.framework in Embed Frameworks */, BFC9E65822B1A22700974663 /* ClipKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; BF1D5C9C22B042870062F474 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( BFC177082399BC4F0058AC51 /* ClipboardReader.appex in Embed App Extensions */, BF7E6FE7247C4FCD0058F4D4 /* ClipBoard.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ BF0AB807247C5D1F0090B43B /* Blur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Blur.swift; sourceTree = ""; }; BF0BDE5022B4414A00E1419D /* UTI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTI.swift; sourceTree = ""; }; BF0BDE5522B456A600E1419D /* PasteboardItem+ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PasteboardItem+ActivityItemSource.swift"; sourceTree = ""; }; BF0BDE5722B4757100E1419D /* UserDefaults+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+App.swift"; sourceTree = ""; }; BF0BDE5922B4771300E1419D /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = ""; }; BF1D5C7E22B02BF30062F474 /* PasteboardMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardMonitor.swift; sourceTree = ""; }; BF1D5D4422B0867C0062F474 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; BF1D5D4622B0869C0062F474 /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; BF1D5D4822B088F40062F474 /* PasteboardItemRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardItemRepresentation.swift; sourceTree = ""; }; BF1D5D4A22B094DB0062F474 /* PasteboardItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardItem.swift; sourceTree = ""; }; BF235BC0247D8B5300CCFCB0 /* UIPasteboard+PasteboardItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPasteboard+PasteboardItem.swift"; sourceTree = ""; }; BF50E7D822C2BF010070E17B /* Bundle+AppGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppGroups.swift"; sourceTree = ""; }; BF69A53F2395D44600CF838A /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; BF69A5412395D44700CF838A /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; BF746541247C900100F66F3B /* ClipBoard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ClipBoard.entitlements; sourceTree = ""; }; BF770E6122BC767A002A40FE /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = ""; }; BF7B9EE022B81C980042C873 /* Int+Bytes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Bytes.swift"; sourceTree = ""; }; BF7E6FE0247C4FCD0058F4D4 /* ClipBoard.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ClipBoard.appex; sourceTree = BUILT_PRODUCTS_DIR; }; BF7E6FE2247C4FCD0058F4D4 /* KeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardViewController.swift; sourceTree = ""; }; BF7E6FE4247C4FCD0058F4D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BF7E6FF0247C50540058F4D4 /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; BF7E6FF2247C518D0058F4D4 /* ClippingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClippingCell.swift; sourceTree = ""; }; BF8F76F2254CC0E0005AF18B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; BFA2DF64247DD2B200E31E4D /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; BFA2DF68247DD31500E31E4D /* UIColor+Clip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Clip.swift"; sourceTree = ""; }; BFA2DF6A247DE47900E31E4D /* Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preview.swift; sourceTree = ""; }; BFA714B922C53F1700EE7236 /* ApplicationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationMonitor.swift; sourceTree = ""; }; BFAC49D222B2EDA80011E7C4 /* ClippingTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClippingTableViewCell.xib; sourceTree = ""; }; BFAC49D422B2EDB20011E7C4 /* ClippingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClippingTableViewCell.swift; sourceTree = ""; }; BFAC49DF22B40EC90011E7C4 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; BFAD2C49257AD4D600FF9532 /* PublishedPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedPipeline.swift; sourceTree = ""; }; BFAD46C82490588800451D6F /* UIInputView+Click.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInputView+Click.swift"; sourceTree = ""; }; BFAD46CA249058DE00451D6F /* SwitchKeyboardButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchKeyboardButton.swift; sourceTree = ""; }; BFC176FC2399BC4F0058AC51 /* ClipboardReader.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ClipboardReader.appex; sourceTree = BUILT_PRODUCTS_DIR; }; BFC177002399BC4F0058AC51 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; BFC177032399BC4F0058AC51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; BFC177052399BC4F0058AC51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BFC1770C2399BCF90058AC51 /* ClipboardReader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ClipboardReader.entitlements; sourceTree = ""; }; BFC1F39922AF0D0E003AC21A /* Clip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Clip.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFC1F39C22AF0D0E003AC21A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BFC1F39E22AF0D0E003AC21A /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; BFC1F3A122AF0D0E003AC21A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BFC1F3A322AF0D0F003AC21A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BFC1F3A622AF0D0F003AC21A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; BFC1F3A822AF0D0F003AC21A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BFC9E62C22B18D5900974663 /* Clip.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Clip.entitlements; sourceTree = ""; }; BFC9E65022B1A22700974663 /* ClipKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ClipKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFC9E65222B1A22700974663 /* ClipKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClipKit.h; sourceTree = ""; }; BFC9E65322B1A22700974663 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BFC9E67122B2C4C400974663 /* Result+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = ""; }; BFC9E67B22B2CA4400974663 /* CFNotification+PasteboardListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CFNotification+PasteboardListener.swift"; sourceTree = ""; }; BFDB5B1D22EF7B5000F74113 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; BFEF44AB2398693300095A92 /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = ""; }; BFF9E4922555F4F40052B1B2 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; D534428D2BAB202700E133EE /* ClippingsMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClippingsMapView.swift; sourceTree = ""; }; D5D375142BAB330200213D84 /* ClippingSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClippingSheet.swift; sourceTree = ""; }; D5D375182BAB4DF800213D84 /* UNNotification+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Keys.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ BF7E6FDD247C4FCD0058F4D4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; BFC176F92399BC4F0058AC51 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BFC176FE2399BC4F0058AC51 /* UserNotificationsUI.framework in Frameworks */, BFC176FD2399BC4F0058AC51 /* UserNotifications.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC1F39622AF0D0E003AC21A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BF1D5C7A22B029E70062F474 /* Roxas.framework in Frameworks */, BFC9E65722B1A22700974663 /* ClipKit.framework in Frameworks */, BFAC49E022B40EC90011E7C4 /* AudioToolbox.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC9E64D22B1A22700974663 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BFC9E66622B1B1E600974663 /* Roxas.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ BF0BDE5422B4568A00E1419D /* Extensions */ = { isa = PBXGroup; children = ( BF0BDE5522B456A600E1419D /* PasteboardItem+ActivityItemSource.swift */, BF770E6122BC767A002A40FE /* UIDevice+Vibration.swift */, ); path = Extensions; sourceTree = ""; }; BF0BDE6622B4871400E1419D /* Settings */ = { isa = PBXGroup; children = ( BF0BDE5922B4771300E1419D /* SettingsViewController.swift */, ); path = Settings; sourceTree = ""; }; BF1D5C6722B029090062F474 /* Dependencies */ = { isa = PBXGroup; children = ( BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */, ); name = Dependencies; sourceTree = ""; }; BF1D5C6F22B029DB0062F474 /* Products */ = { isa = PBXGroup; children = ( BF1D5C7522B029DC0062F474 /* Roxas.framework */, BF1D5C7722B029DC0062F474 /* Roxas.framework */, BF1D5C7922B029DC0062F474 /* RoxasTests.xctest */, ); name = Products; sourceTree = ""; }; BF1D5C8122B038030062F474 /* Resources */ = { isa = PBXGroup; children = ( BFC1F3A322AF0D0F003AC21A /* Assets.xcassets */, ); path = Resources; sourceTree = ""; }; BF1D5C8522B038FE0062F474 /* Supporting Files */ = { isa = PBXGroup; children = ( BFC1F3A522AF0D0F003AC21A /* LaunchScreen.storyboard */, BFC1F3A822AF0D0F003AC21A /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; BF1D5CDF22B063E00062F474 /* Frameworks */ = { isa = PBXGroup; children = ( BFAC49DF22B40EC90011E7C4 /* AudioToolbox.framework */, BF69A53F2395D44600CF838A /* UserNotifications.framework */, BF69A5412395D44700CF838A /* UserNotificationsUI.framework */, ); name = Frameworks; sourceTree = ""; }; BF1D5D4122B083210062F474 /* Pasteboard */ = { isa = PBXGroup; children = ( BF1D5C7E22B02BF30062F474 /* PasteboardMonitor.swift */, BFF9E4922555F4F40052B1B2 /* LocationManager.swift */, ); path = Pasteboard; sourceTree = ""; }; BF7E6FE1247C4FCD0058F4D4 /* ClipBoard */ = { isa = PBXGroup; children = ( BF746541247C900100F66F3B /* ClipBoard.entitlements */, BF7E6FE2247C4FCD0058F4D4 /* KeyboardViewController.swift */, BF7E6FE4247C4FCD0058F4D4 /* Info.plist */, ); path = ClipBoard; sourceTree = ""; }; BF7E6FEF247C50480058F4D4 /* SwiftUI */ = { isa = PBXGroup; children = ( BFA2DF6A247DE47900E31E4D /* Preview.swift */, BF7E6FF0247C50540058F4D4 /* Keyboard.swift */, BF7E6FF2247C518D0058F4D4 /* ClippingCell.swift */, BFAD46CA249058DE00451D6F /* SwitchKeyboardButton.swift */, BF0AB807247C5D1F0090B43B /* Blur.swift */, ); path = SwiftUI; sourceTree = ""; }; BFA2DF63247DD2A500E31E4D /* Resources */ = { isa = PBXGroup; children = ( BFA2DF64247DD2B200E31E4D /* Colors.xcassets */, ); path = Resources; sourceTree = ""; }; BFAC49D722B2EF170011E7C4 /* History */ = { isa = PBXGroup; children = ( BFC1F39E22AF0D0E003AC21A /* HistoryViewController.swift */, BFAC49D422B2EDB20011E7C4 /* ClippingTableViewCell.swift */, BFAC49D222B2EDA80011E7C4 /* ClippingTableViewCell.xib */, ); path = History; sourceTree = ""; }; BFAD2C48257AD4CB00FF9532 /* Types */ = { isa = PBXGroup; children = ( BFAD2C49257AD4D600FF9532 /* PublishedPipeline.swift */, ); path = Types; sourceTree = ""; }; BFC176FF2399BC4F0058AC51 /* ClipboardReader */ = { isa = PBXGroup; children = ( BFC1770C2399BCF90058AC51 /* ClipboardReader.entitlements */, BFC177002399BC4F0058AC51 /* NotificationViewController.swift */, BFC177022399BC4F0058AC51 /* MainInterface.storyboard */, BFC177052399BC4F0058AC51 /* Info.plist */, ); path = ClipboardReader; sourceTree = ""; }; BFC1F39022AF0D0E003AC21A = { isa = PBXGroup; children = ( BFC1F39B22AF0D0E003AC21A /* Clip */, BFC9E65122B1A22700974663 /* ClipKit */, BFC176FF2399BC4F0058AC51 /* ClipboardReader */, BF7E6FE1247C4FCD0058F4D4 /* ClipBoard */, BF1D5C6722B029090062F474 /* Dependencies */, BFC1F39A22AF0D0E003AC21A /* Products */, BF1D5CDF22B063E00062F474 /* Frameworks */, ); sourceTree = ""; }; BFC1F39A22AF0D0E003AC21A /* Products */ = { isa = PBXGroup; children = ( BFC1F39922AF0D0E003AC21A /* Clip.app */, BFC9E65022B1A22700974663 /* ClipKit.framework */, BFC176FC2399BC4F0058AC51 /* ClipboardReader.appex */, BF7E6FE0247C4FCD0058F4D4 /* ClipBoard.appex */, ); name = Products; sourceTree = ""; }; BFC1F39B22AF0D0E003AC21A /* Clip */ = { isa = PBXGroup; children = ( BFC9E62C22B18D5900974663 /* Clip.entitlements */, BFC1F39C22AF0D0E003AC21A /* AppDelegate.swift */, BF8F76F2254CC0E0005AF18B /* SceneDelegate.swift */, BFA714B922C53F1700EE7236 /* ApplicationMonitor.swift */, BFC1F3A022AF0D0E003AC21A /* Main.storyboard */, BFAC49D722B2EF170011E7C4 /* History */, D53442912BAB202A00E133EE /* Map View */, BF0BDE6622B4871400E1419D /* Settings */, BF1D5D4122B083210062F474 /* Pasteboard */, BFDB5B2322EF8F9600F74113 /* Components */, BFAD2C48257AD4CB00FF9532 /* Types */, BF0BDE5422B4568A00E1419D /* Extensions */, BF1D5C8122B038030062F474 /* Resources */, BF1D5C8522B038FE0062F474 /* Supporting Files */, ); path = Clip; sourceTree = ""; }; BFC9E65122B1A22700974663 /* ClipKit */ = { isa = PBXGroup; children = ( BFC9E65222B1A22700974663 /* ClipKit.h */, BF0BDE5022B4414A00E1419D /* UTI.swift */, BF7E6FEF247C50480058F4D4 /* SwiftUI */, BFC9E67022B1DE0A00974663 /* Database */, BFC9E67322B2C6C500974663 /* Extensions */, BFA2DF63247DD2A500E31E4D /* Resources */, BFC9E65322B1A22700974663 /* Info.plist */, ); path = ClipKit; sourceTree = ""; }; BFC9E67022B1DE0A00974663 /* Database */ = { isa = PBXGroup; children = ( BF1D5D4622B0869C0062F474 /* DatabaseManager.swift */, BFC9E67A22B2C85C00974663 /* Model */, ); path = Database; sourceTree = ""; }; BFC9E67322B2C6C500974663 /* Extensions */ = { isa = PBXGroup; children = ( BFC9E67B22B2CA4400974663 /* CFNotification+PasteboardListener.swift */, BFC9E67122B2C4C400974663 /* Result+Conveniences.swift */, BF0BDE5722B4757100E1419D /* UserDefaults+App.swift */, BF7B9EE022B81C980042C873 /* Int+Bytes.swift */, BF50E7D822C2BF010070E17B /* Bundle+AppGroups.swift */, BF235BC0247D8B5300CCFCB0 /* UIPasteboard+PasteboardItem.swift */, BFA2DF68247DD31500E31E4D /* UIColor+Clip.swift */, BFAD46C82490588800451D6F /* UIInputView+Click.swift */, D5D375182BAB4DF800213D84 /* UNNotification+Keys.swift */, ); path = Extensions; sourceTree = ""; }; BFC9E67A22B2C85C00974663 /* Model */ = { isa = PBXGroup; children = ( BF1D5D4322B0867C0062F474 /* Model.xcdatamodeld */, BF1D5D4A22B094DB0062F474 /* PasteboardItem.swift */, BF1D5D4822B088F40062F474 /* PasteboardItemRepresentation.swift */, ); path = Model; sourceTree = ""; }; BFDB5B2322EF8F9600F74113 /* Components */ = { isa = PBXGroup; children = ( BFDB5B1D22EF7B5000F74113 /* GradientView.swift */, BFEF44AB2398693300095A92 /* ForwardingNavigationController.swift */, ); path = Components; sourceTree = ""; }; D53442912BAB202A00E133EE /* Map View */ = { isa = PBXGroup; children = ( D534428D2BAB202700E133EE /* ClippingsMapView.swift */, D5D375142BAB330200213D84 /* ClippingSheet.swift */, ); path = "Map View"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ BFC9E64B22B1A22700974663 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( BFC9E65422B1A22700974663 /* ClipKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ BF7E6FDF247C4FCD0058F4D4 /* ClipBoard */ = { isa = PBXNativeTarget; buildConfigurationList = BF7E6FEC247C4FCD0058F4D4 /* Build configuration list for PBXNativeTarget "ClipBoard" */; buildPhases = ( BF7E6FDC247C4FCD0058F4D4 /* Sources */, BF7E6FDD247C4FCD0058F4D4 /* Frameworks */, ); buildRules = ( ); dependencies = ( BFAD46CD24905E9400451D6F /* PBXTargetDependency */, ); name = ClipBoard; productName = ClipBoard; productReference = BF7E6FE0247C4FCD0058F4D4 /* ClipBoard.appex */; productType = "com.apple.product-type.app-extension"; }; BFC176FB2399BC4F0058AC51 /* ClipboardReader */ = { isa = PBXNativeTarget; buildConfigurationList = BFC177092399BC4F0058AC51 /* Build configuration list for PBXNativeTarget "ClipboardReader" */; buildPhases = ( BFC176F82399BC4F0058AC51 /* Sources */, BFC176F92399BC4F0058AC51 /* Frameworks */, BFC176FA2399BC4F0058AC51 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ClipboardReader; productName = ClipboardReader; productReference = BFC176FC2399BC4F0058AC51 /* ClipboardReader.appex */; productType = "com.apple.product-type.app-extension"; }; BFC1F39822AF0D0E003AC21A /* Clip */ = { isa = PBXNativeTarget; buildConfigurationList = BFC1F3AB22AF0D0F003AC21A /* Build configuration list for PBXNativeTarget "Clip" */; buildPhases = ( BFC1F39522AF0D0E003AC21A /* Sources */, BFC1F39622AF0D0E003AC21A /* Frameworks */, BFC1F39722AF0D0E003AC21A /* Resources */, BF1D5C6D22B029190062F474 /* Embed Frameworks */, BF1D5C9C22B042870062F474 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( BF1D5C7D22B029E80062F474 /* PBXTargetDependency */, BFC9E65622B1A22700974663 /* PBXTargetDependency */, BFC177072399BC4F0058AC51 /* PBXTargetDependency */, BF7E6FE6247C4FCD0058F4D4 /* PBXTargetDependency */, ); name = Clip; productName = ClipboardManager; productReference = BFC1F39922AF0D0E003AC21A /* Clip.app */; productType = "com.apple.product-type.application"; }; BFC9E64F22B1A22700974663 /* ClipKit */ = { isa = PBXNativeTarget; buildConfigurationList = BFC9E65922B1A22700974663 /* Build configuration list for PBXNativeTarget "ClipKit" */; buildPhases = ( BFC9E64B22B1A22700974663 /* Headers */, BFC9E64C22B1A22700974663 /* Sources */, BFC9E64D22B1A22700974663 /* Frameworks */, BFC9E64E22B1A22700974663 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ClipKit; productName = ClipKit; productReference = BFC9E65022B1A22700974663 /* ClipKit.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ BFC1F39122AF0D0E003AC21A /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Riley Testut"; TargetAttributes = { BF7E6FDF247C4FCD0058F4D4 = { CreatedOnToolsVersion = 11.5; }; BFC176FB2399BC4F0058AC51 = { CreatedOnToolsVersion = 11.1; }; BFC1F39822AF0D0E003AC21A = { CreatedOnToolsVersion = 10.2.1; LastSwiftMigration = 1120; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 1; }; com.apple.BackgroundModes = { enabled = 1; }; com.apple.InterAppAudio = { enabled = 1; }; }; }; BFC9E64F22B1A22700974663 = { CreatedOnToolsVersion = 10.2.1; LastSwiftMigration = 1020; }; }; }; buildConfigurationList = BFC1F39422AF0D0E003AC21A /* Build configuration list for PBXProject "Clip" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = BFC1F39022AF0D0E003AC21A; productRefGroup = BFC1F39A22AF0D0E003AC21A /* Products */; projectDirPath = ""; projectReferences = ( { ProductGroup = BF1D5C6F22B029DB0062F474 /* Products */; ProjectRef = BF1D5C6E22B029DB0062F474 /* Roxas.xcodeproj */; }, ); projectRoot = ""; targets = ( BFC1F39822AF0D0E003AC21A /* Clip */, BFC9E64F22B1A22700974663 /* ClipKit */, BFC176FB2399BC4F0058AC51 /* ClipboardReader */, BF7E6FDF247C4FCD0058F4D4 /* ClipBoard */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ BF1D5C7522B029DC0062F474 /* Roxas.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Roxas.framework; remoteRef = BF1D5C7422B029DC0062F474 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; BF1D5C7722B029DC0062F474 /* Roxas.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Roxas.framework; remoteRef = BF1D5C7622B029DC0062F474 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; BF1D5C7922B029DC0062F474 /* RoxasTests.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; path = RoxasTests.xctest; remoteRef = BF1D5C7822B029DC0062F474 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ BFC176FA2399BC4F0058AC51 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( BFC177042399BC4F0058AC51 /* MainInterface.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC1F39722AF0D0E003AC21A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( BFC1F3A722AF0D0F003AC21A /* LaunchScreen.storyboard in Resources */, BFAC49D322B2EDA80011E7C4 /* ClippingTableViewCell.xib in Resources */, BFC1F3A422AF0D0F003AC21A /* Assets.xcassets in Resources */, BFC1F3A222AF0D0E003AC21A /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC9E64E22B1A22700974663 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( BFA2DF65247DD2B200E31E4D /* Colors.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ BF7E6FDC247C4FCD0058F4D4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( BF7E6FE3247C4FCD0058F4D4 /* KeyboardViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC176F82399BC4F0058AC51 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( BFC177012399BC4F0058AC51 /* NotificationViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC1F39522AF0D0E003AC21A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( BF1D5C7F22B02BF30062F474 /* PasteboardMonitor.swift in Sources */, BFA714BA22C53F1700EE7236 /* ApplicationMonitor.swift in Sources */, BFDB5B1E22EF7B5000F74113 /* GradientView.swift in Sources */, BFC1F39F22AF0D0E003AC21A /* HistoryViewController.swift in Sources */, BFF9E4932555F4F40052B1B2 /* LocationManager.swift in Sources */, D5D375152BAB330200213D84 /* ClippingSheet.swift in Sources */, BF8F76F3254CC0E0005AF18B /* SceneDelegate.swift in Sources */, D534428E2BAB202700E133EE /* ClippingsMapView.swift in Sources */, BF770E6522BC7688002A40FE /* UIDevice+Vibration.swift in Sources */, BFC1F39D22AF0D0E003AC21A /* AppDelegate.swift in Sources */, BFEF44AE2398693300095A92 /* ForwardingNavigationController.swift in Sources */, BF0BDE5622B456A600E1419D /* PasteboardItem+ActivityItemSource.swift in Sources */, BFAC49D522B2EDB20011E7C4 /* ClippingTableViewCell.swift in Sources */, BFAD2C4A257AD4D600FF9532 /* PublishedPipeline.swift in Sources */, BF0BDE5A22B4771300E1419D /* SettingsViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; BFC9E64C22B1A22700974663 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( BFA2DF69247DD31500E31E4D /* UIColor+Clip.swift in Sources */, BF0BDE5E22B47D4900E1419D /* UserDefaults+App.swift in Sources */, BFC9E67C22B2CA4400974663 /* CFNotification+PasteboardListener.swift in Sources */, BFC9E66222B1B03900974663 /* DatabaseManager.swift in Sources */, BF0BDE5122B4414A00E1419D /* UTI.swift in Sources */, BF235BC1247D8B5300CCFCB0 /* UIPasteboard+PasteboardItem.swift in Sources */, BFAD46C92490588800451D6F /* UIInputView+Click.swift in Sources */, BFA2DF61247DD23800E31E4D /* ClippingCell.swift in Sources */, BFC9E66422B1B03C00974663 /* PasteboardItemRepresentation.swift in Sources */, BFC9E66522B1B04000974663 /* Model.xcdatamodeld in Sources */, BFAD46CB249058DE00451D6F /* SwitchKeyboardButton.swift in Sources */, BFC9E67222B2C4C500974663 /* Result+Conveniences.swift in Sources */, BFA2DF6B247DE47900E31E4D /* Preview.swift in Sources */, BFA2DF62247DD23C00E31E4D /* Blur.swift in Sources */, BF7B9EE122B81C980042C873 /* Int+Bytes.swift in Sources */, BFC9E66322B1B03C00974663 /* PasteboardItem.swift in Sources */, D5D375192BAB4DF800213D84 /* UNNotification+Keys.swift in Sources */, BFA2DF60247DD21F00E31E4D /* Keyboard.swift in Sources */, BF50E7D922C2BF010070E17B /* Bundle+AppGroups.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ BF1D5C7D22B029E80062F474 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Roxas; platformFilter = ios; targetProxy = BF1D5C7C22B029E80062F474 /* PBXContainerItemProxy */; }; BF7E6FE6247C4FCD0058F4D4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BF7E6FDF247C4FCD0058F4D4 /* ClipBoard */; targetProxy = BF7E6FE5247C4FCD0058F4D4 /* PBXContainerItemProxy */; }; BFAD46CD24905E9400451D6F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BFC9E64F22B1A22700974663 /* ClipKit */; targetProxy = BFAD46CC24905E9400451D6F /* PBXContainerItemProxy */; }; BFC177072399BC4F0058AC51 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BFC176FB2399BC4F0058AC51 /* ClipboardReader */; targetProxy = BFC177062399BC4F0058AC51 /* PBXContainerItemProxy */; }; BFC9E65622B1A22700974663 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; target = BFC9E64F22B1A22700974663 /* ClipKit */; targetProxy = BFC9E65522B1A22700974663 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ BFC177022399BC4F0058AC51 /* MainInterface.storyboard */ = { isa = PBXVariantGroup; children = ( BFC177032399BC4F0058AC51 /* Base */, ); name = MainInterface.storyboard; sourceTree = ""; }; BFC1F3A022AF0D0E003AC21A /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( BFC1F3A122AF0D0E003AC21A /* Base */, ); name = Main.storyboard; sourceTree = ""; }; BFC1F3A522AF0D0F003AC21A /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( BFC1F3A622AF0D0F003AC21A /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ BF7E6FE8247C4FCD0058F4D4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = ClipBoard/ClipBoard.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = ClipBoard/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip.ClipBoard; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; BF7E6FE9247C4FCD0058F4D4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = ClipBoard/ClipBoard.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = ClipBoard/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip.ClipBoard; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; BFC1770A2399BC4F0058AC51 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = ClipboardReader/ClipboardReader.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = ClipboardReader/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip.ClipboardReader; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; BFC1770B2399BC4F0058AC51 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = ClipboardReader/ClipboardReader.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = ClipboardReader/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip.ClipboardReader; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; BFC1F3A922AF0D0F003AC21A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 14; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; BFC1F3AA22AF0D0F003AC21A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 14; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; BFC1F3AC22AF0D0F003AC21A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Clip/Clip.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = Clip/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; BFC1F3AD22AF0D0F003AC21A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Clip/Clip.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; INFOPLIST_FILE = Clip/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Clip; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; BFC9E65A22B1A22700974663 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 6XVY5G3U44; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ClipKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.ClipKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; BFC9E65B22B1A22700974663 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 6XVY5G3U44; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ClipKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.ClipKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ BF7E6FEC247C4FCD0058F4D4 /* Build configuration list for PBXNativeTarget "ClipBoard" */ = { isa = XCConfigurationList; buildConfigurations = ( BF7E6FE8247C4FCD0058F4D4 /* Debug */, BF7E6FE9247C4FCD0058F4D4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; BFC177092399BC4F0058AC51 /* Build configuration list for PBXNativeTarget "ClipboardReader" */ = { isa = XCConfigurationList; buildConfigurations = ( BFC1770A2399BC4F0058AC51 /* Debug */, BFC1770B2399BC4F0058AC51 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; BFC1F39422AF0D0E003AC21A /* Build configuration list for PBXProject "Clip" */ = { isa = XCConfigurationList; buildConfigurations = ( BFC1F3A922AF0D0F003AC21A /* Debug */, BFC1F3AA22AF0D0F003AC21A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; BFC1F3AB22AF0D0F003AC21A /* Build configuration list for PBXNativeTarget "Clip" */ = { isa = XCConfigurationList; buildConfigurations = ( BFC1F3AC22AF0D0F003AC21A /* Debug */, BFC1F3AD22AF0D0F003AC21A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; BFC9E65922B1A22700974663 /* Build configuration list for PBXNativeTarget "ClipKit" */ = { isa = XCConfigurationList; buildConfigurations = ( BFC9E65A22B1A22700974663 /* Debug */, BFC9E65B22B1A22700974663 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ BF1D5D4322B0867C0062F474 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( BF1D5D4422B0867C0062F474 /* Model.xcdatamodel */, ); currentVersion = BF1D5D4422B0867C0062F474 /* Model.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; rootObject = BFC1F39122AF0D0E003AC21A /* Project object */; } ================================================ FILE: Clip.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Clip.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Clip.xcodeproj/xcshareddata/xcschemes/Clip.xcscheme ================================================ ================================================ FILE: Clip.xcodeproj/xcshareddata/xcschemes/ClipKit.xcscheme ================================================ ================================================ FILE: Clip.xcodeproj/xcshareddata/xcschemes/ClipboardReader.xcscheme ================================================ ================================================ FILE: ClipBoard/ClipBoard.entitlements ================================================ com.apple.security.application-groups group.com.rileytestut.Clip ================================================ FILE: ClipBoard/Info.plist ================================================ ALTAppGroups group.com.rileytestut.Clip CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName ClipBoard CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion 1 NSExtension NSExtensionAttributes IsASCIICapable PrefersRightToLeft PrimaryLanguage en-US RequestsOpenAccess NSExtensionPointIdentifier com.apple.keyboard-service NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).KeyboardViewController ================================================ FILE: ClipBoard/KeyboardViewController.swift ================================================ // // KeyboardViewController.swift // ClipBoard // // Created by Riley Testut on 5/25/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit import SwiftUI import Roxas import ClipKit class KeyboardViewController: UIInputViewController { private var hostingViewController: UIHostingController! private var heightConstraint: NSLayoutConstraint! override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) if DatabaseManager.shared.persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty { if !self.hasFullAccess { // Use temporary in-memory store if we don't have full access. let inMemoryStoreDescription = NSPersistentStoreDescription() inMemoryStoreDescription.type = NSInMemoryStoreType DatabaseManager.shared.persistentContainer.persistentStoreDescriptions = [inMemoryStoreDescription] } DatabaseManager.shared.persistentContainer.shouldAddStoresAsynchronously = false DatabaseManager.shared.prepare { (result) in switch result { case .failure(let error): print("Failed to prepare database:", error) case .success: break } } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.inputView?.allowsSelfSizing = true let rootView = Keyboard(inputViewController: self) .environment(\.managedObjectContext, DatabaseManager.shared.persistentContainer.viewContext) self.hostingViewController = UIHostingController(rootView: AnyView(rootView)) self.hostingViewController.view.backgroundColor = .clear self.addChild(self.hostingViewController) self.inputView?.addSubview(self.hostingViewController.view, pinningEdgesWith: .zero) self.hostingViewController.didMove(toParent: self) self.view.setNeedsUpdateConstraints() } override func updateViewConstraints() { super.updateViewConstraints() if self.heightConstraint == nil { for constraint in self.view.constraintsAffectingLayout(for: .vertical) { // UIKit embeds height constraint, even if allowsSelfSizing is true. // Must set to non-required priority, or else it will conflict with // our own self-sizing constraint (annoyingly). constraint.priority = .defaultHigh } self.heightConstraint = self.view.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height / 2) self.heightConstraint.isActive = true } } override func viewWillLayoutSubviews() { if let heightConstraint = self.heightConstraint { heightConstraint.constant = UIScreen.main.bounds.height / 2 } super.viewWillLayoutSubviews() } } ================================================ FILE: ClipKit/ClipKit.h ================================================ // // ClipKit.h // ClipKit // // Created by Riley Testut on 6/12/19. // Copyright © 2019 Riley Testut. All rights reserved. // #import //! Project version number for ClipKit. FOUNDATION_EXPORT double ClipKitVersionNumber; //! Project version string for ClipKit. FOUNDATION_EXPORT const unsigned char ClipKitVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: ClipKit/Database/DatabaseManager.swift ================================================ // // DatabaseManager.swift // Clip // // Created by Riley Testut on 6/11/19. // Copyright © 2019 Riley Testut. All rights reserved. // import CoreData import CoreLocation import Roxas private extension UserDefaults { @NSManaged var previousHistoryToken: Data? } public enum PasteboardError: LocalizedError { case unsupportedImageFormat case unsupportedItem case noItem case duplicateItem public var errorDescription: String? { switch self { case .unsupportedImageFormat: return NSLocalizedString("Unsupported image format.", comment: "") case .unsupportedItem: return NSLocalizedString("Unsupported clipboard item.", comment: "") case .noItem: return NSLocalizedString("No clipboard item.", comment: "") case .duplicateItem: return NSLocalizedString("Duplicate item.", comment: "") } } } private class PersistentContainer: RSTPersistentContainer { override class func defaultDirectoryURL() -> URL { guard let appGroup = Bundle.main.appGroups.first else { return super.defaultDirectoryURL() } let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)! let databaseDirectoryURL = sharedDirectoryURL.appendingPathComponent("Database") try? FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil) print("Database URL:", databaseDirectoryURL) return databaseDirectoryURL } } public class DatabaseManager { public static let shared = DatabaseManager() public let persistentContainer: RSTPersistentContainer = PersistentContainer(name: "Model", bundle: Bundle(for: DatabaseManager.self)) public private(set) var isStarted = false private var prepareCompletionHandlers = [(Result) -> Void]() private let dispatchQueue = DispatchQueue(label: "com.rileytestut.Clip.DatabaseManager") private var previousHistoryToken: NSPersistentHistoryToken? { set { guard let value = newValue else { UserDefaults.shared.previousHistoryToken = nil return } let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true) UserDefaults.shared.previousHistoryToken = data } get { guard let data = UserDefaults.shared.previousHistoryToken else { return nil } let token = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data) return token } } private init() { } public func prepare(completionHandler: @escaping (Result) -> Void) { func finish(_ result: Result) { self.dispatchQueue.async { switch result { case .success: self.isStarted = true case .failure: break } self.prepareCompletionHandlers.forEach { $0(result) } self.prepareCompletionHandlers.removeAll() } } self.dispatchQueue.async { self.prepareCompletionHandlers.append(completionHandler) guard self.prepareCompletionHandlers.count == 1 else { return } guard !self.isStarted else { return finish(.success(())) } self.persistentContainer.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) self.persistentContainer.loadPersistentStores { (description, error) in let result = Result(description, error).map { _ in () } finish(result) self.purge() } } } public func refresh() { DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.previousHistoryToken) do { guard let result = try context.execute(fetchRequest) as? NSPersistentHistoryResult, let transactions = result.result as? [NSPersistentHistoryTransaction] else { return } DispatchQueue.main.async { self.persistentContainer.viewContext.undoManager?.disableUndoRegistration() defer { self.persistentContainer.viewContext.undoManager?.enableUndoRegistration() } for transaction in transactions { self.persistentContainer.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification()) } if let token = transactions.last?.token { self.previousHistoryToken = token } } } catch let error as CocoaError where error.code.rawValue == NSPersistentHistoryTokenExpiredError { self.previousHistoryToken = nil self.refresh() } catch { print("Failed to fetch change history.", error) } } } public func purge() { // In-memory contexts don't support history tracking. guard let description = DatabaseManager.shared.persistentContainer.persistentStoreDescriptions.first, description.type != NSInMemoryStoreType else { return } DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in if let token = self.previousHistoryToken { let deleteHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: token) do { try context.execute(deleteHistoryRequest) } catch { print("Failed to delete persistent distory.", error) } } do { let fetchRequest = PasteboardItem.historyFetchRequest() as! NSFetchRequest fetchRequest.resultType = .managedObjectIDResultType let objectIDs = try context.fetch(fetchRequest) let deletionFetchRequest = PasteboardItem.fetchRequest() as NSFetchRequest deletionFetchRequest.predicate = NSPredicate(format: "NOT (SELF IN %@)", objectIDs) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: deletionFetchRequest) batchDeleteRequest.resultType = .resultTypeObjectIDs guard let result = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult, let deletedObjectIDs = result.result as? [NSManagedObjectID] else { return } let changes = [NSDeletedObjectsKey: deletedObjectIDs] DispatchQueue.main.async { NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [DatabaseManager.shared.persistentContainer.viewContext]) } } catch { print("Failed to delete pasteboard items.", error) } } } } public extension DatabaseManager { func savePasteboard(location: CLLocation?, completionHandler: @escaping (Result) -> Void) { do { guard !UIPasteboard.general.hasColors else { throw PasteboardError.unsupportedItem // Accessing UIPasteboard.items causes crash as of iOS 12.3 if it contains a UIColor. } print("Did update pasteboard!") guard let itemProvider = UIPasteboard.general.itemProviders.first else { throw PasteboardError.noItem } guard !itemProvider.registeredTypeIdentifiers.contains(UTI.clipping) else { throw PasteboardError.duplicateItem } // Ignore copies that we made from the app. let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() PasteboardItemRepresentation.representations(for: itemProvider, in: context) { (representations) in do { guard let pasteboardItem = PasteboardItem(representations: representations, context: context) else { throw PasteboardError.noItem } pasteboardItem.location = location print(pasteboardItem) let fetchRequest = PasteboardItem.fetchRequest() as NSFetchRequest fetchRequest.predicate = NSPredicate(format: "%K == NO", #keyPath(PasteboardItem.isMarkedForDeletion)) fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \PasteboardItem.date, ascending: false)] fetchRequest.relationshipKeyPathsForPrefetching = ["representations"] fetchRequest.includesPendingChanges = false fetchRequest.fetchLimit = 1 if let previousItem = try context.fetch(fetchRequest).first { let representations = pasteboardItem.representations.reduce(into: [:], { ($0[$1.type] = $1.value as? NSObject) }) let previousRepresentations = previousItem.representations.reduce(into: [:], { ($0[$1.type] = $1.value as? NSObject) }) guard representations != previousRepresentations else { throw PasteboardError.duplicateItem } } guard let _ = pasteboardItem.preferredRepresentation else { throw PasteboardError.unsupportedItem } context.transactionAuthor = Bundle.main.bundleIdentifier try context.save() let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterPostNotification(center, .didChangePasteboard, nil, nil, true) DispatchQueue.main.async { completionHandler(.success(())) } } catch { DispatchQueue.main.async { print("Failed to handle pasteboard item.", error) completionHandler(.failure(error)) } } } } catch { completionHandler(.failure(error)) } } } ================================================ FILE: ClipKit/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents ================================================ ================================================ FILE: ClipKit/Database/Model/PasteboardItem.swift ================================================ // // PasteboardItem.swift // Clip // // Created by Riley Testut on 6/11/19. // Copyright © 2019 Riley Testut. All rights reserved. // import CoreData import MobileCoreServices import CoreLocation private extension PasteboardItemRepresentation.RepresentationType { var priority: Int { switch self { case .attributedText: return 0 case .text: return 1 case .url: return 2 case .image: return 3 } } } @objc(PasteboardItem) public class PasteboardItem: NSManagedObject, Identifiable { /* Properties */ @NSManaged public private(set) var date: Date @NSManaged public var isMarkedForDeletion: Bool public var location: CLLocation? { get { guard let latitude, let longitude else { return nil } let coordinate = CLLocation(latitude: latitude.doubleValue, longitude: longitude.doubleValue) return coordinate } set { self.latitude = newValue?.coordinate.latitude as? NSNumber self.longitude = newValue?.coordinate.longitude as? NSNumber } } @NSManaged private var latitude: NSNumber? @NSManaged private var longitude: NSNumber? /* Relationships */ @nonobjc public var representations: [PasteboardItemRepresentation] { return self._representations.array as! [PasteboardItemRepresentation] } @NSManaged @objc(representations) private var _representations: NSOrderedSet @NSManaged public var preferredRepresentation: PasteboardItemRepresentation? private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) } public init?(representations: [PasteboardItemRepresentation], context: NSManagedObjectContext) { guard !representations.isEmpty else { return nil } super.init(entity: PasteboardItem.entity(), insertInto: context) self._representations = NSOrderedSet(array: representations) let prioritizedRepresentationTypes = PasteboardItemRepresentation.RepresentationType.allCases.sorted { $0.priority > $1.priority } for type in prioritizedRepresentationTypes { guard let representation = representations.first(where: { $0.type == type }) else { continue } self.preferredRepresentation = representation break } } override public func awakeFromInsert() { super.awakeFromInsert() self.date = Date() } } public extension PasteboardItem { @nonobjc class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "PasteboardItem") } class func historyFetchRequest() -> NSFetchRequest { let fetchRequest = PasteboardItem.fetchRequest() as NSFetchRequest fetchRequest.predicate = NSPredicate(format: "%K == NO", #keyPath(PasteboardItem.isMarkedForDeletion)) fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \PasteboardItem.date, ascending: false)] fetchRequest.fetchLimit = UserDefaults.shared.historyLimit.rawValue return fetchRequest } } // SwiftUI extension PasteboardItem { class func make(item: NSItemProviderWriting, date: Date = Date(), context: NSManagedObjectContext) -> PasteboardItem { let itemProvider = NSItemProvider(object: item) let semaphore = DispatchSemaphore(value: 0) let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() var objectID: NSManagedObjectID! PasteboardItemRepresentation.representations(for: itemProvider, in: childContext) { (representations) in let item = PasteboardItem(representations: representations, context: childContext)! item.date = date try! childContext.obtainPermanentIDs(for: [item]) objectID = item.objectID try! childContext.save() semaphore.signal() } semaphore.wait() let pasteboardItem = context.object(with: objectID) as! PasteboardItem return pasteboardItem } } ================================================ FILE: ClipKit/Database/Model/PasteboardItemRepresentation.swift ================================================ // // PasteboardItemRepresentation.swift // Clip // // Created by Riley Testut on 6/11/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import CoreData import MobileCoreServices extension PasteboardItemRepresentation { @objc public enum RepresentationType: Int16, CaseIterable { case text case attributedText case url case image public var localizedName: String { switch self { case .text: return NSLocalizedString("Text", comment: "") case .attributedText: return NSLocalizedString("Text", comment: "") case .url: return NSLocalizedString("URL", comment: "") case .image: return NSLocalizedString("Image", comment: "") } } } } @objc(PasteboardItemRepresentation) public class PasteboardItemRepresentation: NSManagedObject { /* Properties */ @NSManaged public private(set) var uti: String @NSManaged public private(set) var type: RepresentationType @NSManaged private var data: Data? @NSManaged private var string: String? @NSManaged private var url: URL? /* Relationships */ @NSManaged public var item: PasteboardItem? @NSManaged private var preferringItem: PasteboardItem? // Inverse of PasteboardItem.preferredRepresentation. private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) } private init(uti: String, type: RepresentationType, context: NSManagedObjectContext) { super.init(entity: PasteboardItemRepresentation.entity(), insertInto: context) self.uti = uti self.type = type } private convenience init(uti: String, text: String, context: NSManagedObjectContext) { self.init(uti: uti, type: .text, context: context) self.string = text } private convenience init(uti: String, data: Data, type: RepresentationType, context: NSManagedObjectContext) { self.init(uti: uti, type: type, context: context) self.data = data } private convenience init(uti: String, url: URL, context: NSManagedObjectContext) { self.init(uti: uti, type: .url, context: context) self.url = url } public static func representations(for itemProvider: NSItemProvider, in context: NSManagedObjectContext, completionHandler: @escaping ([PasteboardItemRepresentation]) -> Void) { var representations = [PasteboardItemRepresentation]() let dispatchGroup = DispatchGroup() let supportedTextUTIs = [kUTTypeUTF8PlainText, kUTTypePlainText, kUTTypeText] if let uti = supportedTextUTIs.first(where: { itemProvider.hasItemConformingToTypeIdentifier($0 as String) }), itemProvider.canLoadObject(ofClass: NSString.self) { dispatchGroup.enter() itemProvider.loadObject(ofClass: NSString.self) { (text, error) in context.perform { switch Result(text, error) { case .failure(let error): print(error) case .success(let text): let representation = PasteboardItemRepresentation(uti: uti as String, text: text as! String, context: context) representations.append(representation) } dispatchGroup.leave() } } } let supportedAttributedTextUTIs = [kUTTypeRTF, kUTTypeHTML, kUTTypeFlatRTFD, kUTTypeRTFD] if let uti = supportedAttributedTextUTIs.first(where: { itemProvider.hasItemConformingToTypeIdentifier($0 as String) }), itemProvider.canLoadObject(ofClass: NSAttributedString.self) { dispatchGroup.enter() itemProvider.loadDataRepresentation(forTypeIdentifier: uti as String) { (data, error) in context.perform { switch Result(data, error) { case .failure(let error): print(error) case .success(let data): let representation = PasteboardItemRepresentation(uti: uti as String, data: data, type: .attributedText, context: context) representations.append(representation) } dispatchGroup.leave() } } } let supportedImageUTIs = [kUTTypePNG, kUTTypeJPEG, kUTTypeImage] if let uti = supportedImageUTIs.first(where: { itemProvider.hasItemConformingToTypeIdentifier($0 as String) }), itemProvider.canLoadObject(ofClass: UIImage.self) { dispatchGroup.enter() itemProvider.loadDataRepresentation(forTypeIdentifier: uti as String) { (data, error) in context.perform { switch Result(data, error) { case .failure(let error): print(error) case .success(let data): guard data.count <= UserDefaults.shared.maximumClippingSize else { break } let representation = PasteboardItemRepresentation(uti: uti as String, data: data, type: .image, context: context) representations.append(representation) } dispatchGroup.leave() } } } let supportedURLUTIs = [kUTTypeFileURL, kUTTypeURL] if let uti = supportedURLUTIs.first(where: { itemProvider.hasItemConformingToTypeIdentifier($0 as String) }), itemProvider.canLoadObject(ofClass: NSURL.self) { dispatchGroup.enter() itemProvider.loadObject(ofClass: NSURL.self) { (url, error) in context.perform { switch Result(url, error) { case .failure(let error as NSError) where error.domain == NSItemProvider.errorDomain && error.code == NSItemProvider.ErrorCode.unavailableCoercionError.rawValue: // Ignore, corrupted data. break case .failure(let error): print("Failed to load URL.", error) case .success(let url): let representation = PasteboardItemRepresentation(uti: uti as String, url: url as! URL, context: context) representations.append(representation) } dispatchGroup.leave() } } } dispatchGroup.notify(queue: .global()) { context.perform { let sortedRepresentations = representations.sorted(by: { (a, b) -> Bool in guard let indexA = itemProvider.registeredTypeIdentifiers.firstIndex(of: a.uti) else { return false } guard let indexB = itemProvider.registeredTypeIdentifiers.firstIndex(of: b.uti) else { return false } return indexA < indexB }) completionHandler(sortedRepresentations) } } } } extension PasteboardItemRepresentation { @nonobjc class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "PasteboardItemRepresentation") } } public extension PasteboardItemRepresentation { var value: Any? { switch self.type { case .text: return self.stringValue case .attributedText: return self.attributedStringValue case .url: return self.urlValue case .image: return self.imageValue } } var pasteboardValue: Any? { switch self.type { case .text: return self.string case .attributedText: return self.data case .url: return self.url case .image: return self.data } } var stringValue: String? { switch self.type { case .text: return self.string case .attributedText: return self.attributedStringValue?.string case .url: return self.urlValue?.absoluteString case .image: return nil } } var imageValue: UIImage? { guard let data = self.data, let image = UIImage(data: data) else { return nil } return image } var urlValue: URL? { return self.url } var dataValue: Data? { return self.data } var attributedStringValue: NSAttributedString? { let type: NSAttributedString.DocumentType switch self.uti { case let uti as CFString where UTTypeConformsTo(uti, kUTTypeRTF): type = .rtf case let uti as CFString where UTTypeConformsTo(uti, kUTTypeHTML): type = .html case let uti as CFString where UTTypeConformsTo(uti, kUTTypeRTFD): type = .rtfd case let uti as CFString where UTTypeConformsTo(uti, kUTTypeFlatRTFD): type = .rtfd default: return nil } guard let data = self.data ?? self.string?.data(using: .utf8) else { return nil } let attributedString = try? NSAttributedString(data: data, options: [.documentType : type], documentAttributes: nil) return attributedString } } ================================================ FILE: ClipKit/Extensions/Bundle+AppGroups.swift ================================================ // // Bundle+AppGroups.swift // ClipKit // // Created by Riley Testut on 6/25/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation public extension Bundle { var appGroups: [String] { let appGroups = self.object(forInfoDictionaryKey: "ALTAppGroups") as? [String] return appGroups ?? [] } } ================================================ FILE: ClipKit/Extensions/CFNotification+PasteboardListener.swift ================================================ // // CFNotification+PasteboardListener.swift // ClipKit // // Created by Riley Testut on 6/13/19. // Copyright © 2019 Riley Testut. All rights reserved. // import CoreFoundation public extension CFNotificationName { static let didChangePasteboard: CFNotificationName = CFNotificationName("com.rileytestut.Clip.DidChangePasteboard" as CFString) static let ignoreNextPasteboardChange: CFNotificationName = CFNotificationName("com.rileytestut.Clip.IgnoreNextPasteboardChange" as CFString) } ================================================ FILE: ClipKit/Extensions/Int+Bytes.swift ================================================ // // Int+Bytes.swift // ClipKit // // Created by Riley Testut on 6/17/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation public extension Int { static var bytesPerMegabyte: Int { return 1024 * 1024 } } ================================================ FILE: ClipKit/Extensions/Result+Conveniences.swift ================================================ // // Result+Conveniences.swift // AltStore // // Created by Riley Testut on 5/22/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation public extension Result { var value: Success? { switch self { case .success(let value): return value case .failure: return nil } } var error: Failure? { switch self { case .success: return nil case .failure(let error): return error } } init(_ value: Success?, _ error: Failure?) { switch (value, error) { case (let value?, _): self = .success(value) case (_, let error?): self = .failure(error) case (nil, nil): preconditionFailure("Either value or error must be non-nil") } } } public extension Result where Success == Void { init(_ success: Bool, _ error: Failure?) { if success { self = .success(()) } else if let error = error { self = .failure(error) } else { preconditionFailure("Error must be non-nil if success is false") } } } public extension Result { init(_ values: (T?, U?), _ error: Failure?) where Success == (T, U) { if let value1 = values.0, let value2 = values.1 { self = .success((value1, value2)) } else if let error = error { self = .failure(error) } else { preconditionFailure("Error must be non-nil if either provided values are nil") } } } ================================================ FILE: ClipKit/Extensions/UIColor+Clip.swift ================================================ // // UIColor+Clip.swift // Clip // // Created by Riley Testut on 7/29/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit public extension UIColor { static let clipPink = UIColor(named: "Pink", in: Bundle(for: DatabaseManager.self), compatibleWith: nil)! static let clipLightPink = UIColor(named: "LightPink", in: Bundle(for: DatabaseManager.self), compatibleWith: nil)! } ================================================ FILE: ClipKit/Extensions/UIInputView+Click.swift ================================================ // // UIInputView+Click.swift // ClipKit // // Created by Riley Testut on 6/9/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit extension UIInputView: UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { return true } func playInputClick() { UIDevice.current.playInputClick() } } ================================================ FILE: ClipKit/Extensions/UIPasteboard+PasteboardItem.swift ================================================ // // UIPasteboard+PasteboardItem.swift // ClipKit // // Created by Riley Testut on 5/26/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit public extension UIPasteboard { func copy(_ pasteboardItem: PasteboardItem) { var representations = pasteboardItem.representations.reduce(into: [:]) { $0[$1.uti] = $1.pasteboardValue } representations[UTI.clipping] = [:] self.setItems([representations], options: [:]) } } ================================================ FILE: ClipKit/Extensions/UNNotification+Keys.swift ================================================ // // UNNotification+Keys.swift // ClipKit // // Created by Riley Testut on 3/20/24. // Copyright © 2024 Riley Testut. All rights reserved. // import UserNotifications public extension UNNotification { static let latitudeUserInfoKey: String = "CLPLatitude" static let longitudeUserInfoKey: String = "CLPLongitude" static let errorMessageUserInfoKey: String = "CLPErrorMessage" } public extension UNNotificationCategory { static let clipboardReaderIdentifier = "ClipboardReader" } ================================================ FILE: ClipKit/Extensions/UserDefaults+App.swift ================================================ // // UserDefaults+App.swift // Clip // // Created by Riley Testut on 6/14/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation import Roxas @objc public enum HistoryLimit: Int, CaseIterable { case _10 = 10 case _25 = 25 case _50 = 50 case _100 = 100 } public extension UserDefaults { static let shared: UserDefaults = { guard let appGroup = Bundle.main.appGroups.first else { return .standard } let sharedUserDefaults = UserDefaults(suiteName: appGroup)! return sharedUserDefaults }() @NSManaged var historyLimit: HistoryLimit @NSManaged var maximumClippingSize: Int @NSManaged var showLocationIcon: Bool } public extension UserDefaults { func registerAppDefaults() { self.register(defaults: [ #keyPath(UserDefaults.historyLimit): HistoryLimit._25.rawValue, #keyPath(UserDefaults.maximumClippingSize): 10 * .bytesPerMegabyte, #keyPath(UserDefaults.showLocationIcon): true ]) } } ================================================ FILE: ClipKit/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) ================================================ FILE: ClipKit/Resources/Colors.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ClipKit/Resources/Colors.xcassets/LightPink.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.404", "green" : "0.404", "red" : "0.988" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ClipKit/Resources/Colors.xcassets/Pink.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.549", "green" : "0.000", "red" : "0.925" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ClipKit/SwiftUI/Blur.swift ================================================ // // Blur.swift // ClipKit // // Created by Riley Testut on 5/25/20. // Copyright © 2020 Riley Testut. All rights reserved. // import SwiftUI import UIKit fileprivate struct BlurStyleKey: EnvironmentKey { static let defaultValue: UIBlurEffect.Style = .regular } public extension EnvironmentValues { var blurStyle: UIBlurEffect.Style { get { self[BlurStyleKey.self] } set { self[BlurStyleKey.self] = newValue } } } public extension View { func blurStyle(_ blurStyle: UIBlurEffect.Style) -> some View { environment(\.blurStyle, blurStyle) } } public struct Blur: View, UIViewRepresentable { @Environment(\.blurStyle) var blurStyle: UIBlurEffect.Style public func makeUIView(context: Context) -> UIVisualEffectView { let visualEffectView = UIVisualEffectView(effect: nil) updateUIView(visualEffectView, context: context) return visualEffectView } public func updateUIView(_ uiView: UIVisualEffectView, context: Context) { let blurEffect = UIBlurEffect(style: self.blurStyle) uiView.effect = blurEffect } } struct Blur_Previews: PreviewProvider { static var previews: some View { ZStack { Color.blue Blur() Text("Hello World!") } .colorScheme(.dark) .previewLayout(.fixed(width: 300, height: 300)) } } ================================================ FILE: ClipKit/SwiftUI/ClippingCell.swift ================================================ // // ClippingCell.swift // ClipKit // // Created by Riley Testut on 5/25/20. // Copyright © 2020 Riley Testut. All rights reserved. // import SwiftUI import CoreData import MobileCoreServices private extension Formatter { static let clipFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .abbreviated formatter.maximumUnitCount = 1 formatter.allowedUnits = [.second, .minute, .hour, .day] return formatter }() } public struct ClippingCell: View { @ObservedObject var pasteboardItem: PasteboardItem public init(pasteboardItem: PasteboardItem) { self.pasteboardItem = pasteboardItem } public var body: some View { let representation = self.pasteboardItem.preferredRepresentation let dateString = Formatter.clipFormatter.string(from: self.pasteboardItem.date, to: Date()) return Group { VStack(alignment: .leading, spacing: 8) { HStack { Text(representation?.type.localizedName ?? "Unknown") .font(.headline) Spacer() Text(dateString ?? "") .font(.caption) } .foregroundColor(Color(.clipPink)) if representation?.stringValue != nil { Text(representation!.stringValue!) .font(.subheadline) .lineLimit(6) } } .padding(.horizontal, nil) .padding(.vertical, 8) } .background(Color.white) .colorScheme(.light) .frame(minWidth: 0, maxWidth: .infinity) .cornerRadius(10) } } struct ClippingCell_Previews: PreviewProvider { static var previews: some View { Preview.prepare() let context = DatabaseManager.shared.persistentContainer.viewContext let date = Date().addingTimeInterval(-1 * 60 * 60) let item = PasteboardItem.make(item: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." as NSString, date: date, context: context) return ClippingCell(pasteboardItem: item) .background(Color(.clipPink)) .environment(\.managedObjectContext, context) .previewLayout(.sizeThatFits) } } ================================================ FILE: ClipKit/SwiftUI/Keyboard.swift ================================================ // // Keyboard.swift // ClipKit // // Created by Riley Testut on 5/25/20. // Copyright © 2020 Riley Testut. All rights reserved. // import SwiftUI import CoreData import UIKit import Roxas @objc private protocol RSTApplication: AnyObject { @objc(openURL:options:completionHandler:) func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any], completionHandler completion: (@MainActor @Sendable (Bool) -> Void)?) } public struct Keyboard: View { private let inputViewController: UIInputViewController? private let needsInputModeSwitchKey: Bool private let hasFullAccess: Bool @FetchRequest(fetchRequest: PasteboardItem.historyFetchRequest()) private var pasteboardItems: FetchedResults @Environment(\.horizontalSizeClass) private var horizontalSizeClass public init(inputViewController: UIInputViewController?, needsInputModeSwitchKey: Bool? = nil, hasFullAccess: Bool? = nil) { self.inputViewController = inputViewController self.needsInputModeSwitchKey = needsInputModeSwitchKey ?? inputViewController?.needsInputModeSwitchKey ?? false self.hasFullAccess = hasFullAccess ?? inputViewController?.hasFullAccess ?? true } public var body: some View { ZStack(alignment: .bottomLeading) { if !self.hasFullAccess { VStack(spacing: 32) { VStack(spacing: 16) { Text("Full Access Disabled") .font(.title) Text("Allow Full Access for this keyboard in Settings to access saved clippings.") .font(.body) } Button(action: self.openSettings) { Text("Open Settings") .font(Font(UIFont.preferredFont(forTextStyle: .title3))) .foregroundColor(Color(.clipPink)) } Button(action: self.pasteUUID) { Text("Paste Random UUID") .font(Font(UIFont.preferredFont(forTextStyle: .title3))) .foregroundColor(Color(.clipPink)) } } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .padding() } else if self.pasteboardItems.isEmpty { VStack(spacing: 16) { Text("No Clippings") .font(.title) Text("Items that you've copied to the clipboard will appear here.") .font(.body) } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .padding() } else { let list = List(self.pasteboardItems, id: \.objectID) { (pasteboardItem) in Button(action: { self.paste(pasteboardItem) }) { ClippingCell(pasteboardItem: pasteboardItem) } .buttonStyle(.plain) .foregroundStyle(.primary) .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(makeInsets()) } .padding(.top, 8) // iPadOS sometimes places List too close to toolbar, so add padding. if #available(iOS 16.4, *) { list .scrollContentBackground(.hidden) .scrollBounceBehavior(.always, axes: .vertical) } else { list } } if self.needsInputModeSwitchKey { let offset = (self.horizontalSizeClass == .regular) ? 16.0 : 8.0 SwitchKeyboardButton(inputViewController: self.inputViewController, tintColor: .clipPink, configuration: .init(textStyle: .title2)) .fixedSize() .padding(.horizontal, 4) .padding(.vertical, 8) .background(Blur()) .blurStyle(.extraLight) .clipShape(Circle()) .offset(x: offset, y: -offset) } } .edgesIgnoringSafeArea(.all) .onAppear { UITableView.appearance().backgroundColor = .clear UITableView.appearance().separatorStyle = .none UITableView.appearance().contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) UITableViewCell.appearance().backgroundColor = .clear } } private func makeInsets() -> EdgeInsets { var insets = EdgeInsets() insets.top = 8 insets.bottom = 8 return insets } } private extension Keyboard { func paste(_ pasteboardItem: PasteboardItem) { guard let text = pasteboardItem.preferredRepresentation?.stringValue else { return } let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterPostNotification(center, .ignoreNextPasteboardChange, nil, nil, true) UIPasteboard.general.copy(pasteboardItem) self.paste(text) } func pasteUUID() { let uuid = UUID().uuidString self.paste(uuid) } func paste(_ text: String) { self.inputViewController?.textDocumentProxy.insertText(text) self.inputViewController?.inputView?.playInputClick() DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { self.inputViewController?.advanceToNextInputMode() } } func openSettings() { // NSExtensionContext.openURL() can only be called from Today extensions. // As a workaround, we can just call UIApplication.openURL(), // but we can't call it directly because it's marked as unavailable for extensions. guard let application = (UIApplication.self as AnyObject).value(forKey: "sharedApplication") as? UIApplication else { return } // UIApplication.openSettingsURLString doesn't work from keyboard extension, // so instead we open Clip which will then open Settings. let openURL = URL(string: "clip://settings")! let tempApp = unsafeBitCast(application, to: RSTApplication.self) tempApp.open(openURL, options: [:], completionHandler: nil) } } struct Keyboard_Previews: PreviewProvider { static var previews: some View { Preview.prepare() let context = DatabaseManager.shared.persistentContainer.viewContext let date = Date().addingTimeInterval(-1 * 60 * 60) _ = PasteboardItem.make(item: "Hello SwiftUI!" as NSString, date: date, context: context) _ = PasteboardItem.make(item: NSURL(string: "https://rileytestut.com")!, date: date, context: context) _ = PasteboardItem.make(item: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." as NSString, date: date, context: context) return Group { Keyboard(inputViewController: nil, needsInputModeSwitchKey: true) Keyboard(inputViewController: nil, needsInputModeSwitchKey: true, hasFullAccess: false) } .background(Color(.lightGray)) .environment(\.managedObjectContext, context) .previewLayout(.fixed(width: 375, height: 500)) } } ================================================ FILE: ClipKit/SwiftUI/Preview.swift ================================================ // // Preview.swift // ClipKit // // Created by Riley Testut on 5/26/20. // Copyright © 2020 Riley Testut. All rights reserved. // import Foundation import CoreData struct Preview { static func prepare() { if DatabaseManager.shared.persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty { let inMemoryStoreDescription = NSPersistentStoreDescription() inMemoryStoreDescription.type = NSInMemoryStoreType DatabaseManager.shared.persistentContainer.persistentStoreDescriptions = [inMemoryStoreDescription] DatabaseManager.shared.persistentContainer.shouldAddStoresAsynchronously = false DatabaseManager.shared.prepare() { (result) in print("Database Result:", result) } } // Manually call initialize() since it isn't normally called when previewing 🤷‍♂️ UserDefaults.initialize() } } ================================================ FILE: ClipKit/SwiftUI/SwitchKeyboardButton.swift ================================================ // // SwitchKeyboardButton.swift // ClipKit // // Created by Riley Testut on 6/9/20. // Copyright © 2020 Riley Testut. All rights reserved. // import SwiftUI struct SwitchKeyboardButton: UIViewRepresentable { var inputViewController: UIInputViewController? var tintColor: UIColor? = nil var configuration: UIImage.SymbolConfiguration? = nil func makeUIView(context: Context) -> UIButton { let button = UIButton(type: .system) button.tintColor = self.tintColor button.addTarget(self.inputViewController, action: #selector(UIInputViewController.handleInputModeList(from:with:)), for: .allTouchEvents) button.setImage(UIImage(systemName: "globe", withConfiguration: self.configuration), for: .normal) button.sizeToFit() return button } func updateUIView(_ button: UIButton, context: Context) { } } struct SwitchKeyboardButton_Previews: PreviewProvider { static var previews: some View { Group { SwitchKeyboardButton(inputViewController: nil) .fixedSize() SwitchKeyboardButton(inputViewController: nil, tintColor: .clipPink, configuration: .init(textStyle: .title1)) .fixedSize() } .previewLayout(.sizeThatFits) } } ================================================ FILE: ClipKit/UTI.swift ================================================ // // UTI.swift // ClipKit // // Created by Riley Testut on 6/14/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation public struct UTI { public static let clipping = "com.rileytestut.Clip.Clipping" } ================================================ FILE: ClipboardReader/Base.lproj/MainInterface.storyboard ================================================ ================================================ FILE: ClipboardReader/ClipboardReader.entitlements ================================================ com.apple.security.application-groups group.com.rileytestut.Clip ================================================ FILE: ClipboardReader/Info.plist ================================================ ALTAppGroups group.com.rileytestut.Clip CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName ClipboardReader CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion 1 NSExtension NSExtensionAttributes UNNotificationExtensionCategory ClipboardReader UNNotificationExtensionDefaultContentHidden UNNotificationExtensionInitialContentSizeRatio 0.14999999999999999 NSExtensionMainStoryboard MainInterface NSExtensionPointIdentifier com.apple.usernotifications.content-extension ================================================ FILE: ClipboardReader/NotificationViewController.swift ================================================ // // NotificationViewController.swift // NotificationClipboard // // Created by Riley Testut on 12/2/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit import UserNotifications import UserNotificationsUI import CoreLocation import ClipKit import Roxas class NotificationViewController: UIViewController, UNNotificationContentExtension { @IBOutlet private var activityIndicatorView: UIActivityIndicatorView! private let preparationDispatchGroup = DispatchGroup() private var databaseError: Swift.Error? required init?(coder: NSCoder) { super.init(coder: coder) UserDefaults.shared.registerAppDefaults() self.preparationDispatchGroup.enter() DatabaseManager.shared.persistentContainer.shouldAddStoresAsynchronously = true DatabaseManager.shared.prepare { (result) in switch result { case .failure(let error): self.databaseError = error case .success: break } self.preparationDispatchGroup.leave() } } func didReceive(_ notification: UNNotification) { guard notification.request.content.userInfo[UNNotification.errorMessageUserInfoKey] == nil else { // This is an error notification, so just dismiss it if user interacts. self.extensionContext?.dismissNotificationContentExtension() return } if let error = self.databaseError { self.finish(.failure(error)) } else { let location: CLLocation? if let latitude = notification.request.content.userInfo[UNNotification.latitudeUserInfoKey] as? Double, let longitude = notification.request.content.userInfo[UNNotification.longitudeUserInfoKey] as? Double { location = CLLocation(latitude: latitude, longitude: longitude) } else { location = nil } self.preparationDispatchGroup.notify(queue: .main) { DatabaseManager.shared.savePasteboard(location: location) { (result) in self.finish(result) } } } } } private extension NotificationViewController { func finish(_ result: Result) { // Can't dismiss extension too early or else we can't read clipboard. self.extensionContext?.dismissNotificationContentExtension() switch result { case .success: break case .failure(PasteboardError.duplicateItem): break case .failure(let error): let content = UNMutableNotificationContent() content.title = NSLocalizedString("Failed to Save Clipboard", comment: "") content.body = error.localizedDescription content.categoryIdentifier = UNNotificationCategory.clipboardReaderIdentifier content.userInfo[UNNotification.errorMessageUserInfoKey] = error.localizedDescription let request = UNNotificationRequest(identifier: "SaveError", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { (error) in if let error = error { print(error) } } } } } ================================================ FILE: README.md ================================================ # Clip > Clip is a clipboard manager for iOS that can monitor your clipboard indefinitely in the background — no jailbreak required. [![Swift Version](https://img.shields.io/badge/swift-5.0-orange.svg)](https://swift.org/) [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) Clip is a simple clipboard manager for iOS. Unlike other clipboard managers available in the App Store, Clip is able to monitor your clipboard indefinitely in the background. This is accomplished through a combination of hacks and workarounds, none of which would pass App Store review. For that reason, Clip is only available to download through [AltStore](https://github.com/rileytestut/AltStore) — my alternative app store for non-jailbroken devices — or by compiling the source code yourself.

## Features - Runs silently in the background, monitoring your clipboard the whole time. - Save text, URLs, and images copied to the clipboard. - Copy, delete, and share clippings. - Customizable history limit. ## Requirements - Xcode 11 - iOS 13 - Swift 5+ ## Project Overview All things considered, Clip is a very simple app. The core app target can be mentally divided up into UI and logic, while each additional target serves a specific role. ### App UI The entire UI is implemented with just two view controllers: **HistoryViewController** The main screen of Clip. A relatively straightforward `UITableViewController` subclass that fetches recent clippings from Clip’s persistent store and displays them in a table view. **SettingsViewController** The settings screen for Clip. Another `UITableViewController` subclass that displays all Clip settings in a list, but is presented as a popover due to limited number of settings. ### App Logic The app logic for Clip is relatively straightforward. Most is self-explanatory, but there are two classes that serve particularly important roles: **PasteboardMonitor** As you might have guessed from the name, this class is in charge of listening for changes to the clipboard. Since `UIPasteboardChangedNotification` is only received when the app is in the foreground, this class uses the private `Pasteboard.framework` to start sending system-wide Darwin notifications whenever the clipboard’s contents change. Once a change is detected, PasteboardMonitor presents a local notification that can be expanded by the user to save their clipboard to Clip. **ApplicationMonitor** This class manages the lifecycle of Clip. Specifically, it is in charge of playing a silent audio clip on loop so Clip can run indefinitely in the background, as well as presenting a local notification whenever Clip stops running (for whatever reason). ### ClipKit ClipKit is a shared framework that includes common code between Clip, ClipboardReader, and ClipBoard. Notably, it contains all model + Core Data logic, so that Clip and each app extension can access the same persistent store with all clippings. ### ClipboardReader ClipboardReader is a Notification Content app extension used to read the clipboard while Clip is running in the background. When Clip detects a change to the clipboard, it will present a local notification. If this notification is expanded, ClipboardReader will be launched and save the contents of the clipboard to disk before dismissing the now-expanded notification.


ClipboardReader in action.

### ClipBoard ClipBoard is a Custom Keyboard app extension that provides quick access to your recent clippings when editing text. This feature is still being worked on, so it is only available in beta versions of Clip for now. ### Roxas Roxas is my internal framework used across all my iOS projects, developed to simplify a variety of common tasks used in iOS development. For more info, check the [Roxas repo](https://github.com/rileytestut/roxas). ## Compilation Instructions Clip is very straightforward to compile and run if you're already an iOS developer. To compile Clip: 1. Clone the repository ``` https://github.com/rileytestut/Clip.git ``` 2. Update submodules: ``` cd Clip git submodule update --init --recursive ``` 3. Open `Clip.xcworkspace` and select the Clip project in the project navigator. On the `Signing & Capabilities` tab, change the team from `Yvette Testut` to your own account. 4. Build + run app! 🎉 ## Licensing Unlike my other projects, Clip uses no 3rd party dependencies. This gives me complete freedom to choose the license I want, so I’m choosing to **release the complete Clip source code into the public domain**. You can view the complete “unlicense” [here](https://github.com/rileytestut/Clip/blob/master/UNLICENSE), but the gist is: > Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. ## Contact Me * Email: riley@rileytestut.com * Twitter: [@rileytestut](https://twitter.com/rileytestut) ================================================ FILE: UNLICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to