Existing Realm Database notworking in swift 3.1 xcode 8 - swift

I created the application use existing database realm 2.3.0 and swift 3.1 xcode 8.3.
But when I try to access the realm database. there is an error.
Could not access database: Error Domain=io.realm Code=2 "Unable to open a realm at path '/Users/dodipurnomo/Library/Developer/CoreSimulator/Devices/858C796B-CBA8-424B-9A97-0893304B758B/data/Containers/Data/Application/A2D910EE-AAC5-4836-9FE7-97F744E802E5/Documents/Conversio.realm': Unsupported Realm file format version." UserInfo={NSFilePath=/Users/dodipurnomo/Library/Developer/CoreSimulator/Devices/858C796B-CBA8-424B-9A97-0893304B758B/data/Containers/Data/Application/A2D910EE-AAC5-4836-9FE7-97F744E802E5/Documents/Conversio.realm,
Above is an error message when I try to execute the database.
As for the class to hendleing the database realm is as follows:
import RealmSwift
import UIKit
class DBManager{
//MARK: - Singleton shared intance
static let sharedIntance = DBManager()
//MARK: - overide init function in realm
static var realm: Realm {
get {
do {
let realm = try Realm()
return realm
}
catch {
print("Could not access database: ", error)
}
return self.realm
}
}
public static func write(realm: Realm, writeClosure: () -> ()) {
do {
try realm.write {
writeClosure()
}
} catch {
print("Could not write to database: ", error)
}
}
public static func query(realm: Realm,queryClosure: () -> ()){
}
func save(entityList: [Object], shouldUpdate update: Bool = false) {
DBManager.realm.beginWrite()
for entity in entityList {
if let key = type(of: entity).primaryKey(), let value = entity[key] , update {
if let existingObject = DBManager.realm.object(ofType: type(of: entity), forPrimaryKey: value as AnyObject) {
let relationships = existingObject.objectSchema.properties.filter {
$0.type == .array
}
for relationship in relationships {
if let newObjectRelationship = entity[relationship.name] as? ListBase , newObjectRelationship.count == 0 {
entity[relationship.name] = existingObject[relationship.name]
}
}
}
}
DBManager.realm.add(entity, update: update)
}
do {
try DBManager.realm.commitWrite()
} catch let writeError {
debugPrint("Unable to commit write: \(writeError)")
}
DBManager.realm.refresh()
}
}
And I set the Realm in appdelegate as follows:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let desPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let fullDesPath = URL(fileURLWithPath: desPath).appendingPathComponent("Conversio.realm")
var config = Realm.Configuration()
config.deleteRealmIfMigrationNeeded = true
config.fileURL = fullDesPath
Realm.Configuration.defaultConfiguration = config
chekDB()
return true
}
//chek database
func chekDB() {
let bundleDB = Bundle.main.path(forResource: "Conversio", ofType: "realm")
let desPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let fileManager = FileManager.default
let fullDesPath = URL(fileURLWithPath: desPath).appendingPathComponent("Conversio.realm")
let fullDestPathString = String(describing: fullDesPath)
if fileManager.fileExists(atPath: fullDesPath.path){
print("Database file is exis !")
print(fileManager.fileExists(atPath: bundleDB!))
}else{
do{
try fileManager.copyItem(atPath: bundleDB!, toPath: fullDesPath.path)
}catch{
print("error encured while copying file to directori \(fullDestPathString)")
}
}
}

The error message you're getting means that realm file was created with newer version of Realm, so update Realm to the latest version.
Also keep in mind if you open realm with Realm Browser that uses a newer version of realm it asks you to convert the file format. If you do that you can open this realm with older version of realm-cocoa.

Related

In memory CoreData container is empty when initialized

