How to save security scoped URL for later use macOS - macos

I've made a Finder extension to add a menu to Finder's Context menu for any file. I'd like to access this file when the user selects this custom menu, obviously this file they select could be anywhere in the file system and outside the allowed sandbox areas.
func accessFile(url: URL, userID: String, completion: #escaping ([String:Any]?, Error?) -> Void){
var bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
print("Testing if we have access to file")
// 1. Test if I have access to a file
let directoryURL = url.deletingLastPathComponent()
let data = bookmarks?[directoryURL]
if data == nil{
print("have not asked for access yet or directory is not saved")
// 2. If I do not, open a open dialog, and get permission
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = false
openPanel.prompt = "Grant Access"
openPanel.directoryURL = directoryURL
openPanel.begin { result in
guard result == .OK, let url = openPanel.url else {return}
// 3. obtain bookmark data of folder URL and store it to keyed archive
do{
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
}catch{
print(error)
}
bookmarks?[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// 4. start using the fileURL via:
url.startAccessingSecurityScopedResource()
// < do whatever to file >
url.stopAccessingSecurityScopedResource()
}
}else{
// We have accessed this directory before, get data from bookmarks
print("we have access already")
let directoryURL = url.deletingLastPathComponent()
guard let data = bookmarks?[directoryURL]! else { return }
var isStale = false
let newURL = try? URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
// 3. Now again I start using file URL and upload:
newURL?.startAccessingSecurityScopedResource()
// < do whatever to file >
newURL?.stopAccessingSecurityScopedResource()
}
}
Currently it always asks for permission, so the bookmark is not getting saved

I'm not 100% sure if this is the source of your problem, but I don't see where you are using the isStale value. If it it comes back true from URL(resolvingBookmarkData:...), you have to remake/resave the bookmark. So in your else block you need some code like this:
var isStale = false
let newURL = try? URL(
resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
if let url = newURL, isStale
{
do
{
data = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
}
catch { fatalError("Remaking bookmark failed") }
// Resave the bookmark
bookmarks?[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
}
newURL?.startAccessingSecurityScopedResource()
// < do whatever to file >
newURL?.stopAccessingSecurityScopedResource()
data will, of course, need to be var instead of let now.
Also remember that stopAccessingSecurityScopedResource() has to be called on main thread, so if you're not sure accessFile is being called on the main thread, you might want to do that explicitly:
DispatchQueue.main.async {
newURL?.stopAccessingSecurityScopedResource()
}
You'd want to do that in both places you call it.
I like to write an extension on URL to make it a little nicer:
extension URL
{
func withSecurityScopedAccess<R>(code: (URL) throws -> R) rethrows -> R
{
self.startAccessingSecurityScopedResource()
defer {
DispatchQueue.main.async {
self.stopAccessingSecurityScopedResource()
}
}
return try code(self)
}
}
So then I can write:
url.withSecurityScopedAccess { url in
// Do whatever with url
}
Whether you use the extension or not, explicitly calling stopAccessingSecurityScopedResource() on DispatchQueue.main does mean that access won't be stopped until the next main run loop iteration. That's normally not a problem, but if you start and stop the access for the same URL multiple times in a single run loop iteration, it might not work, because it will call startAccessingSecurityScopedResource() multiple time without stopAccessingSecurityScopedResource() in between, and the on the next iteration it would call stopAccessingSecurityScopedResource() multiple times as the queued tasks are executed. I have no idea if URL maintains a security access count that would allow that to be safe, or just a flag, in which case it wouldn't be.

Let's make some issues visible by removing the bookmark and NSOPenPanel code:
var bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
// bookmarks is an optional and can be nil (when the file doesn't exist)
let data = bookmarks?[directoryURL]
if data == nil {
// NSOpenPanel
do {
let data = try openPanelUrl.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
// this data is a local data, the other data didn't change
} catch {
print(error)
}
// bookmarks and data are still nil
bookmarks?[openPanelUrl] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// use url
} else {
// get the bookmark data again
guard let data = bookmarks?[directoryURL]! else { return }
// get url from data and use it
}
I would do something like:
var bookmarks: [URL: Data]
if let savedBookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data] {
bookmarks = savedBookmarks
}
else {
bookmarks = [:]
}
// bookmarks is a dictionary and can be saved
if let data = bookmarks[directoryURL] {
// get url from data and use it
}
else {
// NSOpenPanel
do {
if let newData = try openPanelUrl.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) {
// bookmarks and newData are not nil
bookmarks[openPanelUrl] = newData
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// use url
}
} catch {
print(error)
}
}

Related

Sandbox & WKWebView loadFileURL(_, allowingReadAccessTo:) Inconsistency

Using the new shiny WKWebView and sandbox on os/x, require some intervening reset or clear as subsequent calls to load a file URL will be ignored; this is somewhat related to an earlier question on WKWebView loadFileURL works only once -
ios there, here on os/X I do
if loadURL.isFileURL {
webView.loadFileURL(loadURL, allowingReadAccessTo: loadURL)
}
else
{
webView.load(URLRequest(url: loadURL))
}
I've tried to pass loadURL.deletingLastPathComponent() as the second arg but then all breaks - no file URLs get loaded, nor does using the user's home path, or the entire root 'file:///', nor the 'temporary' exception re: absolute file paths. Finally, trying an intervening topLoading() has no affect.
The only solution (yuck) to get a subsequent file URL loaded is to first load a non file URL!
It seems within a sandbox environment this has unintended consequences?
Well, this works but ugly - webView subclass function, as you cannot reuse a webView when a file url was previously loaded. This workaround will instantiate a new window/doc tossing the old - unless as a user preference they want to keep the old window (newWindows flag is true):
func loadNext(url: URL) {
let doc = self.window?.windowController?.document as! Document
let newWindows = UserSettings.createNewWindows.value
var fileURL = url
if !url.isFileURL {
if newWindows {
do
{
let next = try NSDocumentController.shared().openUntitledDocumentAndDisplay(true) as! Document
let oldWindow = self.window
let newWindow = next.windowControllers.first?.window
(newWindow?.contentView?.subviews.first as! MyWebView).load(URLRequest(url: url))
newWindow?.offsetFromWindow(oldWindow!)
}
catch let error {
NSApp.presentError(error)
Swift.print("Yoink, unable to create new url doc for (\(url))")
return
}
}
else
{
self.load(URLRequest(url: url))
}
}
if let origURL = (fileURL as NSURL).resolvedFinderAlias() {
fileURL = origURL
}
if appDelegate.isSandboxed() && !appDelegate.storeBookmark(url: fileURL) {
Swift.print("Yoink, unable to sandbox \(fileURL))")
return
}
if !(self.url?.isFileURL)! && !newWindows {
self.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
doc.update(to: fileURL, ofType: fileURL.pathExtension)
return
}
// We need or want a new window; if need, remove the old afterward
do {
let next = try NSDocumentController.shared().openUntitledDocumentAndDisplay(true) as! Document
let oldWindow = doc.windowControllers.first?.window
let newWindow = next.windowControllers.first?.window
(newWindow?.contentView?.subviews.first as! MyWebView).loadFileURL(fileURL, allowingReadAccessTo: fileURL)
if newWindows {
newWindow?.offsetFromWindow(oldWindow!)
}
else
{
newWindow?.overlayWindow(oldWindow!)
oldWindow?.orderOut(self)
}
next.update(to: fileURL, ofType: fileURL.pathExtension)
}
catch let error
{
NSApp.presentError(error)
Swift.print("Yoink, unable to new doc (\(fileURL))")
}
}

Append swift 2.0 and xcode 7 [duplicate]

I already have read Read and write data from text file
I need to append the data (a string) to the end of my text file.
One obvious way to do it is to read the file from disk and append the string to the end of it and write it back, but it is not efficient, especially if you are dealing with large files and doing in often.
So the question is "How to append string to the end of a text file, without reading the file and writing the whole thing back"?
so far I have:
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
var err:NSError?
// until we find a way to append stuff to files
if let current_content_of_file = NSString(contentsOfURL: fileurl, encoding: NSUTF8StringEncoding, error: &err) {
"\(current_content_of_file)\n\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}else {
"\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}
if err != nil{
println("CANNOT LOG: \(err)")
}
Here's an update for PointZeroTwo's answer in Swift 3.0, with one quick note - in the playground testing using a simple filepath works, but in my actual app I needed to build the URL using .documentDirectory (or which ever directory you chose to use for reading and writing - make sure it's consistent throughout your app):
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self + "\n").appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
//test
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("logFile.txt")
try "Test \(Date())".appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
Thanks PointZeroTwo.
You should use NSFileHandle, it can seek to the end of the file
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
let string = "\(NSDate())\n"
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
if NSFileManager.defaultManager().fileExistsAtPath(fileurl.path!) {
var err:NSError?
if let fileHandle = NSFileHandle(forWritingToURL: fileurl, error: &err) {
fileHandle.seekToEndOfFile()
fileHandle.writeData(data)
fileHandle.closeFile()
}
else {
println("Can't open fileHandle \(err)")
}
}
else {
var err:NSError?
if !data.writeToURL(fileurl, options: .DataWritingAtomic, error: &err) {
println("Can't write \(err)")
}
}
A variation over some of the posted answers, with following characteristics:
based on Swift 5
accessible as a static function
appends new entries to the end of the file, if it exists
creates the file, if it doesn't exist
no cast to NS objects (more Swiftly)
fails silently if the text cannot be encoded or the path does not exist
class Logger {
static var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
return documentsDirectory.appendingPathComponent(fileName)
}
static func log(_ message: String) {
guard let logFile = logFile else {
return
}
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + ": " + message + "\n").data(using: String.Encoding.utf8) else { return }
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
}
}
}
Here is a way to update a file in a much more efficient way.
let monkeyLine = "\nAdding a šŸµ to the end of the file via FileHandle"
if let fileUpdater = try? FileHandle(forUpdating: newFileUrl) {
// Function which when called will cause all updates to start from end of the file
fileUpdater.seekToEndOfFile()
// Which lets the caller move editing to any position within the file by supplying an offset
fileUpdater.write(monkeyLine.data(using: .utf8)!)
// Once we convert our new content to data and write it, we close the file and thatā€™s it!
fileUpdater.closeFile()
}
Here's a version for Swift 2, using extension methods on String and NSData.
//: Playground - noun: a place where people can play
import UIKit
extension String {
func appendLineToURL(fileURL: NSURL) throws {
try self.stringByAppendingString("\n").appendToURL(fileURL)
}
func appendToURL(fileURL: NSURL) throws {
let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
try data.appendToURL(fileURL)
}
}
extension NSData {
func appendToURL(fileURL: NSURL) throws {
if let fileHandle = try? NSFileHandle(forWritingToURL: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(self)
}
else {
try writeToURL(fileURL, options: .DataWritingAtomic)
}
}
}
// Test
do {
let url = NSURL(fileURLWithPath: "test.log")
try "Test \(NSDate())".appendLineToURL(url)
let result = try String(contentsOfURL: url)
}
catch {
print("Could not write to file")
}
In order to stay in the spirit of #PointZero Two.
Here an update of his code for Swift 4.1
extension String {
func appendLine(to url: URL) throws {
try self.appending("\n").append(to: url)
}
func append(to url: URL) throws {
let data = self.data(using: String.Encoding.utf8)
try data?.append(to: url)
}
}
extension Data {
func append(to url: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: url) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
} else {
try write(to: url)
}
}
}
Update: I wrote a blog post on this, which you can find here!
Keeping things Swifty, here is an example using a FileWriter protocol with default implementation (Swift 4.1 at the time of this writing):
To use this, have your entity (class, struct, enum) conform to this protocol and call the write function (fyi, it throws!).
Writes to the document directory.
Will append to the text file if the file exists.
Will create a new file if the text file doesn't exist.
Note: this is only for text. You could do something similar to write/append Data.
import Foundation
enum FileWriteError: Error {
case directoryDoesntExist
case convertToDataIssue
}
protocol FileWriter {
var fileName: String { get }
func write(_ text: String) throws
}
extension FileWriter {
var fileName: String { return "File.txt" }
func write(_ text: String) throws {
guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileWriteError.directoryDoesntExist
}
let encoding = String.Encoding.utf8
guard let data = text.data(using: encoding) else {
throw FileWriteError.convertToDataIssue
}
let fileUrl = dir.appendingPathComponent(fileName)
if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try text.write(to: fileUrl, atomically: false, encoding: encoding)
}
}
}
All answers (as of now) recreate the FileHandle for every write operation. This may be fine for most applications, but this is also rather inefficient: A syscall is made, and the filesystem is accessed each time you create the FileHandle.
To avoid creating the filehandle multiple times, use something like:
final class FileHandleBuffer {
let fileHandle: FileHandle
let size: Int
private var buffer: Data
init(fileHandle: FileHandle, size: Int = 1024 * 1024) {
self.fileHandle = fileHandle
self.size = size
self.buffer = Data(capacity: size)
}
deinit { try! flush() }
func flush() throws {
try fileHandle.write(contentsOf: buffer)
buffer = Data(capacity: size)
}
func write(_ data: Data) throws {
buffer.append(data)
if buffer.count > size {
try flush()
}
}
}
// USAGE
// Create the file if it does not yet exist
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
let fileHandle = try FileHandle(forWritingTo: fileURL)
// Seek will make sure to not overwrite the existing content
// Skip the seek to overwrite the file
try fileHandle.seekToEnd()
let buffer = FileHandleBuffer(fileHandle: fileHandle)
for i in 0..<count {
let data = getData() // Your implementation
try buffer.write(data)
print(i)
}

