CollectionView - Drop pdf file onto collectionView (loadObject(ofClass:) not working properly) - ios

I have a problem loading a pdf file from a drop session (using UICollectionView's Drag&Drop feature).
Inside collectionView(_:performDropWith:coordinator), I want to load the dropped items on a background thread:
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
// I'm dropping a valid pdf file from the Files app in iOS.
// I'm using performBackgroundTask because I want to save stuff to the database
appDelegate.persistentContainer.performBackgroundTask({ (privateContext) in
for item in coordinator.items {
// This correctly returns true
if item.dragItem.itemProvider.canLoadObject(ofClass: MyPDFDocument.self) {
item.dragItem.itemProvider.loadObject(ofClass: MyPDFDocument.self) { (pdfItem, error) in
// This is not called
}
}
}
})
}
final class MyPDFDocument: PDFDocument, NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypePDF as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ComicBookPDFDocument {
return MyPDFDocument(data: data)!
}
}
However, it is not working. The block loadObject(ofClass:) is supposed to be calling, is simply not called. It is working perfectly fine on the main thread.
The thing is, I cannot to put the performBackgroundTask block inside loadObject(ofClass:) (then the dropped objects load perfectly), because if you drop multiple pdf files, this causes merge errors when saving the context (because the background tasks run simultaneously for every dropped file).
Any ideas what's going on here? Is it not allowed to load objects from within another thread?