tl;dr:
clone the project: https://github.com/Jasperav/CoreDataInMemoryFail
Run the test and see it fail. Why does my in memory container not have any data and how can I make sure it will have data?
Long:
I have a sqlite file with filled data and I have an in-memory database in CoreData. Some code:
// ...
func createInMemoryPerformanceTestDatabase() -> NSPersistentContainer {
let url = createPathToSomeSQLiteFile()
let container = NSPersistentContainer(name: dataModelName, managedObjectModel: objectModel)
let description = NSPersistentStoreDescription(url: url)
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { description, error in
XCTAssertNil(error)
}
return container
}
// ...
Although the sqlite file has data inside it, I don't see it back inside my contexts I create with container.
When I create an in-memory database with CoreData pointing to a sqlite file with data, I don't see any results when querying the database. I want to see the data inside the sqlite file. The data should just load all in memory. This is for testing purposes.
The problem with what you have tried was that you set the type of your storeDescription as NSInMemoryStoreType before loading them into the container. Since, the type of storeDescription is stated as NSInMemoryStoreType the api won't read and populate data from the file URL you have provided. In order for the api to read the data from the file url, the type of storeDescription must be the one defined by initialising with the initialiser init(url: URL) which is SQLite in your case.
However if you want to have a persistentStore of type NSInMemoryStoreType with data read from the file url, you can migrate the persistentStores of your persistentContainer with NSInMemoryStoreType type using function migratePersistentStore:toURL:options:withType:error:. You can try out the code snippet below.
import CoreData
import XCTest
#testable import CoreDataInMemoryFail
class CoreDataInMemoryFailTests: XCTestCase {
private func createContainer(modify: (NSPersistentContainer) -> ()) -> NSPersistentContainer {
let bundle = Bundle(for: type(of: self))
let path = bundle.path(forResource: "InMemoryDatabase", ofType: "sqlite")!
let url = URL(fileURLWithPath: path)
let persistentContainer = createPersistentContainer(dataModelName: "InMemoryDatabase")
let storeDescription = NSPersistentStoreDescription(url: url)
persistentContainer.persistentStoreDescriptions = [storeDescription]
persistentContainer.loadPersistentStores { description, error in
XCTAssertEqual(storeDescription.type, description.type)
XCTAssertNil(error)
}
modify(persistentContainer)
return persistentContainer
}
func testFail() {
let persistentContainer = createContainer(modify: { _ in })
let inMemoryContainer = createContainer { persistentContainer in
let coordinator = persistentContainer.persistentStoreCoordinator
coordinator.persistentStores.forEach { (persistentStore) in
do {
try coordinator.migratePersistentStore(persistentStore, to: NSPersistentContainer.defaultDirectoryURL(), options: nil, withType: NSInMemoryStoreType)
} catch {
print("Error while migrating persistentStore")
}
}
}
let persistentContainerCoordinator = persistentContainer.persistentStoreCoordinator
persistentContainerCoordinator.persistentStores.forEach { (persistentStore) in
XCTAssertEqual(persistentStore.type, "SQLite")
}
let inMemoryContainerCoordinator = inMemoryContainer.persistentStoreCoordinator
inMemoryContainerCoordinator.persistentStores.forEach { (persistentStore) in
XCTAssertEqual(persistentStore.type, NSInMemoryStoreType)
}
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
let persistentContainerCount = (try! persistentContainer.viewContext.fetch(fetchRequest)).count
let inMemoryContainerCount = (try! inMemoryContainer.viewContext.fetch(fetchRequest)).count
XCTAssertEqual(8, persistentContainerCount)
XCTAssertEqual(persistentContainerCount, inMemoryContainerCount)
}
}
In the above snippet, I have also added asserts to verify whether persistentStore type is NSInMemoryStoreType in your inMemoryContainer and SQLite in your persistentContainer. Hope it helps.
The InMemoryType is not loading the date from your url as the other answer suggests. If you need to load the data from the file then please use the Migrate approach mentioned however if you only need to fill it with random data for testing purposes then here is another solution.
import CoreData
import XCTest
#testable import CoreDataInMemoryFail
class CoreDataInMemoryFailTests: XCTestCase {
var persistentContainer: NSPersistentContainer!
var inMemoryContainer: NSPersistentContainer!
override func setUp() {
super.setUp()
persistentContainer = createContainer(modify: { _ in })
inMemoryContainer = createContainer { storeDescription in
storeDescription.type = NSInMemoryStoreType
}
initStubs()
}
override class func tearDown() {
super.tearDown()
}
private func createContainer(modify: (NSPersistentStoreDescription) -> ()) -> NSPersistentContainer {
let bundle = Bundle(for: type(of: self))
let path = bundle.path(forResource: "InMemoryDatabase", ofType: "sqlite")!
let url = URL(fileURLWithPath: path)
let fileManager = FileManager.default
let uuid = UUID().uuidString
let saveDirectory = fileManager
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent(uuid)
let saveLocation = saveDirectory.appendingPathComponent(url.lastPathComponent)
try! fileManager.createDirectory(at: saveDirectory, withIntermediateDirectories: false)
try! fileManager.copyItem(at: url, to: saveLocation)
let persistentContainer = createPersistentContainer(dataModelName: "InMemoryDatabase")
let storeDescription = NSPersistentStoreDescription(url: saveLocation)
modify(storeDescription)
print("TYPE OF STORE IS: \(storeDescription)")
persistentContainer.persistentStoreDescriptions = [storeDescription]
persistentContainer.loadPersistentStores { description, error in
XCTAssertEqual(storeDescription.type, description.type)
XCTAssertNil(error)
}
return persistentContainer
}
func initStubs() {
func inserPerson( age: Int32) -> Person? {
let obj = NSEntityDescription.insertNewObject(forEntityName: "Person", into: inMemoryContainer.viewContext)
obj.setValue(age, forKey: "age")
return obj as? Person
}
_ = inserPerson(age: 1)
_ = inserPerson(age: 2)
_ = inserPerson(age: 3)
_ = inserPerson(age: 4)
_ = inserPerson(age: 5)
do {
try inMemoryContainer.viewContext.save()
} catch {
print("create fakes error \(error)")
}
}
func removeData() {
let fetchRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
let objs = try! inMemoryContainer.viewContext.fetch(fetchRequest)
for case let obj as NSManagedObject in objs {
inMemoryContainer.viewContext.delete(obj)
}
try! inMemoryContainer.viewContext.save()
}
func testFail() {
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
let persistentContainerCount = (try! persistentContainer.viewContext.fetch(fetchRequest)).count
let inMemoryContainerCount = (try! inMemoryContainer.viewContext.fetch(fetchRequest)).count
XCTAssertEqual(8, persistentContainerCount)
XCTAssertEqual(5, inMemoryContainerCount)
}
}
More info can be found here