This application is modifying the autolayout engine from a background thread, which can lead to engine corruption

I am getting this error This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes.This will cause an exception in a future release. I don't know what is causing this error. Can anybody help me.
func getUserDataFromTwitterWithUser(user : PFUser)
{
//NRLoader.showLoader()
let strTwURL = "https://api.twitter.com/1.1/users/show.json? screen_name="+PFTwitterUtils.twitter()!.screenName! + "&access_token="+PFTwitterUtils.twitter()!.authToken!
let twURL = NSURL (string: strTwURL)
let request = NSMutableURLRequest(URL: twURL!, cachePolicy: NSURLRequestCachePolicy.UseProtocolCachePolicy, timeoutInterval: 2.0) as NSMutableURLRequest
PFTwitterUtils.twitter()?.signRequest(request)
let session = NSURLSession.sharedSession()
session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if error == nil {
var jsonOptional = Dictionary<String, AnyObject>()
do {
jsonOptional = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! Dictionary<String, AnyObject>
// use jsonData
} catch {
// report error
}
var userName = ""
if let screenName = jsonOptional["screen_name"] as? String{
userName = screenName
}
else if let name = jsonOptional["name"] as? String{
userName = name
}
var profilePicUrl = ""
if let picUrl = jsonOptional["profile_image_url"] as? String{
profilePicUrl = picUrl
}
AppUser.currentUser()?.username = userName
AppUser.currentUser()?.profileAwsURL = profilePicUrl
//NRLoader.hideLoader()
//if ParseUtils.isLoggedInUserIsAnonymous() {
let signUpVC:SignMeUpViewController = self.storyboard!.instantiateViewControllerWithIdentifier("SignMeUpViewController") as! SignMeUpViewController
signUpVC.isFromLogin = true
self.navigationController!.pushViewController(signUpVC, animated: true)
//} else {
// self.pushToSubmitDreamViewController()
//}
}
else {
//NRLoader.hideLoader()
NRToast.showToastWithMessage(error!.description)
}
}).resume()
}
The dataTaskWithRequest call runs in the background and then calls your completion handler from the same thread. Anything that updates the UI should run on the main thread, so all of your current handler code should be within a dispatch_async back onto the main queue:
dispatch_async(dispatch_get_main_queue()) {
// Do stuff to UI
}
Swift 3:
DispatchQueue.main.async() {
// Do stuff to UI
}
Therefore, ideally all the code you currently have within if error == nil should be off in another function, say called handleRequest, so your current code becomes:
session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if error == nil {
dispatch_async(dispatch_get_main_queue(), {
self.handleRequest(...)I
})
}
Swift 3
session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if error == nil {
DispatchQueue.main.async {
self.handleRequest(...)I
}
}
Should try Symbolic Breakpoint to detect the issue:-
Then put your UI Update code in main thread
DispatchQueue.main.async {}
You'd better change UI only in the main thread
swift3,
let liveInfoUrl = URL(string: "http://192.168.1.66/api/cloud/app/liveInfo/7777")
let task = URLSession.shared.dataTask(with: liveInfoUrl! as URL) {data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async {
print(String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) ?? "aaaa")
//do some ui work
}
}
if the above suggestions still give you no joy then the sure-est way is to redesign your functions so that getting what you need with
URLSession.shared.dataTask
then hands over so a variable declared outside that function, then a separate UIControl ( button, swipe etc ) displays it to a label or textview or whatever.
After all that is what the error message is telling you. they're separate concerns