It's been a long time since I looked at the code, but I hope this helps:
The solution for me was to use the method loadObjects(ofClass:completion:). This grabs all objects of the desired files asynchronously. This is not stated in the documentation. However, if you cmd-click the method and go to its declaration, there's this comment:
/* A convenience method that can be used only during the UIDropInteractionDelegate's
* implementation of `-dropInteraction:performDrop:`.
* Asynchronously instantiates objects of the provided class for each
* drag item that can do so. The completion handler is called on the
* main queue, with an array of all objects that were created, in the
* same order as `items`.
* The progress returned is an aggregate of the progress for all objects
* that are loaded.
*/
func loadObjects(ofClass aClass: NSItemProviderReading.Type, completion: #escaping ([NSItemProviderReading]) -> Void) -> Progress
After instantiating the objects, the completion handler is called on the main queue. From here, you can dispatch to a background queue and do whatever you want with the files.
I grabbed the code I ended up with and simplified it:
// ValidComicBookFile is my NSItemProviderReading type that accepts multiple file types, like archive and pdf
coordinator.session.loadObjects(ofClass: ValidComicBookFile.self) { (importedObjects) in
guard let validComicBookFiles = importedObjects as? [ValidComicBookFile] else {
// Handle errors
}
DispatchQueue.global(qos: .userInitiated).async {
// ...
for (index, file) in validComicBookFiles.enumerated() {
switch file.fileType {
case .archive:
// Unpack it and stuff
case .pdf:
// For example:
if let pdfDoc = PDFDocument(data: file.data) {
pdfs.append(pdfDoc) // Append it to an array I created earlier
}
case .none:
break
}
}
// ...
}
}
One note: I didn't end up needing to save to the database, but this way, it shouldn't be a problem (hopefully).

Related

How to store and load data properly with CoreData?

I am a beginner and never worked close with CoreData. I have a JSON response, which results need to be shown in a Table View. I want implement a CoreData to my project.
JSON parsing (in Separate Swift file)
func parseJSON(with currencyData: Data){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(CurrencyData.self, from: currencyData)
for valute in decodedData.Valute.values {
if valute.CharCode != "XDR" {
let currency = Currency(context: self.context)
currency.shortName = valute.CharCode
currency.currentValue = valute.Value
}
}
do {
try context.save()
} catch {
print("Error saving context, \(error)")
}
} catch {
self.delegate?.didFailWithError(self, error: error)
return
}
}
And in my VC I want to load it in my tableView which takes data from currencyArray:
func loadCurrency() {
let request: NSFetchRequest<Currency> = Currency.fetchRequest()
do {
currencyArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}
I start the parsing and load currency data in my VC:
override func viewDidLoad() {
super.viewDidLoad()
currencyNetworking.performRequest()
loadCurrency()
}
But as a result when app first launch my tableView is empty. Second launch - I have doubled data. Maybe this is because loadCurrency() starts before performRequest() able to receive data from JSON and save it to context.
I tried also to not save context in parseJSON() but first through delegate method send it to VC and perform save to context and load from context from here. Then tableView loads from the first launch. But then every time app starts in my CoreData database I see an increase of the same data (33, 66, 99 lines).
My goal is to save parsed data to CoreData once (either where I parseJSON or in VC) and change only currentValue attribute when user want to update it.
How to make it correct?
You need to first choose which is your source of truth where you are showing data from. In your case it seems to be your local database which is filled from some external source.
If you wish to use your local database with data provided from remote server then you need to have some information to keep track of "same" entries. This is most usually achieved by using an id, an identifier which is unique between entries and persistent over changes in entry. Any other property may be used that corresponds to those rules. For instance CharCode may be sufficient in your case if there will always be only one of them.
Next to that you may need a deleted flag which means that you also get deleted items from server and when you find entry with deleted flag set to true you need to delete this object.
Now your pseudo code when getting items from server you should do:
func processEntries(_ remoteEntries: [CurrencyEntry]) {
remoteEntries.forEach { remoteEntry in
if remoteEntry.isDeleted {
if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
existingLocalEntry.deleteFromDatabase()
}
} else {
if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
database.updateCurrency(existingLocalEntry, withRemoteEntry: remoteEntry)
} else {
database.createCurrency(fromRemoteEntry: remoteEntry)
}
}
}
}
And this is just for synchronization approach.
Now going to your view controller. When it appears you call to reload data from server and at the same time display whatever is in your database. This is all fine but you are missing a re-display once new data is available.
What you optimally need is another hook where your view controller will be notified when database has changes. So best thing to do is add event when your database changes which is whenever you save your database. You probably looking at something like this:
extension Database {
func save() {
guard context.hasChanges else { return }
try? context.save()
self.notifyListenersOfChangesInDatabase()
}
and this method would be called within your parseJSON and everywhere else that you save your database. Then you would add your view controller as a listener on your database as
override func viewDidLoad() {
super.viewDidLoad()
database.addListener(self)
currencyNetworking.performRequest()
loadCurrency()
}
How exactly will listener and database be connected is up to you. I would suggest using delegate approach but it would mean a bit more work. So another simpler approach is using NotificationCenter which should have a lot of examples online, including on StackOverflow. The result will be
Instead of database.addListener(self) you have NotificationCenter.default.addObserver...
Instead of notifyListenersOfChangesInDatabase you use NotificationCenter.default.post...
In any of the cases you get a method in your view controller which is triggered whenever your database is changed. And in that method you call loadCurrency. This is pretty amazing because now you don't care who updated the data in your database, why and when. Whenever the change occurs you reload your data and user sees changes. For instance you could have a timer that pools for new data every few minutes and everything would just work without you needing to change anything.
The other approach you can do is simply add a closure to your performRequest method which triggers as soon as your request is done. This way your code should be like
override func viewDidLoad() {
super.viewDidLoad()
loadCurrency()
currencyNetworking.performRequest {
self.loadCurrency()
}
}
note that loadCurrency is called twice. It means that we first want to show whatever is stored in local database so that user sees his data more or less instantly. At the same time we send out a request to remote server which may take a while. And once server response finished processing a reload is done again and user can view updated data.
Your method performRequest could then look like something as the following:
func performRequest(_ completion: #escaping () -> Void) {
getDataFromRemoteServer { rawData in
parseJSON(with: rawData)
completion()
}
}

Fetching and parsing functionality location in MVC

I need to load data from rss and display it in tableview. I decided to do it in some other class rather than in ViewController. So I created DataFetch class. The problem is that I use third party library to work with rss, and it works like this:
func fetchPodcastFeed() -> [RSSFeedItem]{
let feedURL = NSURL(string: feedURLString)!
var feedItems = [RSSFeedItem]()
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), {
// Run parsing in a background thread
FeedParser(URL: feedURL)?.parse({ (result) in
feedItems = (result.rssFeed?.items)!
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//reload table
})
})
})
return feedItems
}
Of course when I call fetchItems = dataFetcher.fetchPodcastFeed() from ViewController I don't get any data. Should I use notifications and if so, how to pass fetched data to ViewController through them? Or is there better approach?
So I assume the fetchPodcastFeed() function is your code, right?
Then I'd suggest defining it with a callback like so:
func fetchPodcastFeed(onCompletion:(result:[RSSFeedItem]) -> Void) {
// setup as you have it ...
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// call the completion closure
onCompletion(feedItems)
})
})
})
}
Then, wherever you're calling the method, do it like this:
fetchPodcastFeed({(result) in
// reload your table with result, which is of type [RSSFeedItem]
})
Theoretically you could also simply pass the completion handler directly into the parse method you're calling on the FeedParser object, but since you are on a background thread it's probably wiser to first send it to the main thread again. Otherwise you'd have to put the reload table stuff in a dispatch_asynccall, it's nicer the way you started.

how can I access field/method from another UIViewController in Swift?

