I'm building a simple app with swift by using MVVM binding.
What my app does is simply fetching data from a url and get a json response, and show the info on a table view.
Each cell contains a title, a subtitle, and an image.
However, the image is showing as a string in the json response. So I will need an extra network fetch to get the image for each cell after the 1st network call.
"articles": [
{
"title": "Wall Street tumbles with Nasdaq leading declines Reuters",
"description": "Wall Street's main indexes tumbled on Monday with Nasdaq leading the declines as technology stocks dropped on expectations of a sooner-than-expected rate hike that pushed U.S. Treasury yields to fresh two-year highs.",
"url": "https://www.reuters.com/markets/europe/wall-street-tumbles-with-nasdaq-leading-declines-2022-01-10/",
"urlToImage": "https://www.reuters.com/resizer/2cEiuwViTo_kOe7eWg4Igm8pm_Q=/1200x628/smart/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/POAM3MQFAJJX3MRXQW772WYKCA.jpg",
}]
My question is, how should I modify my code to make the tableCellView more "pure"? Currently it's calling network fetch to get the image. Where should I move that image fetching part from the tableviewcell config function to?
Should I change my Model to to contains the UIImage but NOT the string?
My Model:
struct Articles: Codable {
let articles: [Article]
}
struct Article: Codable {
let title: String
let description: String?
let urlToImage: String?
}
My ViewModel:
struct ViewMode {
var articles: Observable<[Article]> = Observable([])
}
Main functions in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
viewModel.articles.bind { [weak self] _ in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
fetchArticlesFromLocal(fileNmae: "response")
}
func fetchArticlesFromLocal(fileNmae: String) {
networkManager.fetchLocalJson(name: fileNmae) { [weak self] result in
switch result {
case.success(let data):
guard let data = data else {return}
do {
let articles = try JSONDecoder().decode(Articles.self, from: data)
self.viewModel.articles.value = articles.articles.compactMap({
Article(title: $0.title, description: $0.description, urlToImage: $0.urlToImage)
})
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.articles.value?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.cellID, for: indexPath) as? ImageTableViewCell {
if let articles = viewModel.articles.value {
cell.config(with: articles[indexPath.row])
}
}
return UITableViewCell()
}
}
My tableCellView:
func config(with article: Article) {
titleView.text = article.title
descriptionView.text = article.description
networkManager.fetchImage(url: article.urlToImage) { [weak self] result in
switch result {
case .success(let image):
DispatchQueue.main.async {
self?.iconView.image = image
}
case .failure(let error):
return
}
}
}
My binding:
class Observable<T> {
var value: T? {
didSet {
listener?(value)
}
}
typealias Listener = ((T?) -> Void)
var listener: Listener?
init(_ value: T?) {
self.value = value
}
func bind(_ listener: #escaping Listener) {
self.listener = listener
listener(value)
}
}
First of all, you say you use MVVM but I see some issues in implementation: you are passing a Model (Article instance) to the cell, but it should be a ViewModel (ArticleViewModel instance, but you didn't create that struct) since views should not have a direct reference to models in this architecture.
You also seem to add async/networking code inside the custom cell, which also violates the architecture and separation of concerns (even in MVC, that code should not be there, since Network Requests are part of the Service Layer and not the UI layer). The cell/view should know how to configure itself: 1. when image is available and 2. when image is not available... (you call reload on the cell from VC whenever the image becomes available, for instance)
Regarding your question, a better place to add the image downloading code would be cellForRowAt method. ViewModels should not be tied to UIKit generally speaking, so having a URL or String property for the image is fine.
To make the UI display smoothly on scrolling, you probably want to cancel the request: 1. when the image is prepared for reuse aka thrown in the reuse pool or 2. when it disappears from the visible area of the screen.
Here is a good article that uses approach #1: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/
Note that it caches the images in memory, but you may also want to cache them on disk.
View Controller is generally also considered to be more of a View, so technically we are still mixing Networking with UI. Perhaps it's better to use repository pattern either directly as a dependency of the View Controller when using MVC or a dependency of VC's View Model. View Model should not be aware if the Repository uses web requests to get the images or has them cached in memory or on disk, so use a protocol and Dependency Injection for that (also View Model may have to calculate the size/height of the cell based on the image size, before the cell is actually configured with that image).
Using this approach would work, but you will again violating some principles, and it's all your choice. Some people say it's ok to have UIKit/UIImage references inside view models (https://lickability.com/blog/our-view-on-view-models/, not sure if their argument should be considered valid though...).
Otherwise, you'll have to use some kind of auxiliary class/object to pass the image urls and do the networking part and keep track of what was already downloaded, what requests should be cancelled etc.
Remember, you don't have to follow MVC or MVVM strictly. Some object has to download those images anyway, and that object ideally should not be none of the view, view model or the data model. Name it as you want, ImageLoadingCoordinator, ImageRepository etc. and make sure it downloads asynchronously and has callbacks and gives the possibility to cancel requests and cache images. Generally speaking, try not to give any object to much responsibility or mix networking code with view code.
Related
I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}
Background: I've been teaching myself Swift, iOS and macOS for a few weeks and I've been trying to put together a 'Price Calculator' for macOS that grabs from a Database of paper and uses the selected properties to select a piece of paper. The user would be able to select from a list of properties such as: Size, Weight, Finish, Color, and Brand. From there it would calculate a price of the paper using general arithmetic.
At first I thought about using a SQLite or a similar Database style program to populate my Calculator – but the User needs to be able to populate the Paper Database themselves. Thus, I've started working with Core Data.
Problem: I have a tableView that's taking the Core Data and displaying the different paper's available – or at least it should be doing that. For testing, I've created a 'Add Paper' button that generates a random Paper and adds it to the Database. I know this is creating random random Paper because I'm able to display it in the Output. But I don't actually know if it's saving.
The problem I have is that when I try to refresh the table and display the Data to the tableView... it either Crashes due to Nil values or it does not add to the tableView.
I've come to the conclusion that I may be displaying the Data wrong to the table -OR- I'm not properly saving the Data -OR- I'm not fetching the Data correctly.
The resources online for iOS only helps so much as this is for Mac OS Any help in this would be appreciated. My Code can be found below and Thank you in advance: NOTE: I'm using the boilerplate AppDelegate file from the CoreData.
Defined in ViewController:
private var papers = [FlatPaper]()
private var appDelegate = NSApplication.shared.delegate as! AppDelegate
private let context = (NSApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
The Add Random Paper button (for testing):
#IBAction func addPaper(_ sender: NSButton) {
let data = PaperData()
let paper = FlatPaper(entity: FlatPaper.entity(), insertInto: context)
paper.paperSize = data.paperSize
paper.paperWeight = data.paperWeight
paper.paperBrand = data.paperBrand
paper.paperColor = data.paperColor
paper.paperFinish = data.paperFinish
paper.paperPrice = data.paperPrice
appDelegate.saveAction(paper)
papers.append(paper)
print("Add Paper Button Pressed. \(paper)")
refresh()
tableView.reloadData()
}
Extensions NSTableDelegate and NSTableDataSource:
extension ViewController: NSTableViewDataSource, NSTableViewDelegate {
// Mark: - Specifying how many rows
func numberOfRows(in tableView: NSTableView) -> Int {
return papers.count
}
// Mark: - Populating the Columns and Rows with Data
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
let paperSpecs = papers[row]
if tableColumn!.title == "Paper Size" {
return paperSpecs.paperSize
} else if tableColumn!.title == "Paper Weight" {
return paperSpecs.paperWeight
} else if tableColumn!.title == "Paper Brand" {
return paperSpecs.paperBrand
} else if tableColumn!.title == "Paper Color" {
return paperSpecs.paperColor
} else if tableColumn!.title == "Paper Finish" {
return paperSpecs.paperFinish
} else {
return paperSpecs.paperPrice
}
}
// Mark: - Refresh Method for reloading all of the data
private func refresh() {
do {
papers = try context.fetch(FlatPaper.fetchRequest())
} catch let error as NSError {
print("Could not fetch. \(error), USER INFO: \(error.userInfo)")
}
}
}
In tableview:objectValueFor you should get your paper specs object from your array using the row argument instead of creating a new (empty) instance.
let paperSpecs = papers[row]
You might also want to look into NSFetchedResultsController, it’s really helpful when working with Core data and table views.
I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.
I have a one-section collection view and would like to implement Drag and Drop to allow reordering of the items. The CollectionViewItem has several textviews showing properties form my Parameter objects. Reading the doc I need to implement the NSCollectionView delegate:
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let parameter = parameterForIndexPath(indexPath: indexPath)
return parameter // throws an error "Cannot convert return expression of type 'Parameter' to return type 'NSPasteboardWriting?'"
}
I have not found any information understandable for me describing the nature of the NSPasteboardWriting object. So, I have no idea how to proceed...
What is the NSPasteboardWriting object and what do I need to write in the pasteboard?
Thanks!
Disclaimer: I have struggled to find anything out there explaining this in a way that made sense to me, especially for Swift, and have had to piece the following together with a great deal of difficulty. If you know better, please tell me and I will correct it!
The "pasteboardwriter" methods (such as the one in your question) must return something identifiable for the item about to be dragged, that can be written to a pasteboard. The drag and drop methods then pass around this pasteboard item.
Most examples I've seen simply use a string representation of the object. You need this so that in the acceptDrop method you can get your hands back on the originating object (the item being dragged). Then you can re-order that item's position, or whatever action you need to take with it.
Drag and drop involves four principal steps. I'm currently doing this with a sourcelist view, so I will use that example instead of your collection view.
in viewDidLoad() register the sourcelist view to accept dropped objects. Do this by telling it which pasteboard type(s) it should accept.
// Register for the dropped object types we can accept.
sourceList.register(forDraggedTypes: [REORDER_SOURCELIST_PASTEBOARD_TYPE])
Here I'm using a custom type, REORDER_SOURCELIST_PASTEBOARD_TYPE that I define as a constant like so:
`let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
...where the value is something unique to your app ie yourdomain should be changed to something specific to your app eg com.myapp.sourcelist.item.
I define this outside any class (so it can be accessed from several classes) like so:
import Cocoa
let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
class Something {
// ...etc...
implement the view's pasteboardWriterForItem method. This varies slightly depending on the view you're using (i.e. sourcelist, collection view or whatever). For a sourcelist it looks like this:
// Return a pasteboard writer if this outlineview's item should be able to
// drag-and-drop.
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
let pbItem = NSPasteboardItem()
// See if the item is of the draggable kind. If so, set the pasteboard item.
if let draggableThing = ((item as? NSTreeNode)?.representedObject) as? DraggableThing {
pbItem.setString(draggableThing.uuid, forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
return pbItem;
}
return nil
}
The most notable part of that is draggableThing.uuid which is simply a string that can uniquely identify the dragged object via its pasteboard.
Figure out if your dragged item(s) can be dropped on the proposed item at the index given, and if so, return the kind of drop that should be.
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
// Get the pasteboard types that this dragging item can offer. If none
// then bail out.
guard let draggingTypes = info.draggingPasteboard().types else {
return []
}
if draggingTypes.contains(REORDER_SOURCELIST_PASTEBOARD_TYPE) {
if index >= 0 && item != nil {
return .move
}
}
return []
}
Process the drop event. Do things such as moving the dragged item(s) to their new position in the data model and reload the view, or move the rows in the view.
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
let pasteboard = info.draggingPasteboard()
let uuid = pasteboard.string(forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
// Go through each of the tree nodes, to see if this uuid is one of them.
var sourceNode: NSTreeNode?
if let item = item as? NSTreeNode, item.children != nil {
for node in item.children! {
if let collection = node.representedObject as? Collection {
if collection.uuid == uuid {
sourceNode = node
}
}
}
}
if sourceNode == nil {
return false
}
// Reorder the items.
let indexArr: [Int] = [1, index]
let toIndexPath = NSIndexPath(indexes: indexArr, length: 2)
treeController.move(sourceNode!, to: toIndexPath as IndexPath)
return true
}
Aside: The Cocoa mandate that we use pasteboard items for drag and drop seems very unnecessary to me --- why it can't simply pass around the originating (i.e. dragged) object I don't know! Obviously some drags originate outside the application, but for those that originate inside it, surely passing the object around would save all the hoop-jumping with the pasteboard.
The NSPasteboardWriting protocol provides methods that NSPasteboard (well, technically anyone, I guess) can use to generate different representations of an object for transferring around pasteboards, which is an older Apple concept that is used for copy/paste (hence Pasteboard) and, apparently, drag and drop in some cases.
It seems that, basically, a custom implementation of the protocol needs to implement methods that:
tell what UTI types (Apple's way of identifying file types [JPEG, GIF, TXT, DOCX, etc], similar to MIME-types—and that's a fun Google search 😬) your type can be transformed into
writeableTypes(for:) & writingOptions(forType:pasteboard:) to a lesser extent
provide a representation of your class for each of the UTI types you claimed to support
pasteboardPropertyList(forType:)
The other answer provides a straightforward implementation of this protocol for use within a single app.
But practically?
The Cocoa framework classes NSString, NSAttributedString, NSURL, NSColor, NSSound, NSImage, and NSPasteboardItem implement this protocol.
So if you've got a draggable item that can be completely represented as a URL (or String, or Color, or Sound, or Image, etc), just take the URL you have and cast it to NSPasteboardWriting?:
func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
let url: URL? = someCodeToGetTheURL(for: indexPath) // or NSURL
return url as NSPasteboardWriting?
}
This is not helpful if you have a complicated type, but I hope it's helpful if you have a collection view of images or some other basic item.
In my app, I built my own asynchronous image loading class. I pass in a object, then it checks if the cache (NSCache) has the image, if not it will then check the file system if the image is saved already. If the image is not saved already, it will then download the image in the background (NSOperations help).
This works great so far, but I have ran into a few small issues with the table view loading the images.
First off, this is the function I use to set up the table view cell fromtableView(tableView:, willDisplayCell:, forRowAtIndexPath:)
func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {
// Configure cell
if let show = dataSource.showFromIndexPath(indexPath) {
ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
self.indexPathsForFadedInImages.append(indexPath)
if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
func fadeInImage() {
// Fade in image
fetchCell.backgroundImageView!.alpha = 0.0
fetchCell.backgroundImage = image
UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
fetchCell.backgroundImageView!.alpha = 1.0
})
}
if #available(iOS 9, *) {
if NSProcessInfo.processInfo().lowPowerModeEnabled {
fetchCell.backgroundImage = image
}
else {
fadeInImage()
}
}
else {
fadeInImage()
}
}
else {
// Issues are here
}
}
else {
// Set image
cell.backgroundImage = image
}
})
...
}
Where "// Issues are here" comment is, that is where I run into multiple issues.
So far, I have not figured out another way to validate that the image belongs to the cell for sure where "// Issues are here" is. If I add
cell.backgroundImage = image
there, then it fixes the issue where sometimes the image will not display on the table view cell. So far the only cause I have found for this is that the image is being returned faster than I can return the table view cell so that is why the table view says there is not a cell at that index path.
But if I add that code there, then I run into another issue! Cells will display the wrong images and then it lags down the app and the image will constantly switch, or even just stay on the wrong image.
I have checked that it runs on the main thread, image downloading and caching is all fine. It just has to do that the table is saying there is no cell at that index path, and I have tried getting an indexPath for the cell which returns also nil.
A semi-solution to this problem is called tableView.reloadData() in viewWillAppear/viewDidAppear. This will fix the issue, but then I lose the animation for table view cells on screen.
EDIT:
If I pass the image view into getImageForShow() and set it directly it will fix this issue, but that is less ideal design of code. The image view obviously exists, the cell exists, but for some reason it doesn't want to work every time.
Table views reuse cells to save memory, which can cause problems with any async routines that need to be performed to display the cell's data (like loading an image). If the cell is supposed to be displaying different data when the async operation completes, the app can suddenly go into an inconsistent display state.
To get around this, I recommend adding a generation property to your cells, and checking that property when the async operation completes:
protocol MyImageManager {
static var sharedManager: MyImageManager { get }
func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}
struct MyCellData {
let url: String
}
class MyTableViewCell: UITableViewCell {
// The generation will tell us which iteration of the cell we're working with
var generation: Int = 0
override func prepareForReuse() {
super.prepareForReuse()
// Increment the generation when the cell is recycled
self.generation++
self.data = nil
}
var data: MyCellData? {
didSet {
// Reset the display state
self.imageView?.image = nil
self.imageView?.alpha = 0
if let data = self.data {
// Remember what generation the cell is on
var generation = self.generation
// In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
weak var wcell = self
// Retrieve the image from the server (or from the local cache)
MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
if let error = error {
println("There was a problem fetching the image")
} else if let cell = wcell, image = image where cell.generation == generation {
// Make sure that UI updates happen on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Only update the cell if the generation value matches what it was prior to fetching the image
cell.imageView?.image = image
cell.imageView?.alpha = 0
UIView.animateWithDuration(0.25, animations: { () -> Void in
cell.imageView?.alpha = 1
})
})
}
})
}
}
}
}
class MyTableViewController: UITableViewController {
var rows: [MyCellData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
cell.data = self.rows[indexPath.row]
return cell
}
}
A couple other notes:
Don't forget to do your display updates on the main thread. Updating on a network activity thread can cause the display to change at a seemingly random time (or never)
Be sure to weakly reference the cell (or any other UI elements) when you're performing an async operation in case the UI should be destroyed before the async op completes.