Core Data Error: "Can't find model for source store"

I just deleted an attribute of my entity inside CoreData and after that this error appears:
2016-02-02 21:37:54.499 toRep_Management[32110:4679391] CoreData: error: -addPersistentStoreWithType:XML configuration:(null) URL:file:///Users/Tom/Library/Application%20Support/de.toEducate.toRep_Management/CocoaAppCD.storedata options:{
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
} ... returned error Error Domain=NSCocoaErrorDomain Code=134130 "Persistent store migration failed, missing source managed object model." UserInfo={URL=file:///Users/Tom/Library/Application%20Support/de.toEducate.toRep_Management/CocoaAppCD.storedata, metadata={type = immutable dict, count = 6,
entries =>
0 : {contents = "NSStoreUUID"} = {contents = "A3FD787E-495C-493D-A3B9-2E7F7925EF7C"}
3 : {contents = "NSStoreModelVersionIdentifiers"} = (
""
)
4 : {contents = "NSStoreType"} = {contents = "XML"}
10 : {contents = "NSPersistenceFrameworkVersion"} = {value = +641, type = kCFNumberSInt64Type}
11 : {contents = "NSStoreModelVersionHashes"} = {type = mutable dict, count = 1,
entries =>
0 : Notice = {length = 32, capacity = 32, bytes = 0xd22eaaf9bb53e406e8914544584e0e72 ... 0899bacb36b735bd}
}
12 : {contents = "NSStoreModelVersionHashesVersion"} = {value = +3, type = kCFNumberSInt64Type}
}
, reason=Can't find model for source store} with userInfo dictionary {
URL = "file:///Users/Tom/Library/Application%20Support/de.toEducate.toRep_Management/CocoaAppCD.storedata";
metadata = {
NSPersistenceFrameworkVersion = 641;
NSStoreModelVersionHashes = {
Notice = ;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
""
);
NSStoreType = XML;
NSStoreUUID = "A3FD787E-495C-493D-A3B9-2E7F7925EF7C";
};
reason = "Can't find model for source store";
}
2016-02-02 21:37:54.499 toRep_Management[32110:4679391] CoreData: annotation: NSPersistentStoreCoordinator's current model hashes are {
Notice = <2465d170 cdc5f276 8e769b4d 25c03a0d 8efb193b 6707ab4d 46419c42 17733df1>;
}
I thought I implemented a lightweight migration correctly but now IĀ“m not sure anymore.
My AppDelegate:
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
// MARK: - Core Data stack
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "de.toEducate.toRep_Management" in the user's Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)
let appSupportURL = urls[urls.count - 1]
return appSupportURL.URLByAppendingPathComponent("de.toEducate.toRep_Management")
}()
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("toRep_Management", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
let fileManager = NSFileManager.defaultManager()
var failError: NSError? = nil
var shouldFail = false
var failureReason = "There was an error creating or loading the application's saved data."
// Make sure the application files directory is there
do {
let properties = try self.applicationDocumentsDirectory.resourceValuesForKeys([NSURLIsDirectoryKey])
if !properties[NSURLIsDirectoryKey]!.boolValue {
failureReason = "Expected a folder to store application data, found a file \(self.applicationDocumentsDirectory.path)."
shouldFail = true
}
} catch {
let nserror = error as NSError
if nserror.code == NSFileReadNoSuchFileError {
do {
try fileManager.createDirectoryAtPath(self.applicationDocumentsDirectory.path!, withIntermediateDirectories: true, attributes: nil)
} catch {
failError = nserror
}
} else {
failError = nserror
}
}
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = nil
let migrateOptions = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
if failError == nil {
coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("CocoaAppCD.storedata")
do {
try coordinator!.addPersistentStoreWithType(NSXMLStoreType, configuration: nil, URL: url, options: migrateOptions)
} catch {
failError = error as NSError
}
}
if shouldFail || (failError != nil) {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
if failError != nil {
dict[NSUnderlyingErrorKey] = failError
}
let error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
NSApplication.sharedApplication().presentError(error)
abort()
} else {
return coordinator!
}
}()
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// MARK: - Core Data Saving and Undo support
#IBAction func saveAction(sender: AnyObject!) {
// Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
if !managedObjectContext.commitEditing() {
NSLog("\(NSStringFromClass(self.dynamicType)) unable to commit editing before saving")
}
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSApplication.sharedApplication().presentError(nserror)
}
}
}
func windowWillReturnUndoManager(window: NSWindow) -> NSUndoManager? {
// Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
return managedObjectContext.undoManager
}
func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
// Save changes in the application's managed object context before the application terminates.
if !managedObjectContext.commitEditing() {
NSLog("\(NSStringFromClass(self.dynamicType)) unable to commit editing to terminate")
return .TerminateCancel
}
if !managedObjectContext.hasChanges {
return .TerminateNow
}
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
// Customize this code block to include application-specific recovery steps.
let result = sender.presentError(nserror)
if (result) {
return .TerminateCancel
}
let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message")
let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info");
let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title")
let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title")
let alert = NSAlert()
alert.messageText = question
alert.informativeText = info
alert.addButtonWithTitle(quitButton)
alert.addButtonWithTitle(cancelButton)
let answer = alert.runModal()
if answer == NSAlertFirstButtonReturn {
return .TerminateCancel
}
}
// If we got here, it is time to quit.
return .TerminateNow
}
Could anybody help with the error and how can I handle this in future with a better migration?
"Can't find model for source store" means that you're trying to do a model migration but that Core Data can't find the old version of the data model. For migration to work, you need to have both the old version and the new version. The error suggests that you only have the new version.
When you're going to change a data model and migrate data, you need to create a new version of the model in your project. You'd do this by selecting the model file in Xcode and using "Editor" menu --> "Add Model Version". Then you'd make your changes to the new version while leaving the old one in place.
You need to restore the old model. The best way to do that is to just look back through your SCM (git, probably) commits and get it.

