I am currently working on a safari app extension that blocks content. I want the user to configure the rule (turning a rule on and off). Since I can’t overwrite the bundled JSON files and we can’t write to the documents folder, as it’s not accessible to the extension I decided to use App Groups. My approach looks like this:
Within the ContentBlockerRequestHandler I want to save the blockerList.json into the app group (Only when launched for the first time)
When this is done I want that the handler reads from the app group by taking the url of my json which is within the app group instead of taking the default json in the extension
Since I can not debug the handler I don't know if I am on the right path. The following shows my code:
class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
guard let rulesUrl = loadRules() else {
let clonedRules = cloneBlockerList()
save(rules: clonedRules)
return
}
guard let attachment = NSItemProvider(contentsOf: rulesUrl) else { return }
let item = NSExtensionItem()
item.attachments = [attachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
private func cloneBlockerList() -> [Rule] {
var rules: [Rule] = []
if let url = Bundle.main.url(forResource: "blockerList", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(ResponseData.self, from: data)
rules = jsonData.rules
} catch {
print("error:(error)")
}
}
return rules
}
private func save(rules: [Rule]) {
let documentsDirectory = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "my group identifier")
let archiveURL = documentsDirectory?.appendingPathComponent("rules.json")
let encoder = JSONEncoder()
if let dataToSave = try? encoder.encode(rules) {
do {
try dataToSave.write(to: archiveURL!)
} catch {
// TODO: ("Error: Can't save Counters")
return;
}
}
}
private func loadRules() -> URL? {
let documentFolder = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "my group identifier")
guard let jsonURL = documentFolder?.appendingPathComponent("rules.json") else {
return nil
}
return jsonURL
}
}
Thankful for any help
I am new with MACOS Development and making one app. In my application, I am opening NSOpenPanel and selecting a folder or any directory(like download, document) then NSOPenPanel begin method gives me the selected URL. I want to save multiple images on that selected URL but unable to achieve that facing this error.
The file “xxx” couldn’t be saved in the folder “xxx”.
#IBAction func menuExportTapped(_ sender: NSMenuItem) {
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = false
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = true
openPanel.title = NSLocalizedString("change_the_folder", comment: "")
openPanel.runModal()
openPanel.begin { [weak self] (result) -> Void in
if result == .OK {
self?.myImage?.writePNG(toURL: openPanel.url!)
} else {
openPanel.close()
}
}
}
I tested writePNG code it's working fine.
extension NSImage {
public func writePNG(toURL url: URL) {
guard let data = tiffRepresentation,
let rep = NSBitmapImageRep(data: data),
let imgData = rep.representation(using: .png, properties: [.compressionFactor : NSNumber(floatLiteral: 1.0)]) else {
Swift.print("\(self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)")
return
}
do {
try imgData.write(to: url)
}catch let error {
Swift.print("\(self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)")
}
}
}
Please help me with this. Thank You!
I have an (n) of data (UIImage JPEG) inside my CoreData.
let imageData: [Data]...
I have already this two frameworks/ Pods: Zip and ZIPFoundation
I have a few Question about that:
I need to create a temp URL for each of my imageData?
If yes, I have to add tempURL.appendingPathExtension("jpg") to each temp URLs before or after call data.write(to: tempURL) ?
After that, I have an Array of URLs, so I just need to create a Zip File and share it. But it doesn't work, I get a .zip - .cpgz Loop on my Mac.
private func createURLsFrom(imageData: [ImageData]?) {
var urlArray = [URL]()
imageData?.forEach { imData in
if let data = imData.imageData,
let tempURL = NSURL.fileURL(withPathComponents: [NSTemporaryDirectory(), NSUUID().uuidString])?.appendingPathExtension("jpg") {
do {
try data.write(to: tempURL)
urlArray.append(tempURL)
} catch {...}
}
}
self.createZipFile(urlArray: urlArray)
}
private func createZipFile(urlArray: [URL]) {
if let zipURL = try? Zip.quickZipFiles(urlArray, fileName: "ZIP_Test1") {
self.sendEmailWith(dataURL: zipURL)
} else {...}
}
private func sendEmailWith(dataURL: URL) {
if MFMailComposeViewController.canSendMail() {
let mailComposer = MFMailComposeViewController()
mailComposer.mailComposeDelegate = self
mailComposer.setSubject("setSubject")
mailComposer.setMessageBody("setMessageBody", isHTML: false)
mailComposer.addAttachmentData(dataURL.dataRepresentation, mimeType: "application/zip", fileName: ("ZIP_Test1.zip"))
self.present(mailComposer, animated: true, completion: nil)
}
}
What am I doing wrong :(
It's a bit lengthy, and––disclaimer––untested. Let me know if it works or if you have any questions.
Create a temp directory for all the files:
func createTempDirectory() -> URL? {
if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let dir = documentDirectory.appendingPathComponent("temp-dir-\(UUID().uuidString)")
do {
try FileManager.default.createDirectory(atPath: dir.path, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error.localizedDescription)
}
return dir
} else {
return nil
}
}
Save all the images to the temp directory:
func saveImages(data: [Data]) -> URL? {
guard let directory = createTempDirectory() else { return nil }
do {
for (i, imageData) in data.enumerated() {
try imageData.write(to: directory.appendingPathComponent("image\(i).jpg"))
}
return directory
} catch {
return nil
}
}
Get the URL for the zipped file. This is an optional in case an error occurred along the way. Also, done on a background thread because it could take a bit of time, and you don't want to block the main thread.
func zipImages(data: [Data], completion: #escaping ((URL?) -> ())) {
DispatchQueue.main.async {
guard let directory = saveImages(data: data) else {
completion(nil)
return
}
do {
let zipFilePath = try Zip.quickZipFiles([directory], fileName: "archive-\(UUID().uuidString)")
completion(zipFilePath)
} catch {
completion(nil)
}
}
}
After you send the file, you'll probably want to delete the temp directory so your app size doesn't start growing.
After a lot of debugging finally, I found the problem.
First of all Daniels answer was correct too.
But the main issue was my sendEmailWith function, "application/zip" is not working!
So I added UIActivityViewController with the directory URL and it's work.
Now I can share my Zip file via Mail and Airdrop!
Move the
self.createZipFile(urlArray: urlArray)
to after the forEach loop.
I am developing MAC OS app which have functionality to create file on the behalf of your. First user select folder for storing file (One time at start of app) and then user can select type and name of the file user want to create on selected folder (Folder selected on start of the app) using apple script. I am able to create file when i add below temporary-exception in entitlement file but its not able to app apple review team but works in sandboxing.
Guideline 2.4.5(i) - Performance
We've determined that one or more temporary entitlement exceptions requested for this app are not appropriate and will not be granted:
com.apple.security.temporary-exception.files.home-relative-path.read-write
/FolderName/
I found :
Enabling App Sandbox - Allows apps to write executable files.
And
Enabling User-Selected File Access - Xcode provides a pop-up menu, in the Summary tab of the target editor, with choices to enable read-only or read/write access to files and folders that the user explicitly selects. When you enable user-selected file access, you gain programmatic access to files and folders that the user opens using an NSOpenPanel object, and files the user saves using an NSSavePanel object.
Using below code for creating file :
let str = "Super long string here"
let filename = getDocumentsDirectory().appendingPathComponent("/xyz/output.txt")
do {
try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}
Also tried adding com.apple.security.files.user-selected.read-write in entitlement file for an NSOpenPanel object :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
Is there any way to get pass apple review team to approve Mac App with read and write permission to user selected folder ?
Here is my Answer
How to do implement and persist Read and write permission of user selected folder in Mac OS app?
GitHub Example Project link
First :
Add user-selected and bookmarks.app permissions in entitlement file :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Then i created class for all bookmark related function required for storeing, loading ... etc bookmarks app.
import Foundation
import Cocoa
var bookmarks = [URL: Data]()
func openFolderSelection() -> URL?
{
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = true
openPanel.canChooseFiles = false
openPanel.begin
{ (result) -> Void in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue
{
let url = openPanel.url
storeFolderInBookmark(url: url!)
}
}
return openPanel.url
}
func saveBookmarksData()
{
let path = getBookmarkPath()
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: path)
}
func storeFolderInBookmark(url: URL)
{
do
{
let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
}
catch
{
Swift.print ("Error storing bookmarks")
}
}
func getBookmarkPath() -> String
{
var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
url = url.appendingPathComponent("Bookmarks.dict")
return url.path
}
func loadBookmarks()
{
let path = getBookmarkPath()
bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: path) as! [URL: Data]
for bookmark in bookmarks
{
restoreBookmark(bookmark)
}
}
func restoreBookmark(_ bookmark: (key: URL, value: Data))
{
let restoredUrl: URL?
var isStale = false
Swift.print ("Restoring \(bookmark.key)")
do
{
restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
}
catch
{
Swift.print ("Error restoring bookmarks")
restoredUrl = nil
}
if let url = restoredUrl
{
if isStale
{
Swift.print ("URL is stale")
}
else
{
if !url.startAccessingSecurityScopedResource()
{
Swift.print ("Couldn't access: \(url.path)")
}
}
}
}
Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.
To open NSOpenPanel :
let selectedURL = openFolderSelection()
saveBookmarksData()
and to load existing bookmark after app close :
loadBookmarks()
Thats it.
I Hope it will help someone.
Add user-selected and bookmarks.app permissions in entitlement file :
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.
Since 'unarchiveObject(withFile:)' was deprecated in macOS 10.14, created a new answer in case someone has a similar question.
So after setting this in plist,
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Create a BookMark class like below:
import Foundation
#objcMembers final class BookMarks: NSObject, NSSecureCoding {
struct Keys {
static let data = "data"
}
var data: [URL:Data] = [URL: Data]()
static var supportsSecureCoding: Bool = true
required init?(coder: NSCoder) {
self.data = coder.decodeObject(of: [NSDictionary.self, NSData.self, NSURL.self], forKey: Keys.data) as? [URL: Data] ?? [:]
}
required init(data: [URL: Data]) {
self.data = data
}
func encode(with coder: NSCoder) {
coder.encode(data, forKey: Keys.data)
}
func store(url: URL) {
do {
let bookmark = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
data[url] = bookmark
} catch {
print("Error storing bookmarks")
}
}
func dump() {
let path = Self.path()
do {
try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true).write(to: path)
} catch {
print("Error dumping bookmarks")
}
}
static func path() -> URL {
var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
url = url.appendingPathComponent("Bookmarks.dict")
return url
}
static func restore() -> BookMarks? {
let path = Self.path()
let nsdata = NSData(contentsOf: path)
guard nsdata != nil else { return nil }
do {
let bookmarks = try NSKeyedUnarchiver.unarchivedObject(ofClass: Self.self, from: nsdata! as Data)
for bookmark in bookmarks?.data ?? [:] {
Self.restore(bookmark)
}
return bookmarks
} catch {
// print(error.localizedDescription)
print("Error loading bookmarks")
return nil
}
}
static func restore(_ bookmark: (key: URL, value: Data)) {
let restoredUrl: URL?
var isStale = false
print("Restoring \(bookmark.key)")
do {
restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
} catch {
print("Error restoring bookmarks")
restoredUrl = nil
}
if let url = restoredUrl {
if isStale {
print("URL is stale")
} else {
if !url.startAccessingSecurityScopedResource() {
print("Couldn't access: \(url.path)")
}
}
}
}
}
Then using it:
loading
let bookmarks = BookMarks.restore() ?? BookMarks(data: [:])
adding
bookmarks.store(url: someUrl)
saving
bookmarks.dump()
Swift 5 with Xcode 14.2 - Jan-2023 :- below code works fine in my macOS app:
Keep below code in a class and follow instructions given after the code:
private static let BOOKMARK_KEY = "bookmark"
// Check permission is granted or not
public static func isPermissionGranted() -> Bool {
if let data = UserDefaults.standard.data(forKey: BOOKMARK_KEY) {
var bookmarkDataIsStale: ObjCBool = false
do {
let url = try (NSURL(resolvingBookmarkData: data, options: [.withoutUI, .withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale) as URL)
if bookmarkDataIsStale.boolValue {
NSLog("WARNING stale security bookmark")
return false
}
return url.startAccessingSecurityScopedResource()
} catch {
print(error.localizedDescription)
return false
}
}
return false
} // isPermissionGranted
static func selectFolder(folderPicked: () -> Void) {
let folderChooserPoint = CGPoint(x: 0, y: 0)
let folderChooserSize = CGSize(width: 450, height: 400)
let folderChooserRectangle = CGRect(origin: folderChooserPoint, size: folderChooserSize)
let folderPicker = NSOpenPanel(contentRect: folderChooserRectangle, styleMask: .utilityWindow, backing: .buffered, defer: true)
let homePath = "/Users/\(NSUserName())"
folderPicker.directoryURL = NSURL.fileURL(withPath: homePath, isDirectory: true)
folderPicker.canChooseDirectories = true
folderPicker.canChooseFiles = false
folderPicker.allowsMultipleSelection = false
folderPicker.canDownloadUbiquitousContents = false
folderPicker.canResolveUbiquitousConflicts = false
folderPicker.begin { response in
if response == .OK {
let url = folderPicker.urls
NSLog("\(url)")
// Save Url Bookmark
if let mUrl = folderPicker.url {
storeFolderInBookmark(url: mUrl)
}
}
}
}
private static func storeFolderInBookmark(url: URL) { // mark 1
do {
let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(data, forKey: BOOKMARK_KEY)
} catch {
NSLog("Error storing bookmarks")
}
}
How to Use:
isPermissionGranted() - this function is to check user has granted directory permission or not. If it returns true then use directory/file operation read/write. If it returns false then use call selectFolder() function
selectFolder() - if isPermissionGranted() returns false then call this function to take permission from user. user will just need to click on as home directory will choose automatically.
storeFolderInBookmark() - Just keep it in the code you don't need to modify it, it will save url as bookmark for future use
Hope this will help & save lots of time. Thanks.
I found the best and working answer here - reusing security scoped bookmark
Super simple, easy to understand and does the job pretty well.
The solution was :-
var userDefault = NSUserDefaults.standardUserDefaults()
var folderPath: NSURL? {
didSet {
do {
let bookmark = try folderPath?.bookmarkDataWithOptions(.SecurityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeToURL: nil)
userDefault.setObject(bookmark, forKey: "bookmark")
} catch let error as NSError {
print("Set Bookmark Fails: \(error.description)")
}
}
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let bookmarkData = userDefault.objectForKey("bookmark") as? NSData {
do {
let url = try NSURL.init(byResolvingBookmarkData: bookmarkData, options: .WithoutUI, relativeToURL: nil, bookmarkDataIsStale: nil)
url.startAccessingSecurityScopedResource()
} catch let error as NSError {
print("Bookmark Access Fails: \(error.description)")
}
}
}
Updated to Swift 5 (Thanks Jay!)
var folderPath: URL? {
didSet {
do {
let bookmark = try folderPath?.bookmarkData(options: .securityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(bookmark, forKey: "bookmark")
} catch let error as NSError {
print("Set Bookmark Fails: \(error.description)")
}
}
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let bookmarkData = UserDefaults.standard.object(forKey: "bookmark") as? Data {
do {
var bookmarkIsStale = false
let url = try URL.init(resolvingBookmarkData: bookmarkData as Data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkIsStale)
url.startAccessingSecurityScopedResource()
} catch let error as NSError {
print("Bookmark Access Fails: \(error.description)")
}
}
}
I am trying to persist read/write permissions to Applications folder through security-scoped bookmarks but unsuccessfully.
I use grandAccess() to open a panel and select the Applications folder and gain permissions. At the same time, the panel's URL is stored as a security-scoped bookmark in UserDefaults. Then I can delete apps in the Applications folder with FileManager.
When I relaunch my app, I resolve the stored bookmark for Applications folder path and while the startAccessingSecurityScopedResource() returns TRUE for this URL, If I try to delete the application I then get a permission error.
Any idea why this happens???
My code is below:
// Move to Trash
let startAccess = SecurityScopeManager.shared.startAccess()
guard startAccess == true else {
log.warning("Starting file access failed")
}
do {
let url = URL(fileURLWithPath: application.path)
try FileManager.default.trashItem(at: url, resultingItemURL: nil)
} catch let error {
log.error(error)
}
// End
class SecurityScopeManager: NSObject {
static let shared = SecurityScopeManager()
private let defaultsKey = DefaultsKeys.securityScopeBookmark.rawValue
private var url: URL?
func grandAccess() -> Bool {
guard resolveBookmarkData() == false else {
return true
}
let success = runPanel()
return success
}
private override init() {
super.init()
}
/// In the panel, I select the Applications folder and press the Open button
private func runPanel() -> Bool {
let applicationsFolderPath = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask).first!
let panel = NSOpenPanel()
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.directoryURL = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first!
if panel.runModal() == NSModalResponseOK {
guard let url = panel.url, url == applicationsFolderPath else {
return false
}
return storeBookmarkData(url: url)
} else {
return false
}
}
/// Save bookmark data to UserDefaults. Returns true if success
private func storeBookmarkData(url: URL) -> Bool {
do {
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(data, forKey: defaultsKey)
return true
} catch {
log.error(error.localizedDescription)
return false
}
}
/// Load bookmark data from UserDefaults
private func resolveBookmarkData() -> Bool {
guard let data = UserDefaults.standard.data(forKey: defaultsKey) else {
return false
}
do {
var isStale = Bool()
url = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
if isStale { // If cannot update bookmark delete previous bookmark data
UserDefaults.standard.removeObject(forKey: defaultsKey)
}
return true
} catch {
log.error(error)
return false
}
}
func startAccess() -> Bool {
_ = resolveBookmarkData()
guard url != nil else {
_ = grandAccess()
return false
}
let result = url?.startAccessingSecurityScopedResource() ?? false
return result
}
func stopAccess() {
url?.stopAccessingSecurityScopedResource()
}
}
UPDATE 1
What I discovered is that if I select in the panel the exact application that I want to delete (and not the Application directory )and save the bookmark of its path then it works.
The problem occurs when I select the Application folder in the panel. Then I don't have permission to delete an app under that directory after the relaunch of my app and the resolve of the stored bookmark.
UPDATE 2
I made a trial to delete a file under Desktop folder with security scoped bookmark and it worked. The issue comes with a bookmark for Applications folder.