Swift Core Data Class method?

I'm currently learning Core Data and I have two view controllers that are using the same piece of code to get a users profile. The problem is that it's the same code copy and pasted and I would like to avoid this. I'm using the Managed Class approach to access the data and each controller has the following method:
var profileHolder: Profile!
let profileRequest = Profile.createFetchRequest()
profileRequest.predicate = NSPredicate(format: "id == %d", 1)
profileRequest.fetchLimit = 1
if let profiles = try? context.fetch(profileRequest) {
if profiles.count > 0 {
profileHolder = profiles[0]
}
}
if profileHolder == nil {
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
profileHolder = newProfile
}
profile = profileHolder
Profile is a var inside the controller: var profile: Profile! and I call the above inside viewWillAppear()
I know there's a cleaner approach and I would like to move this logic inside the class but unsure how to.
Thanks
var profileHolder: Profile!
profileHolder here is force unwrapping optional value. And you are fetching from core data and assigning the value in viewWillAppear, which is risky as profileHolder would be nil and can trigger crash if you access it before viewWillAppear.
My suggestion would be:
var profileHolder: Profile
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}()
This will ensure profileHolder is either fetched or created when the view controller is initialised.
However this would not work if
context
is a stored property of viewController, in which case, do:
var profileHolder: Profile?
override func viewDidLoad()
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}
Here is the struct I created for a project I did that allows me to access my CoreData functions anywhere. Create a new empty swift file and do something like this.
import CoreData
// MARK: - CoreDataStack
struct CoreDataStack {
// MARK: Properties
private let model: NSManagedObjectModel
internal let coordinator: NSPersistentStoreCoordinator
private let modelURL: URL
internal let dbURL: URL
let context: NSManagedObjectContext
let privateContext: NSManagedObjectContext
// MARK: Initializers
init?(modelName: String) {
// Assumes the model is in the main bundle
guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
print("Unable to find \(modelName)in the main bundle")
return nil
}
self.modelURL = modelURL
// Try to create the model from the URL
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
print("unable to create a model from \(modelURL)")
return nil
}
self.model = model
// Create the store coordinator
coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
// create a context and add connect it to the coordinator
//context.persistentStoreCoordinator = coordinator
privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = coordinator
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = privateContext
// Add a SQLite store located in the documents folder
let fm = FileManager.default
guard let docUrl = fm.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Unable to reach the documents folder")
return nil
}
self.dbURL = docUrl.appendingPathComponent("model.sqlite")
// Options for migration
let options = [NSInferMappingModelAutomaticallyOption: true,NSMigratePersistentStoresAutomaticallyOption: true]
do {
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: options as [NSObject : AnyObject]?)
} catch {
print("unable to add store at \(dbURL)")
}
}
// MARK: Utils
func addStoreCoordinator(_ storeType: String, configuration: String?, storeURL: URL, options : [NSObject:AnyObject]?) throws {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Removing Data)
internal extension CoreDataStack {
func dropAllData() throws {
// delete all the objects in the db. This won't delete the files, it will
// just leave empty tables.
try coordinator.destroyPersistentStore(at: dbURL, ofType:NSSQLiteStoreType , options: nil)
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Save Data)
extension CoreDataStack {
func saveContext() throws {
/*if context.hasChanges {
try context.save()
}*/
if privateContext.hasChanges {
try privateContext.save()
}
}
func autoSave(_ delayInSeconds : Int) {
if delayInSeconds > 0 {
do {
try saveContext()
print("Autosaving")
} catch {
print("Error while autosaving")
}
let delayInNanoSeconds = UInt64(delayInSeconds) * NSEC_PER_SEC
let time = DispatchTime.now() + Double(Int64(delayInNanoSeconds)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time) {
self.autoSave(delayInSeconds)
}
}
}
}
Create a class(CoreDataManager) that can manage core data operations.
import CoreData
class CoreDataManager:NSObject{
/// Application Document directory
lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls[urls.count-1]
}()
/// Core data manager
static var shared = CoreDataManager()
/// Managed Object Model
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: “your DB name”, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
/// Persistent Store Coordinator
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
let options = [ NSInferMappingModelAutomaticallyOption : true,
NSMigratePersistentStoresAutomaticallyOption : true]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options)
persistanceStoreKeeper.sharedInstance.persistanceStorePath = url
} catch {
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
abort()
}
return coordinator
}()
/// Managed Object Context
lazy var managedObjectContext: NSManagedObjectContext = {
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
/// Save context
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
}
Add the bellow function in your class.
func fetchProfile(profileId:String,fetchlimit:Int,completion: ((_ fetchedList:["Your model class"]) -> Void)){
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Your entity name")
let predicate:NSPredicate = NSPredicate(format: "id = %#", profileId)
fetchRequest.predicate=predicate
fetchRequest.fetchLimit = fetchlimit
do {
let results =
try CoreDataManager.shared.managedObjectContext.fetch(fetchRequest)
let profileList:["Your model class"] = results as! ["Your model class"]
if(profileList.count == 0){
//Empty fetch list
}
else{
completion(profileList)
}
}
catch{
//error
}
}
replace "Your model class" according to your requirement.
You can call the function "fetchProfile" and you will get the result inside the completion block.

