├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Screenshots ├── screenshot-1.png └── screenshot-2.png ├── Sources └── SwiftySandboxFileAccess │ ├── AccessInfo.swift │ ├── Permissions.swift │ ├── PowerboxAccess.swift │ ├── SandboxFileAccess.swift │ ├── SandboxFileAccessOpenSavePanelDelegate.swift │ └── SandboxFileAccessPersist.swift ├── SwiftySandboxFileAccess.podspec └── SwiftySandboxFileAccessDemo ├── SwiftySandboxDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── SwiftySandboxDemo ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── Main.storyboard ├── Info.plist ├── Manager.swift ├── SwiftySandboxDemo.entitlements └── ViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /.swiftpm 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original work by Leigh MucCulloch as below 2 | Additional work by Rob Jonson - also under the same licence 3 | 4 | Copyright (c) 2013, Leigh McCulloch All rights reserved. 5 | 6 | BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause 7 | 8 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 9 | 10 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftySandboxFileAccess", 8 | products: [ 9 | .library( 10 | name: "SwiftySandboxFileAccess", 11 | targets: ["SwiftySandboxFileAccess"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "SwiftySandboxFileAccess", 16 | dependencies: []) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwiftySandboxFileAccess 2 | ==================== 3 | 4 | This is a swift version of the original [AppSandboxFileAccess](https://github.com/leighmcculloch/AppSandboxFileAccess) with a simpler more swifty API and a handful of additional features. 5 | 6 | Version 3.0 Released 7 | ==================== 8 | 9 | Version 3.0 is available with SPM. 10 | It has a new cleaner API, and integrates with the powerbox. 11 | This allows it to work with 'Do I have access to this file' rather than 'Do I have a bookmark which gives me access to this file'? 12 | 13 | Details 14 | ==================== 15 | 16 | A simple class that wraps up writing and accessing files outside a Mac apps App Sandbox files. The class will request permission from the user with a simple to understand dialog consistent with Apple's documentation and persist permissions across application runs using security bookmarks. 17 | 18 | This is specifically useful for when you need to write files, or gain access to directories that are not already accessible to your application. 19 | 20 | When using this class, if the user needs to give permission to access the folder, the NSOpenPanel is used to request permission. Only the path or file requiring permission, or parent paths are selectable in the NSOpenPanel. The panel text, title and button are customisable. 21 | ![](Screenshots/screenshot-1.png) 22 | 23 | 24 | 25 | How to Use 26 | ==================== 27 | 28 | ### SwiftPackageManager 29 | 30 | Standard drill! 31 | 32 | ### CocoaPods (deprecated - stuck on v 2!) 33 | 34 | ### Entitlements: 35 | 36 | In Xcode click on your project file, then the Capabilities tab. Turn on App Sandbox and change 'User Selected File' to 'Read/Write' or 'Read Only', whichever you need. In your project Xcode will have created a .entitlements file. Open this and you should see the below. If you plan on persisting permissions you'll need to add the third entitlement. 37 | 38 | ![](Screenshots/screenshot-2.png) 39 | 40 | 41 | Main Function Groups 42 | ==================== 43 | 44 | Version 3.0 dramatically simplifies the API 45 | 46 | 47 | use `SandboxFileAccess().someFunction` 48 | 49 | 50 | ### Save permission 51 | 52 | `persistPermission(url:) -> Data?` 53 | 54 | Saves a permission which the app has recieved in some other way (dropped on dock, file open, etc) 55 | 56 | 57 | ### Access a file 58 | 59 | ``` 60 | access(fileURL: URL, 61 | acceptablePermission:Permissions = .bookmark, 62 | askIfNecessary:Bool, 63 | fromWindow:NSWindow? = nil, 64 | persistPermission persist: Bool = true, 65 | with block: @escaping SandboxFileAccessBlock) 66 | ``` 67 | 68 | Use this block to asynchronously access your file. 69 | 70 | If any of the acceptable permissions are met, then the block is called with a `.success` result 71 | 72 | acceptablePermission is .bookmark by default which means you'll only get `.success` if there is a stored bookmark -even if powerbox already grants access to the file 73 | 74 | If you only care about access now, then you can use `.anyReadOnly` or `.anyReadWrite` 75 | 76 | NB: The access info in the block shows the url of the bookmark _actually used_ to get access. This may be a parent of the url you need to use. 77 | 78 | 79 | ### Check whether you can access a file 80 | 81 | `canAccess(fileURL:URL, acceptablePermission:Permissions = .anyReadWrite) -> Bool` 82 | 83 | Returns whether we can currently access the fileURL with the required permissions 84 | 85 | ### Check what access you have to a file 86 | 87 | `accessInfo(forFileURL fileURL:URL) -> AccessInfo` 88 | 89 | ### Synchronously Access a file if permission is already available (or stored) 90 | 91 | ``` 92 | synchronouslyAccess(fileURL: URL, 93 | acceptablePermission:Permissions = .bookmark, 94 | with block: SandboxFileAccessBlock) -> SandboxResult 95 | ``` 96 | 97 | License 98 | ==================== 99 | 100 | Copyright (c) 2013, Leigh McCulloch 101 | and Rob Jonson 102 | All rights reserved. 103 | 104 | See included Licence file 105 | -------------------------------------------------------------------------------- /Screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConfusedVorlon/SwiftySandboxFileAccess/7d4e9549cf3e1c331742b86a1dbcacccf52b7274/Screenshots/screenshot-1.png -------------------------------------------------------------------------------- /Screenshots/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConfusedVorlon/SwiftySandboxFileAccess/7d4e9549cf3e1c331742b86a1dbcacccf52b7274/Screenshots/screenshot-2.png -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/AccessInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Rob Jonson on 21/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Info on what access was available to access the required file 11 | public struct AccessInfo { 12 | //Note -this is the url of the bookmark actually used to access your file. It may be a parent of the file you want. 13 | public let securityScopedURL:URL? 14 | public let bookmarkData:Data? 15 | public let permissions:Permissions 16 | 17 | public static let empty = AccessInfo(securityScopedURL: nil,bookmarkData: nil,permissions: .none) 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/Permissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Rob Jonson on 21/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Option set describing list of permissions required or available 11 | public struct Permissions: OptionSet { 12 | public let rawValue: Int 13 | 14 | public static let bookmark = Permissions(rawValue: 1 << 0) 15 | public static let powerboxReadOnly = Permissions(rawValue: 1 << 1) 16 | public static let powerboxReadWrite = Permissions(rawValue: 1 << 2) 17 | 18 | public static let none: Permissions = [] 19 | public static let anyReadOnly: Permissions = [.bookmark, .powerboxReadOnly] 20 | public static let anyReadWrite: Permissions = [.bookmark, .powerboxReadWrite] 21 | 22 | public var canRead:Bool { 23 | return self.contains(.bookmark) || self.contains(.powerboxReadOnly) 24 | } 25 | 26 | public var canWrite:Bool { 27 | return self.contains(.bookmark) || self.contains(.powerboxReadWrite) 28 | } 29 | 30 | public init(rawValue newRawValue: Int){ 31 | rawValue = newRawValue 32 | } 33 | 34 | public func meets(required:Permissions) -> Bool { 35 | if required == .none { 36 | return true 37 | } 38 | 39 | return !self.intersection(required).isEmpty 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/PowerboxAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Rob Jonson on 16/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum PowerboxAccessMode { 11 | case readOnly 12 | case readWrite 13 | 14 | var permission:Int32 { 15 | switch self { 16 | case .readOnly: 17 | return R_OK 18 | case .readWrite: 19 | return (R_OK | W_OK) 20 | } 21 | } 22 | } 23 | 24 | /// Check whether powerbox allows access to the file without bookmarks 25 | public class Powerbox { 26 | static func allowsAccess(forFileURL fileURL:URL,mode:PowerboxAccessMode) -> Bool { 27 | let path = fileURL.path as NSString 28 | return Darwin.access(path.fileSystemRepresentation, mode.permission) == 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/SandboxFileAccess.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | 4 | 5 | public typealias SandboxResult = Result 6 | public typealias SandboxFileAccessBlock = (SandboxResult) -> Void 7 | 8 | 9 | public protocol SandboxFileAccessProtocol: AnyObject { 10 | func bookmarkData(for url: URL) -> Data? 11 | func setBookmark(data: Data?, for url: URL) 12 | func clearBookmarkData(for url: URL) 13 | } 14 | 15 | 16 | extension Bundle { 17 | enum Key: String { 18 | case name = "CFBundleName", 19 | displayName = "CFBundleDisplayName" 20 | } 21 | 22 | subscript(index: Bundle.Key) -> Any? { 23 | get { 24 | return self.object(forInfoDictionaryKey: index.rawValue) 25 | } 26 | 27 | } 28 | } 29 | 30 | 31 | open class SandboxFileAccess { 32 | 33 | // MARK: UI Customisation for file panel 34 | 35 | /// The title of the NSOpenPanel displayed when asking permission to access a file. Default: "Allow Access" 36 | open var title:String = {NSLocalizedString("Allow Access", comment: "Sandbox Access panel title.")}() 37 | 38 | /// The message contained on the the NSOpenPanel displayed when asking permission to access a file. 39 | /// If this is null, then a default message is constructed 40 | open var message:String? 41 | 42 | /// The prompt button on the the NSOpenPanel displayed when asking permission to access a file. 43 | open var prompt:String = {NSLocalizedString("Allow", comment: "Sandbox Access panel prompt.")}() 44 | 45 | // MARK: Bookmark management 46 | 47 | /// This is an optional delegate object that can be provided to customize the persistance of bookmark data (e.g. in a Core Data database). 48 | public weak var bookmarkPersistanceDelegate: SandboxFileAccessProtocol? 49 | 50 | private var defaultDelegate: SandboxFileAccessProtocol = SandboxFileAccessPersist() 51 | private var bookmarkPersistanceDelegateOrDefault:SandboxFileAccessProtocol { 52 | return bookmarkPersistanceDelegate ?? defaultDelegate 53 | } 54 | 55 | // MARK: Failure codes 56 | 57 | public enum Fail:Error { 58 | case needToAskPermission(AccessInfo) 59 | case noWindowProvided(AccessInfo) 60 | case userDidntGrantPermission(AccessInfo) 61 | case unexpectedlyUnableToAccessBookmark(AccessInfo) 62 | } 63 | 64 | // MARK: Implementation 65 | 66 | public init() { 67 | 68 | } 69 | 70 | /// Check whether we have access to a given file synchronously. 71 | /// - Parameter fileURL: A file URL, either a file or folder, that the caller needs access to. 72 | /// - acceptablePermission: an optionlist of acceptable permissions. If _ANY_ of the acceptablePermission are met, then the function returns true 73 | /// - Returns: true if we have permission to access the given file 74 | public func canAccess(fileURL:URL, acceptablePermission:Permissions = .anyReadWrite) -> Bool { 75 | let info = accessInfo(forFileURL: fileURL) 76 | return info.permissions.meets(required: acceptablePermission) 77 | } 78 | 79 | public func accessInfo(forFileURL fileURL:URL) -> AccessInfo { 80 | // standardize the file url and remove any symlinks so that the url we lookup in bookmark data would match a url given by the askPermissionForURL method 81 | let standardisedFileURL = fileURL.standardizedFileURL.resolvingSymlinksInPath() 82 | 83 | var permissions = Permissions.none 84 | 85 | let (allowedURL,bookmarkData) = allowedURLAndBookmarkData(forFileURL:standardisedFileURL) 86 | // if url is stored, then we'll get a url and bookmark data. 87 | if allowedURL != nil { 88 | permissions.insert(.bookmark) 89 | } 90 | 91 | if Powerbox.allowsAccess(forFileURL: fileURL, mode: .readOnly) { 92 | permissions.insert(.powerboxReadOnly) 93 | } 94 | 95 | if Powerbox.allowsAccess(forFileURL: fileURL, mode: .readWrite) { 96 | permissions.insert(.powerboxReadWrite) 97 | } 98 | 99 | return AccessInfo(securityScopedURL: allowedURL, 100 | bookmarkData: bookmarkData, 101 | permissions: permissions) 102 | } 103 | 104 | 105 | /// Access a file, requesting permission if needed 106 | /// If the file is accessible through a stored bookmark, then start/stop AccessingSecurityScopedResource is called around the block 107 | /// 108 | /// - Parameters: 109 | /// - fileURL: required URL 110 | /// - acceptablePermission: an optionlist of acceptable permissions. If _ANY_ of the acceptablePermission are met, then the access procedes 111 | /// - askIfNecessary: whether to ask the user for permission (requires window to be provided) 112 | /// - fromWindow: window to present sheet on 113 | /// - persist: whether to persist the permission 114 | /// - block: block called with access info. 115 | /// Note that the returned url in accessInfo may be a parent of the url you requested 116 | public func access(fileURL: URL, 117 | acceptablePermission:Permissions = .bookmark, 118 | askIfNecessary:Bool, 119 | fromWindow:NSWindow? = nil, 120 | persistPermission persist: Bool = true, 121 | with block: @escaping SandboxFileAccessBlock) { 122 | 123 | 124 | let accessInfo = accessInfo(forFileURL: fileURL) 125 | 126 | if accessInfo.permissions.meets(required: acceptablePermission){ 127 | _ = secureAccess(accessInfo: accessInfo, block: block) 128 | return 129 | } 130 | 131 | if !askIfNecessary { 132 | block(.failure(Fail.needToAskPermission(accessInfo))) 133 | return 134 | } 135 | 136 | //continuing means we don't have what we want and we're willing to ask 137 | 138 | guard let fromWindow = fromWindow else { 139 | print("ERROR: Unable to ask permission in swiftySandboxFileAccess as no window has been given") 140 | block(.failure(Fail.noWindowProvided(accessInfo))) 141 | return 142 | } 143 | 144 | let standardisedFileURL = fileURL.standardizedFileURL.resolvingSymlinksInPath() 145 | askPermissionWithSheet(for: standardisedFileURL, fromWindow: fromWindow) { (url) in 146 | guard let url = url else { 147 | block(.failure(Fail.userDidntGrantPermission(accessInfo))) 148 | return 149 | } 150 | 151 | if persist == true { 152 | _ = self.persistPermission(url: url) 153 | } 154 | 155 | //update info with (potentially) new data 156 | let accessInfo = self.accessInfo(forFileURL: fileURL) 157 | _ = self.secureAccess(accessInfo: accessInfo, block: block) 158 | } 159 | 160 | } 161 | 162 | 163 | /// Provides synchronous access if the required permissions are available without asking 164 | /// - Parameters: 165 | /// - fileURL: required URL 166 | /// - acceptablePermission: an optionlist of acceptable permissions. 167 | /// If _ANY_ of the acceptablePermission are met, then the access procedes 168 | /// - block: block called with access info. 169 | /// Note that the returned url in accessInfo may be a parent of the url you requested 170 | /// - Returns: the access result 171 | public func synchronouslyAccess(fileURL: URL, 172 | acceptablePermission:Permissions = .bookmark, 173 | with block: SandboxFileAccessBlock) -> SandboxResult { 174 | 175 | 176 | let accessInfo = accessInfo(forFileURL: fileURL) 177 | 178 | if accessInfo.permissions.meets(required: acceptablePermission){ 179 | return secureAccess(accessInfo: accessInfo, block: block) 180 | } 181 | else { 182 | let result:SandboxResult = .failure(Fail.needToAskPermission(accessInfo)) 183 | block(result) 184 | return result 185 | } 186 | } 187 | 188 | /// If we have a bookmark, then do the security access dance around the block 189 | /// This is possibly excessive if the user only asked for powerbox access - but shouldn't do any harm 190 | /// - Parameters: 191 | /// - accessInfo: info 192 | /// - block: block to run 193 | private func secureAccess(accessInfo:AccessInfo, block: SandboxFileAccessBlock) -> SandboxResult { 194 | 195 | guard let securityScopedFileURL = accessInfo.securityScopedURL else { 196 | //we don't have bookmark info - but we do meet the requirements, so just return with success 197 | let result:SandboxResult = .success(accessInfo) 198 | block(result) 199 | return result 200 | } 201 | 202 | if (securityScopedFileURL.startAccessingSecurityScopedResource() == true) { 203 | let result:SandboxResult = .success(accessInfo) 204 | block(result) 205 | securityScopedFileURL.stopAccessingSecurityScopedResource() 206 | return result 207 | } 208 | else { 209 | //not sure when/why this path happens. 210 | print("Unexpectedly failed to startAccessingSecurityScopedResource") 211 | let result:SandboxResult = .failure(Fail.unexpectedlyUnableToAccessBookmark(accessInfo)) 212 | block(result) 213 | return result 214 | } 215 | 216 | } 217 | 218 | 219 | /// Persist a security bookmark for the given URL. The calling application must already have permission. 220 | 221 | /// discussion Use this function to persist permission of a URL that has already been granted when a user introduced 222 | /// a file to the calling application. E.g. by dropping the file onto the application window, or dock icon, or when using an NSOpenPanel. 223 | 224 | /// Note: If the calling application does not have access to this file, this call will do nothing. 225 | /// 226 | /// - Parameter url: The URL with permission that will be persisted. 227 | /// - Returns: Bookmark data if permission was granted or already available, nil otherwise. 228 | public func persistPermission(url: URL) -> Data? { 229 | 230 | // store the sandbox permissions 231 | var bookmarkData: Data? = nil 232 | do { 233 | bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) 234 | } catch { 235 | } 236 | if bookmarkData != nil { 237 | bookmarkPersistanceDelegateOrDefault.setBookmark(data: bookmarkData, for: url) 238 | } 239 | return bookmarkData 240 | } 241 | 242 | /// Quickly persist multiple URLs. Useful with openPanel responses 243 | /// - Parameter urls: urls to persist 244 | public func persistPermissions(urls:[URL]) { 245 | for url in urls { 246 | _ = persistPermission(url: url) 247 | } 248 | } 249 | 250 | //MARK: Utility methods 251 | 252 | 253 | 254 | private func defaultOpenPanelMessage(forFileURL fileURL:URL) -> String { 255 | let applicationName = (Bundle.main[.displayName] as? String) 256 | ?? (Bundle.main[.name] as? String) 257 | ?? "This App" 258 | 259 | return "\(applicationName) needs to access '\(fileURL.lastPathComponent)' to continue. Click Allow to continue." 260 | } 261 | 262 | private func allowedURLAndBookmarkData(forFileURL fileURL:URL) -> (URL?,Data?) { 263 | 264 | var allowedURL: URL? = nil 265 | 266 | // standardize the file url and remove any symlinks so that the url we lookup in bookmark data would match a url given by the askPermissionForURL method 267 | let standardisedFileURL = fileURL.standardizedFileURL.resolvingSymlinksInPath() 268 | 269 | // lookup bookmark data for this url, this will automatically load bookmark data for a parent path if we have it 270 | var bookmarkData:Data? = bookmarkPersistanceDelegateOrDefault.bookmarkData(for: standardisedFileURL) 271 | if let concreteData = bookmarkData { 272 | // resolve the bookmark data into an NSURL object that will allow us to use the file 273 | var bookmarkDataIsStale: Bool = false 274 | do { 275 | allowedURL = try URL.init(resolvingBookmarkData:concreteData, options: [.withSecurityScope, .withoutUI], relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale) 276 | } catch { 277 | } 278 | // if the bookmark data is stale we'll attempt to recreate it with the existing url object if possible (not guaranteed) 279 | if bookmarkDataIsStale { 280 | bookmarkData = nil 281 | bookmarkPersistanceDelegateOrDefault.clearBookmarkData(for: standardisedFileURL) 282 | if allowedURL != nil { 283 | bookmarkData = persistPermission(url: allowedURL!) 284 | if bookmarkData == nil { 285 | allowedURL = nil 286 | } 287 | } 288 | } 289 | } 290 | 291 | return (allowedURL,bookmarkData) 292 | } 293 | 294 | private func existingUrlOrParent(for url:URL) -> URL { 295 | let fileManager = FileManager.default 296 | var path = url.path 297 | while (path.count) > 1 { 298 | // give up when only '/' is left in the path or if we get to a path that exists 299 | if fileManager.fileExists(atPath: path){ 300 | break 301 | } 302 | path = URL(fileURLWithPath: path).deletingLastPathComponent().path 303 | } 304 | let existingURL = URL(fileURLWithPath: path) 305 | return existingURL 306 | } 307 | 308 | private func openPanel(for url:URL) -> (NSOpenPanel,SandboxFileAccessOpenSavePanelDelegate) { 309 | // create delegate that will limit which files in the open panel can be selected, to ensure only a folder 310 | // or file giving permission to the file requested can be selected 311 | let openPanelDelegate = SandboxFileAccessOpenSavePanelDelegate(fileURL: url) 312 | 313 | let existingURL = existingUrlOrParent(for: url) 314 | var isDirectory:ObjCBool = false 315 | FileManager.default.fileExists(atPath: existingURL.path, isDirectory: &isDirectory) 316 | 317 | let openPanel = NSOpenPanel() 318 | openPanel.message = self.message ?? defaultOpenPanelMessage(forFileURL: url) 319 | openPanel.canCreateDirectories = false 320 | openPanel.canChooseFiles = !isDirectory.boolValue 321 | openPanel.canChooseDirectories = true 322 | openPanel.allowsMultipleSelection = false 323 | openPanel.prompt = self.prompt 324 | openPanel.title = self.title 325 | openPanel.showsHiddenFiles = false 326 | openPanel.isExtensionHidden = false 327 | openPanel.directoryURL = existingURL 328 | openPanel.delegate = openPanelDelegate 329 | 330 | return (openPanel,openPanelDelegate) 331 | } 332 | 333 | 334 | 335 | private func askPermissionWithSheet(for url: URL, fromWindow:NSWindow, with block: @escaping (URL?)->Void) { 336 | let requestedURL = url 337 | 338 | // display the open panel 339 | let displayOpenPanelBlock = { 340 | let (openPanel,openPanelDelegate) = self.openPanel(for: requestedURL) 341 | 342 | NSApplication.shared.activate(ignoringOtherApps: true) 343 | 344 | openPanel.beginSheetModal(for:fromWindow) { (result) in 345 | if result == NSApplication.ModalResponse.OK { 346 | block(openPanel.url) 347 | } 348 | else { 349 | block(nil) 350 | } 351 | 352 | //use anonymous assignment to ensure that openPanelDelegate is retained 353 | _ = openPanelDelegate 354 | } 355 | 356 | } 357 | if Thread.isMainThread { 358 | displayOpenPanelBlock() 359 | } else { 360 | DispatchQueue.main.async(execute: displayOpenPanelBlock) 361 | } 362 | 363 | } 364 | } 365 | 366 | 367 | -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/SandboxFileAccessOpenSavePanelDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | // Created by Leigh McCulloch on 23/11/2013. 3 | // 4 | // Copyright (c) 2013, Leigh McCulloch 5 | // All rights reserved. 6 | // 7 | // BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause 8 | // 9 | // Redistribution and use in source and binary forms, with or without 10 | // modification, are permitted provided that the following conditions are 11 | // met: 12 | // 13 | // 1. Redistributions of source code must retain the above copyright 14 | // notice, this list of conditions and the following disclaimer. 15 | // 16 | // 2. Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 21 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 23 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 26 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | // 32 | 33 | import AppKit 34 | 35 | enum SandboxDelegateError: Error { 36 | case triedToChooseWrongFile 37 | } 38 | 39 | extension SandboxDelegateError: LocalizedError { 40 | public var errorDescription: String? { 41 | switch self { 42 | case .triedToChooseWrongFile: 43 | return NSLocalizedString("Please allow the originally requested location", comment: "Wrong File") 44 | } 45 | } 46 | } 47 | 48 | 49 | class SandboxFileAccessOpenSavePanelDelegate: NSObject, NSOpenSavePanelDelegate { 50 | private var pathComponents: [Any] = [] 51 | 52 | 53 | 54 | init(fileURL: URL) { 55 | super.init() 56 | 57 | pathComponents = fileURL.pathComponents 58 | } 59 | 60 | 61 | 62 | // MARK: -- NSOpenSavePanelDelegate 63 | func panel(_ sender: Any, shouldEnable url: URL) -> Bool { 64 | 65 | let pathComponents = self.pathComponents 66 | let otherPathComponents = url.pathComponents 67 | 68 | // if the url passed in has more components, it could not be a parent path or a exact same path 69 | if (otherPathComponents.count) > pathComponents.count { 70 | return false 71 | } 72 | 73 | // check that each path component in url, is the same as each corresponding component in self.url 74 | for i in 0..<(otherPathComponents.count) { 75 | let comp1 = otherPathComponents[i] 76 | let comp2 = pathComponents[i] as? String 77 | // not the same, therefore url is not a parent or exact match to self.url 78 | if !(comp1 == comp2) { 79 | return false 80 | } 81 | } 82 | 83 | // there were no mismatches (or no components meaning url is root) 84 | return true 85 | } 86 | 87 | func panel(_ sender: Any, validate url: URL) throws { 88 | let allowed = self.panel(sender, shouldEnable: url) 89 | if !allowed { 90 | NSSound.beep() 91 | throw SandboxDelegateError.triedToChooseWrongFile 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftySandboxFileAccess/SandboxFileAccessPersist.swift: -------------------------------------------------------------------------------- 1 | 2 | // Created by Leigh McCulloch on 23/11/2013. 3 | // 4 | // Copyright (c) 2013, Leigh McCulloch 5 | // All rights reserved. 6 | // 7 | // BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause 8 | // 9 | // Redistribution and use in source and binary forms, with or without 10 | // modification, are permitted provided that the following conditions are 11 | // met: 12 | // 13 | // 1. Redistributions of source code must retain the above copyright 14 | // notice, this list of conditions and the following disclaimer. 15 | // 16 | // 2. Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 21 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 23 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 26 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | // 32 | 33 | import Foundation 34 | 35 | public class SandboxFileAccessPersist: SandboxFileAccessProtocol { 36 | 37 | public func bookmarkData(for url: URL) -> Data? { 38 | let defaults = UserDefaults.standard 39 | 40 | // loop through the bookmarks one path at a time down the URL 41 | var subURL = url 42 | while (subURL.path.count) > 1 { 43 | // give up when only '/' is left in the path 44 | let key = SandboxFileAccessPersist.keyForBookmarkData(for: subURL) 45 | let bookmark = defaults.data(forKey: key) 46 | if bookmark != nil { 47 | // if a bookmark is found, return it 48 | return bookmark 49 | } 50 | subURL = subURL.deletingLastPathComponent() 51 | } 52 | 53 | // no bookmarks for the URL, or parent to the URL were found 54 | return nil 55 | } 56 | 57 | public func setBookmark(data: Data?, for url: URL) { 58 | let defaults = UserDefaults.standard 59 | let key = SandboxFileAccessPersist.keyForBookmarkData(for: url) 60 | defaults.set(data, forKey: key) 61 | } 62 | 63 | public func clearBookmarkData(for url: URL) { 64 | let defaults = UserDefaults.standard 65 | let key = SandboxFileAccessPersist.keyForBookmarkData(for: url) 66 | defaults.removeObject(forKey: key) 67 | } 68 | 69 | class func keyForBookmarkData(for url: URL) -> String { 70 | let urlStr = url.absoluteString 71 | return String(format: "bd_%1$@", urlStr) 72 | } 73 | 74 | /// Handy dev/debugging option 75 | public class func deleteAllBookmarkData() { 76 | let allDefaults = UserDefaults.standard.dictionaryRepresentation() 77 | 78 | for key in allDefaults.keys { 79 | if key.hasPrefix("bd_") { 80 | UserDefaults.standard.removeObject(forKey: key) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccess.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "SwiftySandboxFileAccess" 4 | s.version = "2.0.2" 5 | s.summary = "A simpler way to access and store permissions for files outside of the AppStore sandbox." 6 | 7 | s.description = <<-DESC 8 | Deprecated in favour version 3 which requires Swift Package Manager 9 | A class that wraps up writing and accessing files outside a Mac apps App Sandbox files into a simple interface. 10 | The class will request permission from the user with a simple to understand dialog consistent 11 | with Apple's documentation and persist permissions across application runs. 12 | DESC 13 | 14 | s.homepage = "https://github.com/ConfusedVorlon/SwiftySandboxFileAccess" 15 | s.license = { :type => "BSD-2", :file => "LICENSE" } 16 | s.author = { "Rob Jonson" => "Rob@HobbyistSoftware.com","Leigh McCulloch" => "leigh@mcchouse.com" } 17 | s.platform = :osx, "10.9" 18 | s.source = { :git => "https://github.com/ConfusedVorlon/SwiftySandboxFileAccess.git", :tag => s.version } 19 | s.source_files = "Sources/SwiftySandboxFileAccess/*.{swift}" 20 | s.swift_version = '5' 21 | s.deprecated = true 22 | 23 | end 24 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D419ADF824850EB6009B580B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D419ADF724850EB6009B580B /* AppDelegate.swift */; }; 11 | D419ADFA24850EB6009B580B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D419ADF924850EB6009B580B /* ViewController.swift */; }; 12 | D419ADFC24850EB7009B580B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D419ADFB24850EB7009B580B /* Assets.xcassets */; }; 13 | D419ADFF24850EB7009B580B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D419ADFD24850EB7009B580B /* Main.storyboard */; }; 14 | D419AE0824851151009B580B /* Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D419AE0724851151009B580B /* Manager.swift */; }; 15 | D43EB6F22680932C00CD3D4D /* AccessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6EC2680932C00CD3D4D /* AccessInfo.swift */; }; 16 | D43EB6F32680932C00CD3D4D /* SandboxFileAccessPersist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6ED2680932C00CD3D4D /* SandboxFileAccessPersist.swift */; }; 17 | D43EB6F42680932C00CD3D4D /* SandboxFileAccessOpenSavePanelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6EE2680932C00CD3D4D /* SandboxFileAccessOpenSavePanelDelegate.swift */; }; 18 | D43EB6F52680932C00CD3D4D /* SandboxFileAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6EF2680932C00CD3D4D /* SandboxFileAccess.swift */; }; 19 | D43EB6F62680932C00CD3D4D /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6F02680932C00CD3D4D /* Permissions.swift */; }; 20 | D43EB6F72680932C00CD3D4D /* PowerboxAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43EB6F12680932C00CD3D4D /* PowerboxAccess.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | D419ADF424850EB6009B580B /* SwiftySandboxDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftySandboxDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | D419ADF724850EB6009B580B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | D419ADF924850EB6009B580B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | D419ADFB24850EB7009B580B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | D419ADFE24850EB7009B580B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | D419AE0024850EB7009B580B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | D419AE0124850EB7009B580B /* SwiftySandboxDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftySandboxDemo.entitlements; sourceTree = ""; }; 31 | D419AE0724851151009B580B /* Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Manager.swift; sourceTree = ""; }; 32 | D43EB6EC2680932C00CD3D4D /* AccessInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessInfo.swift; sourceTree = ""; }; 33 | D43EB6ED2680932C00CD3D4D /* SandboxFileAccessPersist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxFileAccessPersist.swift; sourceTree = ""; }; 34 | D43EB6EE2680932C00CD3D4D /* SandboxFileAccessOpenSavePanelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxFileAccessOpenSavePanelDelegate.swift; sourceTree = ""; }; 35 | D43EB6EF2680932C00CD3D4D /* SandboxFileAccess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SandboxFileAccess.swift; sourceTree = ""; }; 36 | D43EB6F02680932C00CD3D4D /* Permissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 37 | D43EB6F12680932C00CD3D4D /* PowerboxAccess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PowerboxAccess.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | D419ADF124850EB6009B580B /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 1039AF53A9D5F7F504580F42 /* Pods */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | ); 55 | path = Pods; 56 | sourceTree = ""; 57 | }; 58 | D419ADEB24850EB6009B580B = { 59 | isa = PBXGroup; 60 | children = ( 61 | D419ADF624850EB6009B580B /* SwiftySandboxDemo */, 62 | D419ADF524850EB6009B580B /* Products */, 63 | 1039AF53A9D5F7F504580F42 /* Pods */, 64 | ); 65 | sourceTree = ""; 66 | }; 67 | D419ADF524850EB6009B580B /* Products */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | D419ADF424850EB6009B580B /* SwiftySandboxDemo.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | D419ADF624850EB6009B580B /* SwiftySandboxDemo */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | D43EB6EB2680932C00CD3D4D /* SwiftySandboxFileAccess */, 79 | D419AE0724851151009B580B /* Manager.swift */, 80 | D419ADF724850EB6009B580B /* AppDelegate.swift */, 81 | D419ADF924850EB6009B580B /* ViewController.swift */, 82 | D419ADFB24850EB7009B580B /* Assets.xcassets */, 83 | D419ADFD24850EB7009B580B /* Main.storyboard */, 84 | D419AE0024850EB7009B580B /* Info.plist */, 85 | D419AE0124850EB7009B580B /* SwiftySandboxDemo.entitlements */, 86 | ); 87 | path = SwiftySandboxDemo; 88 | sourceTree = ""; 89 | }; 90 | D43EB6EB2680932C00CD3D4D /* SwiftySandboxFileAccess */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | D43EB6EC2680932C00CD3D4D /* AccessInfo.swift */, 94 | D43EB6ED2680932C00CD3D4D /* SandboxFileAccessPersist.swift */, 95 | D43EB6EE2680932C00CD3D4D /* SandboxFileAccessOpenSavePanelDelegate.swift */, 96 | D43EB6EF2680932C00CD3D4D /* SandboxFileAccess.swift */, 97 | D43EB6F02680932C00CD3D4D /* Permissions.swift */, 98 | D43EB6F12680932C00CD3D4D /* PowerboxAccess.swift */, 99 | ); 100 | name = SwiftySandboxFileAccess; 101 | path = ../../Sources/SwiftySandboxFileAccess; 102 | sourceTree = ""; 103 | }; 104 | /* End PBXGroup section */ 105 | 106 | /* Begin PBXNativeTarget section */ 107 | D419ADF324850EB6009B580B /* SwiftySandboxDemo */ = { 108 | isa = PBXNativeTarget; 109 | buildConfigurationList = D419AE0424850EB7009B580B /* Build configuration list for PBXNativeTarget "SwiftySandboxDemo" */; 110 | buildPhases = ( 111 | D419ADF024850EB6009B580B /* Sources */, 112 | D419ADF124850EB6009B580B /* Frameworks */, 113 | D419ADF224850EB6009B580B /* Resources */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = SwiftySandboxDemo; 120 | packageProductDependencies = ( 121 | ); 122 | productName = SwiftySandboxDemo; 123 | productReference = D419ADF424850EB6009B580B /* SwiftySandboxDemo.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | /* End PBXNativeTarget section */ 127 | 128 | /* Begin PBXProject section */ 129 | D419ADEC24850EB6009B580B /* Project object */ = { 130 | isa = PBXProject; 131 | attributes = { 132 | LastSwiftUpdateCheck = 1150; 133 | LastUpgradeCheck = 1150; 134 | ORGANIZATIONNAME = HobbyistSoftware; 135 | TargetAttributes = { 136 | D419ADF324850EB6009B580B = { 137 | CreatedOnToolsVersion = 11.5; 138 | LastSwiftMigration = 1250; 139 | }; 140 | }; 141 | }; 142 | buildConfigurationList = D419ADEF24850EB6009B580B /* Build configuration list for PBXProject "SwiftySandboxDemo" */; 143 | compatibilityVersion = "Xcode 9.3"; 144 | developmentRegion = en; 145 | hasScannedForEncodings = 0; 146 | knownRegions = ( 147 | en, 148 | Base, 149 | ); 150 | mainGroup = D419ADEB24850EB6009B580B; 151 | packageReferences = ( 152 | ); 153 | productRefGroup = D419ADF524850EB6009B580B /* Products */; 154 | projectDirPath = ""; 155 | projectRoot = ""; 156 | targets = ( 157 | D419ADF324850EB6009B580B /* SwiftySandboxDemo */, 158 | ); 159 | }; 160 | /* End PBXProject section */ 161 | 162 | /* Begin PBXResourcesBuildPhase section */ 163 | D419ADF224850EB6009B580B /* Resources */ = { 164 | isa = PBXResourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | D419ADFC24850EB7009B580B /* Assets.xcassets in Resources */, 168 | D419ADFF24850EB7009B580B /* Main.storyboard in Resources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXResourcesBuildPhase section */ 173 | 174 | /* Begin PBXSourcesBuildPhase section */ 175 | D419ADF024850EB6009B580B /* Sources */ = { 176 | isa = PBXSourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | D43EB6F62680932C00CD3D4D /* Permissions.swift in Sources */, 180 | D43EB6F52680932C00CD3D4D /* SandboxFileAccess.swift in Sources */, 181 | D43EB6F72680932C00CD3D4D /* PowerboxAccess.swift in Sources */, 182 | D419ADFA24850EB6009B580B /* ViewController.swift in Sources */, 183 | D43EB6F22680932C00CD3D4D /* AccessInfo.swift in Sources */, 184 | D43EB6F42680932C00CD3D4D /* SandboxFileAccessOpenSavePanelDelegate.swift in Sources */, 185 | D419AE0824851151009B580B /* Manager.swift in Sources */, 186 | D43EB6F32680932C00CD3D4D /* SandboxFileAccessPersist.swift in Sources */, 187 | D419ADF824850EB6009B580B /* AppDelegate.swift in Sources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXSourcesBuildPhase section */ 192 | 193 | /* Begin PBXVariantGroup section */ 194 | D419ADFD24850EB7009B580B /* Main.storyboard */ = { 195 | isa = PBXVariantGroup; 196 | children = ( 197 | D419ADFE24850EB7009B580B /* Base */, 198 | ); 199 | name = Main.storyboard; 200 | sourceTree = ""; 201 | }; 202 | /* End PBXVariantGroup section */ 203 | 204 | /* Begin XCBuildConfiguration section */ 205 | D419AE0224850EB7009B580B /* Debug */ = { 206 | isa = XCBuildConfiguration; 207 | buildSettings = { 208 | ALWAYS_SEARCH_USER_PATHS = NO; 209 | CLANG_ANALYZER_NONNULL = YES; 210 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 211 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 212 | CLANG_CXX_LIBRARY = "libc++"; 213 | CLANG_ENABLE_MODULES = YES; 214 | CLANG_ENABLE_OBJC_ARC = YES; 215 | CLANG_ENABLE_OBJC_WEAK = YES; 216 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 217 | CLANG_WARN_BOOL_CONVERSION = YES; 218 | CLANG_WARN_COMMA = YES; 219 | CLANG_WARN_CONSTANT_CONVERSION = YES; 220 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 221 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 222 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 223 | CLANG_WARN_EMPTY_BODY = YES; 224 | CLANG_WARN_ENUM_CONVERSION = YES; 225 | CLANG_WARN_INFINITE_RECURSION = YES; 226 | CLANG_WARN_INT_CONVERSION = YES; 227 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 228 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 229 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 230 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 231 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 232 | CLANG_WARN_STRICT_PROTOTYPES = YES; 233 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 234 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 235 | CLANG_WARN_UNREACHABLE_CODE = YES; 236 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 237 | COPY_PHASE_STRIP = NO; 238 | DEBUG_INFORMATION_FORMAT = dwarf; 239 | ENABLE_STRICT_OBJC_MSGSEND = YES; 240 | ENABLE_TESTABILITY = YES; 241 | GCC_C_LANGUAGE_STANDARD = gnu11; 242 | GCC_DYNAMIC_NO_PIC = NO; 243 | GCC_NO_COMMON_BLOCKS = YES; 244 | GCC_OPTIMIZATION_LEVEL = 0; 245 | GCC_PREPROCESSOR_DEFINITIONS = ( 246 | "DEBUG=1", 247 | "$(inherited)", 248 | ); 249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 251 | GCC_WARN_UNDECLARED_SELECTOR = YES; 252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 253 | GCC_WARN_UNUSED_FUNCTION = YES; 254 | GCC_WARN_UNUSED_VARIABLE = YES; 255 | MACOSX_DEPLOYMENT_TARGET = 10.15; 256 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 257 | MTL_FAST_MATH = YES; 258 | ONLY_ACTIVE_ARCH = YES; 259 | SDKROOT = macosx; 260 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 261 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 262 | }; 263 | name = Debug; 264 | }; 265 | D419AE0324850EB7009B580B /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_NONNULL = YES; 270 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 272 | CLANG_CXX_LIBRARY = "libc++"; 273 | CLANG_ENABLE_MODULES = YES; 274 | CLANG_ENABLE_OBJC_ARC = YES; 275 | CLANG_ENABLE_OBJC_WEAK = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_BOOL_CONVERSION = YES; 278 | CLANG_WARN_COMMA = YES; 279 | CLANG_WARN_CONSTANT_CONVERSION = YES; 280 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 281 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 282 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 283 | CLANG_WARN_EMPTY_BODY = YES; 284 | CLANG_WARN_ENUM_CONVERSION = YES; 285 | CLANG_WARN_INFINITE_RECURSION = YES; 286 | CLANG_WARN_INT_CONVERSION = YES; 287 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 289 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 290 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 292 | CLANG_WARN_STRICT_PROTOTYPES = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 295 | CLANG_WARN_UNREACHABLE_CODE = YES; 296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 299 | ENABLE_NS_ASSERTIONS = NO; 300 | ENABLE_STRICT_OBJC_MSGSEND = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu11; 302 | GCC_NO_COMMON_BLOCKS = YES; 303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 305 | GCC_WARN_UNDECLARED_SELECTOR = YES; 306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 307 | GCC_WARN_UNUSED_FUNCTION = YES; 308 | GCC_WARN_UNUSED_VARIABLE = YES; 309 | MACOSX_DEPLOYMENT_TARGET = 10.15; 310 | MTL_ENABLE_DEBUG_INFO = NO; 311 | MTL_FAST_MATH = YES; 312 | SDKROOT = macosx; 313 | SWIFT_COMPILATION_MODE = wholemodule; 314 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 315 | }; 316 | name = Release; 317 | }; 318 | D419AE0524850EB7009B580B /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 322 | CLANG_ENABLE_MODULES = YES; 323 | CODE_SIGN_ENTITLEMENTS = SwiftySandboxDemo/SwiftySandboxDemo.entitlements; 324 | CODE_SIGN_STYLE = Automatic; 325 | COMBINE_HIDPI_IMAGES = YES; 326 | DEVELOPMENT_TEAM = JHV5RC82C3; 327 | ENABLE_HARDENED_RUNTIME = YES; 328 | INFOPLIST_FILE = SwiftySandboxDemo/Info.plist; 329 | LD_RUNPATH_SEARCH_PATHS = ( 330 | "$(inherited)", 331 | "@executable_path/../Frameworks", 332 | ); 333 | MACOSX_DEPLOYMENT_TARGET = 10.12; 334 | PRODUCT_BUNDLE_IDENTIFIER = com.HobbyistSoftware.SwiftySandboxDemo; 335 | PRODUCT_NAME = "$(TARGET_NAME)"; 336 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 337 | SWIFT_VERSION = 5.0; 338 | }; 339 | name = Debug; 340 | }; 341 | D419AE0624850EB7009B580B /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | CLANG_ENABLE_MODULES = YES; 346 | CODE_SIGN_ENTITLEMENTS = SwiftySandboxDemo/SwiftySandboxDemo.entitlements; 347 | CODE_SIGN_STYLE = Automatic; 348 | COMBINE_HIDPI_IMAGES = YES; 349 | DEVELOPMENT_TEAM = JHV5RC82C3; 350 | ENABLE_HARDENED_RUNTIME = YES; 351 | INFOPLIST_FILE = SwiftySandboxDemo/Info.plist; 352 | LD_RUNPATH_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "@executable_path/../Frameworks", 355 | ); 356 | MACOSX_DEPLOYMENT_TARGET = 10.12; 357 | PRODUCT_BUNDLE_IDENTIFIER = com.HobbyistSoftware.SwiftySandboxDemo; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | SWIFT_VERSION = 5.0; 360 | }; 361 | name = Release; 362 | }; 363 | /* End XCBuildConfiguration section */ 364 | 365 | /* Begin XCConfigurationList section */ 366 | D419ADEF24850EB6009B580B /* Build configuration list for PBXProject "SwiftySandboxDemo" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | D419AE0224850EB7009B580B /* Debug */, 370 | D419AE0324850EB7009B580B /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | D419AE0424850EB7009B580B /* Build configuration list for PBXNativeTarget "SwiftySandboxDemo" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | D419AE0524850EB7009B580B /* Debug */, 379 | D419AE0624850EB7009B580B /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | /* End XCConfigurationList section */ 385 | }; 386 | rootObject = D419ADEC24850EB6009B580B /* Project object */; 387 | } 388 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftySandboxDemo 4 | // 5 | // Created by Rob Jonson on 01/06/2020. 6 | // Copyright © 2020 HobbyistSoftware. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | // Handle a file dropped on the dock icon 26 | func application(_ application: NSApplication, open urls: [URL]) { 27 | print("dock received: \(urls)") 28 | Manager.shared.persist(urls) 29 | return 30 | } 31 | 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | Default 531 | 532 | 533 | 534 | 535 | 536 | 537 | Left to Right 538 | 539 | 540 | 541 | 542 | 543 | 544 | Right to Left 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | Default 556 | 557 | 558 | 559 | 560 | 561 | 562 | Left to Right 563 | 564 | 565 | 566 | 567 | 568 | 569 | Right to Left 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 726 | 736 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | All Files 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Alternate 16 | LSItemContentTypes 17 | 18 | public.data 19 | public.content 20 | 21 | 22 | 23 | CFBundleExecutable 24 | $(EXECUTABLE_NAME) 25 | CFBundleIconFile 26 | 27 | CFBundleIdentifier 28 | $(PRODUCT_BUNDLE_IDENTIFIER) 29 | CFBundleInfoDictionaryVersion 30 | 6.0 31 | CFBundleName 32 | $(PRODUCT_NAME) 33 | CFBundlePackageType 34 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 35 | CFBundleShortVersionString 36 | 1.0 37 | CFBundleVersion 38 | 1 39 | LSMinimumSystemVersion 40 | $(MACOSX_DEPLOYMENT_TARGET) 41 | NSHumanReadableCopyright 42 | Copyright © 2020 HobbyistSoftware. All rights reserved. 43 | NSMainStoryboardFile 44 | Main 45 | NSPrincipalClass 46 | NSApplication 47 | NSSupportsAutomaticTermination 48 | 49 | NSSupportsSuddenTermination 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/Manager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Manager.swift 3 | // SwiftySandboxDemo 4 | // 5 | // Created by Rob Jonson on 01/06/2020. 6 | // Copyright © 2020 HobbyistSoftware. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | class Test { 13 | class func isAccessible(fromSandbox path: String?) -> Bool { 14 | return access((path as NSString?)?.fileSystemRepresentation, R_OK) == 0 15 | } 16 | } 17 | 18 | class Manager { 19 | static let shared = Manager() 20 | 21 | /// Persist URL when a file is dropped on the dock (so permission is implicitly given) 22 | func persist(_ urls:[URL]){ 23 | let access = SandboxFileAccess() 24 | for url in urls { 25 | _ = access.persistPermission(url:url) 26 | lastDockDroppedPath = url.path 27 | } 28 | } 29 | 30 | func clearStoredPermissions() { 31 | SandboxFileAccessPersist.deleteAllBookmarkData() 32 | } 33 | 34 | func pickFile(from window:NSWindow){ 35 | let access = SandboxFileAccess() 36 | access.access(fileURL: picturesURL, 37 | acceptablePermission: .anyReadOnly, 38 | askIfNecessary: true, 39 | fromWindow: window) {result in 40 | if case .success(let info) = result { 41 | print("access \(self.picturesURL) here") 42 | print("note that the url in info could be a parent URL. \(String(describing: info.securityScopedURL))") 43 | } 44 | 45 | } 46 | } 47 | 48 | func accessDownloads(from window:NSWindow){ 49 | 50 | let access = SandboxFileAccess() 51 | access.access(fileURL: downloadURL, 52 | acceptablePermission: .powerboxReadOnly, 53 | askIfNecessary: false) {result in 54 | if case .success = result { 55 | print("access \(String(describing: self.downloadURL)) here") 56 | } 57 | } 58 | } 59 | 60 | func pickFile() { 61 | let access = SandboxFileAccess() 62 | access.access(fileURL: picturesURL, 63 | askIfNecessary: true) {result in 64 | switch result { 65 | 66 | case .success(_): 67 | print("access \(String(describing: self.picturesURL)) here") 68 | case .failure(let error): 69 | print("access failed \(error)") 70 | } 71 | 72 | 73 | } 74 | 75 | } 76 | 77 | func checkAccessToLastDockDroppedPath() { 78 | guard let lastOpenedPath = lastDockDroppedPath else { 79 | return 80 | } 81 | 82 | let lastOpenedURL = URL(fileURLWithPath: lastOpenedPath) 83 | 84 | let access = SandboxFileAccess() 85 | access.access(fileURL: lastOpenedURL, 86 | askIfNecessary: false) {result in 87 | if case .success = result { 88 | print("success: \(lastOpenedURL)") 89 | } 90 | } 91 | } 92 | 93 | //MARK: Utilities 94 | 95 | 96 | 97 | let picturesURL:URL = { 98 | return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Pictures") 99 | }() 100 | 101 | let downloadURL:URL = { 102 | return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Downloads") 103 | }() 104 | 105 | 106 | static let lastDockDroppedPathKey = "lastDockDroppedPath" 107 | var lastDockDroppedPath:String? { 108 | get { 109 | return UserDefaults.standard.string(forKey: Manager.lastDockDroppedPathKey) 110 | } 111 | set { 112 | UserDefaults.standard.set(newValue, forKey: Manager.lastDockDroppedPathKey) 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/SwiftySandboxDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.bookmarks.app-scope 8 | 9 | com.apple.security.files.downloads.read-only 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftySandboxFileAccessDemo/SwiftySandboxDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftySandboxDemo 4 | // 5 | // Created by Rob Jonson on 01/06/2020. 6 | // Copyright © 2020 HobbyistSoftware. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override var representedObject: Any? { 20 | didSet { 21 | // Update the view, if already loaded. 22 | } 23 | } 24 | 25 | @IBAction func clearStoredPermissions(_ sender: Any) { 26 | Manager.shared.clearStoredPermissions() 27 | } 28 | 29 | 30 | @IBAction func accessDownloads(_ sender: Any) { 31 | Manager.shared.accessDownloads(from:self.view.window!) 32 | } 33 | 34 | 35 | @IBAction func pickFileInSheet(_ sender: Any) { 36 | Manager.shared.pickFile(from:self.view.window!) 37 | } 38 | 39 | @IBAction func checkAccess(_ sender: Any) { 40 | Manager.shared.checkAccessToLastDockDroppedPath() 41 | } 42 | 43 | } 44 | 45 | --------------------------------------------------------------------------------