I have the following situation:
I have a UIViewController that has embedded two Container Views, one is hidden and one is visible. Both of those Container Views have embedded another UIViewControllers with some elements.
I want to present a data fetched from webservice on both panels, but I want to fetch the data only once, then just parse it in a specific way, based on what panel user currently sees.
I have a method that fetches data as json:
func loadInitialDataWithoutCluster() {
print("loadInitialData");
RestApiManager.sharedInstance.getRequests { json in
if let jsonData = json.array {
for requestJSON in jsonData {
dispatch_async(dispatch_get_main_queue(),{
if let request = SingleRequest.fromJSON(requestJSON){
//how can I add those single requests
// to a list/array/whatever here that will be
// accessible from both of two other panels?
}
})
}
}
}
}
and then when it fetches all the data and (somehow) assigns it to the list/array - how can I refer to it from two different panels?
This would be the solution for best programming practice. Technically you shouldn't allow any class to manipulate your data directly, instead create functions which do the work for you. I have included a function which reads and writes to an array, you can manipulate these to your needs.
class ArrayObject {
private var downloadedData = [1,2,3]
internal func readData() -> [Int] {
return downloadedData
}
internal func addData(number: Int) -> [Int] {
downloadedData.append(number)
return downloadedData
}
}
class genericControllerClass: UIViewController {
func currentData() {
let getUsers = ArrayObject().addData(15)
}
}
You can create a new file with a simple structure to store an array of the details you need, you can access this from anywhere in the project.
struct Arrays {
static var downloadedData = [TheTypeYouNeedToStore]()
}
You can add to this array once the data has been downloaded just by using Arrays.downloadedData.append(). Then anywhere else in the project this can be accessed and modified using Arrays.downloadedData[0]

Capturing closure values in Swift

My question is very similar to several others here but I just can't get it to work. I'm making an API call via a helper class that I wrote.
First I tried a standard function with a return value and the result was as expected. The background task completed after I tired to assign the result.
Now I'm using a closure and I can get the value back into my view controller but its still stuck in the closure, I have the same problem. I know I need to use GCD to get the assignment to happen in the main queue.
this is what I have in my view controller
var artists = [String]()
let api = APIController()
api.getArtistList("foo fighters") { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
print("in the closure: \(artists)")
}
}
}
print ("method 1 results: \(artists)")
as the results are:
method 1 results: []
in the closure: [Foo Fighters & Brian May, UK Foo Fighters, John Fogerty with Foo Fighters, Foo Fighters, Foo Fighters feat. Norah Jones, Foo Fighters feat. Brian May, Foo Fighters vs. Beastie Boys]
I know why this is happening, I just don't know how to fix it :( The API calls need to be async, so what is the best practice for capturing these results? Based on what the user selects in the table view I'll be making subsequent api calls so its not like I can handle everything inside the closure
I completely agree with the #Craig proposal of the use of the GCD, but as your question involves the request of the API call every time you select a row, you can do the following:
Let's suppose you use the tableView:didSelectRowAtIndexPath: method to handle the selection, then you can do the following inside it:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// it is just a form to get the item
let selectedItem = items.objectAtIndex(indexPath.row) as String
api.getArtistList(selectedItem) { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
}
}
}
}
And then you can observe the property and handle do you want inside it :
var artists: [String] = [] {
didSet {
self.tableView.reloadData() // or anything you need to handle.
}
}
It just another way to see it. I hope this help you.
The easy solution is to do whatever you're doing at your print(), inside the closure.
Since you're already dispatch_asyncing to the main queue (the main/GUI thread), you can complete any processing there. Push a new view controller, present some modal data, update your current view controller, etc.
Just make sure that you don't have multiple threads modifying/accessing your local/cached data that is being displayed. Especially if it's being used by UITableViewDelegate / UITableViewDataSource implementations, which will throw fits if you start getting wishy-washy or inconsistent with your return values.
As long as you can retrieve the data in the background, and the only processing that needs to occur on the main thread is an instance variable reassignment, or some kind of array appending, just do that on the main thread, using the data you retrieved on the back end. It's not heavy. If it is heavy, then you're going to need more sophisticated synchronization methods to protect your data.
Normally the pattern looks like:
dispatch_async(getBackgroundQueue(), {
var theData = getTheDataFromNetwork();
dispatch_async(dispatch_get_main_queue() {
self.data = theData // Update the instance variable of your ViewController
self.tableView.reloadData() // Or some other 'reload' method
});
})
So where you'd normally refresh a table view or notify your ViewController that the operation has completed (or that local data has been updated), you should continue your main-thread processing.

problems using Grand Central Dispatch IOS Swift Xcode 6.3.1