Trouble fetching database path

Integrated SQLITE Database, below is my code which I have written,
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
self.copyDatabaseIfNeeded()
return true
}
func copyDatabaseIfNeeded() {
let fileManager = FileManager.default
let dbPath = getDBPath()
var success: Bool = fileManager.fileExists(atPath: dbPath!)
if !success {
let defaultDBPath = URL(fileURLWithPath: Bundle.main.resourcePath ?? "").appendingPathComponent(APPLICATION_DB).absoluteString
success = ((try?fileManager.copyItem(atPath: defaultDBPath, toPath: dbPath!)) != nil)
if !success {
print("Failed to create writable database!")
}
}
}
func getDBPath() -> String? {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDir = paths[0]
return URL(fileURLWithPath: documentsDir).appendingPathComponent(APPLICATION_DB).absoluteString
}
It always print below output,
Tried clean + Build Project
Uninstall the application, reinstall it
Remove derived data
none of the above has worked.
Also, my sqlite file is there in project target -> Build Phases -> Copy Bundle Resources : below screenshot for reference.
I am not sure whether hierarchy of my file matters or not, attaching screenshot of that as well.
Can anyone help me why I am getting issue in fetching file path of my database file?
Try this.
func copyDatabaseIfNeeded() {
// Move database file from bundle to documents folder
let fileManager = FileManager.default
let documentsUrl = fileManager.urls(for: .documentDirectory,
in: .userDomainMask)
guard documentsUrl.count != 0 else {
return // Could not find documents URL
}
let finalDatabaseURL = documentsUrl.first!.appendingPathComponent("SQL.sqlite")
if !( (try? finalDatabaseURL.checkResourceIsReachable()) ?? false) {
print("DB does not exist in documents folder")
let documentsURL = Bundle.main.resourceURL?.appendingPathComponent("SQL.sqlite")
do {
try fileManager.copyItem(atPath: (documentsURL?.path)!, toPath: finalDatabaseURL.path)
} catch let error as NSError {
print("Couldn't copy file to final location! Error:\(error.description)")
}
} else {
print("Database file found at path: \(finalDatabaseURL.path)")
}
}