Continuously update a UILabel in Swift

I have a function, shown below, that I would like to continuously update. It is taking data from a webpage, and every so often that webpage is updated to reflect current information. Is there a way that I can catch this update and reflect that in my application? I'm pretty new to Swift and iOS programming. Some of the code made seem very bizarre, but it currently works for whatever song is playing when you first open the app (that is, it updates the text to show that song playing but doesn't update later).
let url = NSURL(string: "http://api.vicradio.org/songs/current")!
let request = NSMutableURLRequest(URL: url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) in
if error != nil {
return
}
let name = NSString(data: data!, encoding: NSUTF8StringEncoding) as! String
var songName = ""
var artistName = "by "
var quoteNumber = 0
for character in name.characters {
if character == "\"" {
quoteNumber++
}
if quoteNumber == 3 && character != "\"" {
songName += String(character)
} else if quoteNumber == 7 && character != "\"" {
artistName += String(character)
}
}
if (songName != "no song metadata provided") {
self.SongNowText.text = songName
self.ArtistNowText.text = artistName
self.SongNowText.setNeedsDisplay()
self.ArtistNowText.setNeedsDisplay()
} else if (songName == "no song metadata provided") {
self.SongNowText.text = "The Best of What's Next!"
self.ArtistNowText.text = "only on VIC Radio"
}
}
task!.resume()
It looks like the URL you're accessing there is an API endpoint putting out JSON. I highly recommend using NSJSONSerialization.JSONObjectWithData to parse the response body into a dictionary and use that instead of rolling your own solution by counting quote marks.
The callback to dataTaskWithURL is executed on a background thread. Avoid updating the UI on anything besides the main thread because it can cause problems. Use dispatch_async to execute your UI update function on the main thread as in the example.
All you can do with this API is send it requests and read the responses. You can poll the endpoint at a regular interval while the app is open and get decent results from that. NSTimer is one way to do that, and it requires you put the method you want to execute repeatedly in a class inheriting from NSObject because it depends on Objective-C style message sending.
Throw this in a playground and try it:
import Cocoa
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely()
class RadioDataAccessor : NSObject {
private let callback: [String : AnyObject] -> Void
init(callback: [String : AnyObject] -> Void) {
self.callback = callback
super.init()
NSTimer.scheduledTimerWithTimeInterval(5.0, target: self,
selector: "updateData", userInfo: nil, repeats: true)
// just so it happens quickly the first time
updateData()
}
func updateData() {
let session = NSURLSession.sharedSession()
let url = NSURL(string: "http://api.vicradio.org/songs/current")!
session.dataTaskWithURL(url) { data, response, error in
if error != nil {
return
}
var jsonError = NSErrorPointer()
let json = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.allZeros,
error: jsonError) as? [String : AnyObject]
if jsonError != nil {
return
}
dispatch_async(dispatch_get_main_queue()) { self.callback(json!) }
}.resume()
}
}
RadioDataAccessor() { data in
println(data)
}
You may want to save the timer to a variable and expose a function that lets you invalidate it.

Resources