I'm learning swift now and having hard time understanding the multithreading issues..
specific problem Im having is that i'm loading data from the internet and
trying to return an array ("broadcasts") containing this data while using dispatch_async .
My problem is that the return execution with the empty array happens before
the array is filled with the data (this line "println(broadcasts)" happens but the array returns empty.. )
Here is my code:
import UIKit
public class BroadcastRequest {
func requestNewBroadcasts() -> [BroadcastModel] {
var broadcasts = [BroadcastModel]()
var sectionsOfBroadcasts = [[BroadcastModel]]()
DataManager.getBroadcastsFrom׳TweeterWithSuccess { (youTubeData) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let json = JSON(data: youTubeData)
if let broadcastsArray = json["result"].array {
for broadcastDict in broadcastsArray{
var broadcastID: String? = broadcastDict["id"].string
var broadcastURL: String? = broadcastDict["broadcast"].string
DataManager.getBroadcastDetailsFromYouTubeWithSuccess(broadcastURL!) { (youTubeData) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let json2 = JSON(data: youTubeData)
if let broadcastDetailsDict = json2["result"].dictionary {
var cover: String! = broadcastDetailsDict["cover"]!.string
var caption: String! = broadcastDetailsDict["caption"]!.string
var broadcast = BroadcastModel(id: broadcastID, broadcastURL: broadcastURL, cover: cover, caption: caption)
broadcasts.append(broadcast)
**println(broadcasts)**
}
}
}
}
}
}
}
**return broadcasts**
}
}
After looking at answers i have changed the code to this:
import UIKit
public class BroadcastRequest {
func requestNewBroadcasts() {
var broadcasts = [BroadcastModel]()
var sectionsOfBroadcasts = [[BroadcastModel]]()
DataManager.getBroadcastsFrom׳TweeterWithSuccess { (youTubeData) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let json = JSON(data: youTubeData)
if let broadcastsArray = json["result"].array {
for broadcastDict in broadcastsArray{
var broadcastID: String? = broadcastDict["id"].string
var broadcastURL: String? = broadcastDict["broadcast"].string
DataManager.getBroadcastDetailsFromYouTubeWithSuccess(broadcastURL!) { (youTubeData) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let json2 = JSON(data: youTubeData)
if let broadcastDetailsDict = json2["result"].dictionary {
var cover: String! = broadcastDetailsDict["cover"]!.string
var caption: String! = broadcastDetailsDict["caption"]!.string
var broadcast = BroadcastModel(id: broadcastID, broadcastURL: broadcastURL, cover: cover, caption: caption)
broadcasts.append(broadcast)
println(broadcasts)
}
}
}
}
}
}
}
**TableViewController.requestFinished(broadcasts)**
}
}
And in class TableViewController-
public class TableViewController: UITableViewController {
...
...
public func requestFinished(requestedBroadcasts: [BroadcastModel]) {
self.broadcasts = requestedBroadcasts
self.tableView.reloadData()
}
Now i get the error:
Cannot invoke 'requestFinished' with an argument list of type '([(BroadcastModel)])'
Your code is wrong. A call to a GCD function like dispatch_async returns immediately, before the code in the closure has even begun executing. That's the nature of concurrent programming.
You can't write async code that makes a call and then has the answer on the next line. Instead you need to change your logic so that you make the call to dispatch async and then return and forget about the request until the block completes. Then you put the code that handles the result inside the closure. You can add a call inside your closure that calls out to your app on the main thread to notify it that processing is complete.
EDIT:
Your new code has multiple problems:
Your call to dispatch_async is using the main queue, which is a queue on the main thread. If your goal is to get this code to run in the background, this code fails to do that.
The call to TableViewController.requestFinished(broadcasts) is still in the wrong place. It needs to be inside the block, after the data has been fetched. it also needs to be performed on the main thread.
The call to TableViewController.requestFinished(broadcasts) appears to be sending a message to the TableViewController class rather than to an instance of the TableViewController class, which is wrong. You can't fix that unless your block has access to the instance of TableViewController that you want to send the message to.
You should probably refactor your requestNewBroadcasts method as follows:
Make it not return anything. (The result won't be available until after the async block completes anyway.)
Make requestNewBroadcasts take a completion block (or rather a closure, as they are called in Swift). Get rid of the TableViewController.requestFinished(broadcasts) call entirely, and instead have your network code call the completion block once the network request is complete.
Make your call to dispatch_async use a background queue rather than the main queue so that your task actually runs in the background.
Make your requestNewBroadcasts method invoke the completion block on the main thread.
Each of those steps is going to require work and research on your part.
Figuring out how to add a closure as a parameter will take digging. (See the Swift Programming Language iBook. It explains it well.)
Figuring out how to get a background queue will take work. (See the Xcode docs documentation on GCD. In particular look at dispatch_get_global_queue)
Figuring out how to make a call to the main thread from a background thread will take research (again see the Xcode docs on GCD. Hint: Your current call to dispatch_async is sending your block to a queue on the main thread.).
return broadcasts is executed before the loop that appends data to your array because of DataManager.getBroadcastsFromTweeterWithSuccess's closure.

Resources