Converting local realms to synced realm

I want to convert my local realm database to a synced one to allow user authentication. I'm using swift and it wasn't included in the documentation, however, I found this method but it keeps giving SIGABRT exception and I don't know what seems to be the problem.
Here's what I added in the App Delegate:
import UIKit
import RealmSwift
import Realm
import Realm.Dynamic
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let sourceFilePath = Bundle.main.url(forResource: "fieldFlow", withExtension: "realm")
let configuration = RLMRealmConfiguration()
configuration.fileURL = sourceFilePath
configuration.dynamic = true
configuration.readOnly = true
let localRealm = try! RLMRealm(configuration: configuration)
let creds = SyncCredentials.usernamePassword(username: "admin#realm.io", password: "password")
SyncUser.logIn(with: creds, server: URL(string: "http://localhost:9080")!) { (syncUser, error) in
DispatchQueue.main.async {
if let syncUser = syncUser {
self.copyToSyncRealmWithRealm(realm: localRealm, user: syncUser)
}
}
}
let config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 1,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// Nothing to do!
// Realm will automatically detect new properties and removed properties
// And will update the schema on disk automatically
}
})
// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config
// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let realm = try! Realm()
// Override point for customization after application launch.
return true
}
func copyToSyncRealmWithRealm(realm: RLMRealm, user: RLMSyncUser) {
let syncConfig = RLMRealmConfiguration()
syncConfig.syncConfiguration = RLMSyncConfiguration(user: user, realmURL: URL(string: "realm://localhost:9080/~/fieldRow")!)
syncConfig.customSchema = realm.schema
let syncRealm = try! RLMRealm(configuration: syncConfig)
syncRealm.schema = syncConfig.customSchema!
try! syncRealm.transaction {
let objectSchema = syncConfig.customSchema!.objectSchema
for schema in objectSchema {
let allObjects = realm.allObjects(schema.className)
for i in 0..<allObjects.count {
let object = allObjects[i]
RLMCreateObjectInRealmWithValue(syncRealm, schema.className, object, true)
}
}
}
}

Read and write permission for user selected folder in Mac OS app?

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)")
}
}
}