How to add simple save-load functions to document-based Swift 3 app? - swift

I'm studying Swift (for just a hobby) at the moment, and trying to figure out how I'm supposed to and save and load functions to document.swift file in document-based swift app? I'd like to know how to save and load simple txt-files. I'm using NSTextView, so I guess I have to change that to NSString?
Here are those functions at the moment:
override func data(ofType typeName: String) throws -> Data {
// Insert code here to write your document to data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override fileWrapperOfType:error:, writeToURL:ofType:error:, or writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
// Insert code here to read your document from the given data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning false.
// You can also choose to override readFromFileWrapper:ofType:error: or readFromURL:ofType:error: instead.
// If you override either of these, you should also override -isEntireFileLoaded to return false if the contents are lazily loaded.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}

In you document.swift file these functions below will load and set your text views string value.
Hope this helps!
var content = ""
override func makeWindowControllers() {
//Make the window controller
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
//Access the view controller
let vc = windowController.contentViewController as! ViewController
//Set the text view string to the variable that was loaded
vc.textView.string = content
}
override func data(ofType typeName: String) throws -> Data {
//Access the current view controller
if let vc = self.windowControllers[0].contentViewController as? ViewController {
//Get the textView's String Value; or call a function that will create a string of what needs to be saved
return vc.textView.string?.data(using: String.Encoding.utf8) ?? Data()
}else {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
override func read(from url: URL, ofType typeName: String) throws {
do {
//Set a string variable to the loaded string
//We can't directly set the text views string value because this function is called before the makeWindowControllers() so now text view is created yet.
content = try String(contentsOf: url, encoding: String.Encoding.utf8)
}catch {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}

Related

How to get data from NSDocument's read method

I'm having difficulty trying to put 'data' onto NSPasteboard. By 'data', I mean a type other than the specific PasteboardType formats: text, HTML, image, etc. (It's actually MIDI data.)
override func copy() -> Any {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(data, forType: .typeMidi)
return true
}
When I try to put my data in, I get:
Cannot convert value of type '(String) throws -> Data' to expected element type 'NSPasteboardWriting'.
That's probably because I've been trying to use NSDocument's data method, but it turns out that's only for writing out to disk.
The data needs to come from the read function:
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
But it seems that the read function's arguments are not NSDocument's data function (which only relates to writing). I've no idea where the arguments come from.
Adding something like self.myData = data to the read function (in an attempt to get the data in a useful property) produce "Expected pattern" errors.
SOLVED: The problem was a schoolboy error of using copy() instead of copy(_:).
Vadian's now deleted answer was helpful in creating a custom Pasteboard type.
The complete (relevant) code is as follows:
extension NSPasteboard.PasteboardType {
static let typeMidi = NSPasteboard.PasteboardType(rawValue: "public.midi-audio")
}
class Document: NSDocument {
var theMIDIPlayer: AVMIDIPlayer?
var myData: Data?
#IBAction func copy(_: Any) {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(myData, forType: .typeMidi)
}
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
self.myData = data
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}

How to get Custom Cell to update labels to information from core data

I'm not getting any errors, just not the result I am looking for.
Here is where I gather data from the user
(If this is not relevant please let me know so I can declutter the post)
I want my custom cell to display the core data record once it's added, but it keeps displaying placeholder labels instead of updating.
class ViewController: UIViewController,UITableViewDataSource, UITableViewDelegate
{
var parties: [NSManagedObject] = []
#IBOutlet weak var tableView: UITableView!
#IBAction func addParty(_ sender: UIBarButtonItem)
{
/*init alert controller with title, message & .alert style*/
let alert = UIAlertController(title: "New Name",
message: "Add a new name",
preferredStyle: .alert)
/*create a name text field, with placeholder "name"*/
alert.addTextField(configurationHandler: { (textFieldName) in
textFieldName.placeholder = "name"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldSize) in
textFieldSize.placeholder = "size"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldContact) in
textFieldContact.placeholder = "contact"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldLocation) in
textFieldLocation.placeholder = "location"
})
/*create a save action*/
let saveAction = UIAlertAction(title: "Save", style: .default) { [unowned self] action in
/*find textfield's text (name) guard let way to get unwrap value otherwise return early*/
guard let textField = alert.textFields?.first,
let nameToSave = textField.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldSize = alert.textFields?[1],
let sizeToSave = textFieldSize.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldContact = alert.textFields?[2],
let contactToSave = textFieldContact.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldLocation = alert.textFields?[3],
let locationToSave = textFieldLocation.text else {
return
}
/*call save method by passing nameToSave and SSNToSave*/
self.save(name: nameToSave, size: sizeToSave, contact: contactToSave, location: locationToSave)
self.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true)
}
// Save core data function
func save(name: String, size : String, contact: String, location: String)
{
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
/*1.
Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
Think of saving a new managed object to Core Data as a two-step process: first, you insert a new managed object into a managed object context; then, after you’re happy with your shiny new managed object, you “commit” the changes in your managed object context to save it to disk.
Xcode has already generated a managed object context as part of the new project’s template. Remember, this only happens if you check the Use Core Data checkbox at the beginning. This default managed object context lives as a property of the NSPersistentContainer in the application delegate. To access it, you first get a reference to the app delegate.
*/
let managedContext = appDelegate.persistentContainer.viewContext
/*
An NSEntityDescription object is associated with a specific class instance
Class
NSEntityDescription
A description of an entity in Core Data.
Retrieving an Entity with a Given Name here person
*/
let entity = NSEntityDescription.entity(forEntityName: "Party",
in: managedContext)!
/*
Initializes a managed object and inserts it into the specified managed object context.
init(entity: NSEntityDescription,
insertInto context: NSManagedObjectContext?)
*/
let party = NSManagedObject(entity: entity,
insertInto: managedContext)
/*
With an NSManagedObject in hand, you set the name attribute using key-value coding. You must spell the KVC key (name in this case) exactly as it appears in your Data Model
*/
party.setValue(name, forKeyPath: "name")
party.setValue(size, forKeyPath: "size")
party.setValue(contact, forKeyPath: "contact")
party.setValue(location, forKeyPath: "location")
/*
You commit your changes to person and save to disk by calling save on the managed object context. Note save can throw an error, which is why you call it using the try keyword within a do-catch block. Finally, insert the new managed object into the people array so it shows up when the table view reloads.
*/
do {
try managedContext.save()
parties.append(party)
tableView.reloadData()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
// TABLE VIEW CODE
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return parties.count
}
//NEED TO FIX WHY CUSTOM CELL NOT DISPLAYING INFO
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
(print(tableView.dequeueReusableCell(withIdentifier: "PartyCell", for: indexPath)))
let party = parties[indexPath.row] as NSManagedObject
let cell = tableView.dequeueReusableCell(withIdentifier: "PartyCell",
for: indexPath) as! PartyCell
cell.nameLabel?.text = party.value(forKeyPath: "name") as? String
cell.sizeLabel.text = party.value(forKeyPath: "size") as? String
cell.contactLabel.text = party.value(forKeyPath: "contact") as? String
cell.locationLabel.text = party.value(forKeyPath: "location") as? String
return cell
}
override func viewDidLoad()
{
super.viewDidLoad()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Singleton not accessible in Sirikit IntentHandler

I have a project that I want to add sirikit to. I added the intent and wanted to store values in my datastorage which is realm, when I tried to access the function that is used to create this task , I get an eeror. this is my code below
extension IntentHandler : INCreateTaskListIntentHandling {
public func handle(intent: INCreateTaskListIntent,
completion: #escaping (INCreateTaskListIntentResponse) -> Swift.Void) {
guard let title = intent.title else {
completion(INCreateTaskListIntentResponse(code: .failure, userActivity: nil))
return
}
CategoryFunctions.instance.createList(name: title.spokenPhrase,.....)
var tasks: [INTask] = []
if let taskTitles = intent.taskTitles {
let taskTitlesStrings = taskTitles.map {
taskTitle -> String in
return taskTitle.spokenPhrase
}
tasks = createTasks(fromTitles: taskTitlesStrings)
CategoryFunctions.instance.add(tasks: taskTitlesStrings, toList: title.spokenPhrase)
}
let response = INCreateTaskListIntentResponse(code: .success, userActivity: nil)
response.createdTaskList = INTaskList(title: title,
tasks: tasks,
groupName: nil,
createdDateComponents: nil,
modifiedDateComponents: nil,
identifier: nil)
completion(response)
}
}
this singlton instantiation works well in my app but I do not know why I get an error saying Use of unresolved identifier 'CategoryFunctions'
my CategoryFunctions singleton
class CategoryFunctions {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
var database:Realm!
static let instance = CategoryFunctions()
.....
...
Select your file in xcode, on the right, choose the File Inspector, then under Target Membership, pick your Intent.

Cocoa: How to re-read file content after file has been changed from another app using NSDocument?

I have a document based cocoa app that opens an .md file to display the markdown content in a nice format. If I change the .md file in another app like textedit, I want to reload the views in my app.
Here's what I have working so far:
import Cocoa
class Document: NSDocument {
var fileContent = "Nothing yet :("
override init() {
// Add your subclass-specific initialization here.
super.init()
}
override class var autosavesInPlace: Bool {
return false
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
fileContent = (try String(data: data, encoding: .utf8))!
}
// this fn is called every time textEdit changes the file content.
override func presentedItemDidChange() {
// Here is the PROBLEM:
// HOW do I access the new file content?
}
}
Here is the problem
presentedItemDidChange() is called every time textEdit makes a change. That works great. But I can't for the life of me figure out how then to access the new file content, so I can reassign fileContent = newContent. Any thoughts?
I would call for the document readFromURL:ofType:error: as described here.

UIImage returns nil on segue push

I have an image URL that needs to be parsed and displayed. The URL exists, but returns nil.
It successfully parses in the cellForRowAt function by calling cell.recipeImage.downloadImage(from: (self.tableViewDataSource[indexPath.item].image))
With this line the image displays. However, it doesn't exist when calling it in didSelectRowAt
RecipeTableViewController.swift
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let resultsVC = Storyboard.instantiateViewController(withIdentifier: "ResultsViewController") as! ResultsViewController
// Information to be passed to ResultsViewController
if (tableViewDataSource[indexPath.item] as? Recipe) != nil {
if isSearching {
resultsVC.getTitle = filteredData[indexPath.row].title
//resultsVC.imageDisplay.downloadImage(from: (self.filteredData[indexPath.row].image))
} else {
resultsVC.getTitle = tableViewDataSource[indexPath.row].title
// Parse images
resultsVC.imageDisplay.downloadImage(from: (self.tableViewDataSource[indexPath.row].image))
}
}
// Push to next view
self.navigationController?.pushViewController(resultsVC, animated: true)
}
extension UIImageView {
func downloadImage(from url: String) {
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.sync {
self.image = UIImage(data: data!)
}
}
task.resume()
}
}
ResultsViewController.swift
class ResultsViewController: UIViewController {
var getTitle = String()
var getImage = String()
#IBOutlet weak var recipeDisplay: UILabel!
#IBOutlet weak var imageDisplay: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
recipeDisplay.text! = getTitle
}
...
}
Returns the error
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
From my understanding, the app is getting crashed at this line:
recipeDisplay.text! = getTitle
If it is, obviously this is not the proper way to do it. Just remove the force unwrapping because the text on the label here is nil by default. Force referencing a nil value will crash the app.
recipeDisplay.text = getTitle
UPDATED:
- Let's make sure that you wired the label and the outlets properly. Connect ti to the VC, not the File Owner.
You're calling view-related code on views that haven't been initialized yet. Remember, IBOutlets are implicitly unwrapped properties, so if you try to access them before they're initialized they'll force-unwrap and crash. So it's not that the UIImage is coming up nil, it's that recipeDisplay is nil and is getting force unwrapped.
The idiomatic iOS thing to do is to hand a view model of some sort (an object or a struct) to the view controller, and then let it do the work with that item once it has finished loading.
So, in you didSelect method, you could create your view model (which you'd need to define) and hand it off like this:
let title = filteredData[indexPath.row].title
let imageURL = self.tableViewDataSource[indexPath.row].image
let viewModel = ViewModel(title: title, imageURL: imageURL)
resultsVC.viewModel = viewModel
And then in your resultsVC, you'd do something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let vm = viewModel {
recipeDisplay.text = vm.title
downloadImage(from: vm.imageURL)
}
}
So in your case all you'd need to do is hand those strings to your VC (you can wrap them up in a view model or hand them off individually) and then in that VC's viewDidLoad() that's where you'd call downloadImage(from:). That way there's no danger of calling a subview before that subview has been loaded.
One last note: Your download method should be a little safer with its use of the data and error variables, and its references to self. Remember, avoid using ! whenever you don't absolutely have to use it (use optional chaining instead), and unless you have a really good reason to do otherwise, always use [weak self] in closures.
I'd recommend doing it like this:
func downloadImage(from url: String) {
let urlRequest = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] (data,response,error) in
if let error = error {
print(error)
return
}
if let data = data {
DispatchQueue.main.sync {
self?.image = UIImage(data: data)
}
}
}
task.resume()
}
Update: Because the 'view model' concept was a little too much at once, let me explain.
A view model is just an object or struct that represents the presentation data a screen needs to be in a displayable state. It's not the name of a type defined by Apple and isn't defined anywhere in the iOS SDK. It's something you'd need to define yourself. So, in this case, I'd recommend defining it in the same fine where you're going to use it, namely in the same file as ResultsViewController.
You'd do something like this:
struct ResultsViewModel {
let title: String
let imageURL: String
}
and then on the ResultsViewController, you'd create a property like:
var viewModel: ResultsViewModel?
or if you don't like dealing with optionals, you can do:
var viewModel = ResultsViewModel(title: "", imageURL: "")
OR, you can do what you're already doing, but I'd highly recommend renaming those properties. getTitle sounds like it's doing something more besides just holding onto a value. title would be a better name. Same criticism goes for getImage, with the additional criticism that it's also misleading because it sounds like it's storing an image, but it's not. It's storing an image url. imageURL is a better name.