How to correctly load information from Firebase? - ios

I will try my best to explain what I'm doing. I have an infinite scrolling collection view that loads a value from my firebase database for every cell. Every time the collection view creates a cell it calls .observe on the database location and gets a snapchat. If it loads any value it sets the cells background to black. Black background = cell loaded from database. If you look at the image below you can tell not all cells are loading from the database. Can the database not handle this many calls? Is it a threading issue? Does what I'm doing work with firebase? As of right now I call firebase in my
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
method
After some testing it seems that it is loading everything from firebase fine, but it's not updating the UI. This leads me to believe it's creating the cell before it's loading the information for some reason? I will try to figure out how to make it 'block' somehow.
The image

You should delegate the loading to the cell itself, not your collectionView: cellForItemAtIndexPath method. The reason for this is the delegate method will hang asynchronously and for the callback of the FireBase network task. While the latter is usually quick (by experience), you might have some issues here with UI loading.. Judging by the number of squares on your view..
So ideally, you'd want something like this:
import FirebaseDatabase
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let ref = Database.database().reference(fromURL: "Your Firebase URL")
}
class BasicCell : UICollectionViewCell {
var firPathObserver : String { //This will make sure that as soon as you set the value, it will fetch from firebase
didSet {
let path = firPathObserver
FirebaseNode.ref.thePathToYouDoc(path) ..... {
snapshot _
self.handleSnapshot(snapshot)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubViews()
}
func setupSubViews() {
//you add your views here..
}
func handleSnapshot(_ snapshot: FIRSnapshot) {
//use the content of the observed value
DispatchQueue.main.async {
//handle UI updates/animations here
}
}
}
And you'd use it:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let path = someWhereThatStoresThePath(indexPath.item)//you get your observer ID according to indexPath.item.. in whichever way you do this
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Cell ID", for: indexPath) as! BasicCell
cell.firPathObserver = path
return cell
}
If this doesn't work, it's probable that you might be encountering some Firebase limitation.. which is rather unlikely imo.
Update .. with some corrections and with local cache.
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let node = FirebaseNode()
let ref = Database.database().reference(fromURL: "Your Firebase URL")
//This is the cache, set to private, since type casting between String and NSString would add messiness to your code
private var cache2 = NSCache<NSString, DataSnapshot>()
func getSnapshotWith(_ id: String) -> DataSnapshot? {
let identifier = id as NSString
return cache2.object(forKey: identifier)
}
func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
cache2.setObject(snapshot, forKey: id as NSString)
}
}
class BasicCell : UICollectionViewCell {
var firPathObserver : String? { //This will make sure that as soon as you set the value, it will fetch from firebase
didSet {
handleFirebaseContent(self.firPathObserver)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubViews() {
//you add your views here..
}
func handleFirebaseContent(_ atPath: String?) {
guard let path = atPath else {
//there is no content
handleNoPath()
return
}
if let localSnap = FirebaseNode.node.getSnapshotWith(path) {
handleSnapshot(localSnap)
return
}
makeFirebaseNetworkTaskFor(path)
}
func handleSnapshot(_ snapshot: DataSnapshot) {
//use the content of the observed value, create and apply vars
DispatchQueue.main.async {
//handle UI updates/animations here
}
}
private func handleNoPath() {
//make the change.
}
private func makeFirebaseNetworkTaskFor(_ id: String) {
FirebaseNode.node.ref.child("go all the way to your object tree...").child(id).observeSingleEvent(of: .value, with: {
(snapshot) in
//Add the conditional logic here..
//If snapshot != "<null>"
FirebaseNode.node.addSnapToCache(id, snapshot)
self.handleSnapshot(snapshot)
//If snapshot == "<null>"
return
}, withCancel: nil)
}
}
One point however, using NSCache: this works really well for small to medium sized lists or ranges of content; but it has a memory management feature which can de-alocated content if memory becomes scarce.. So when dealing with larger sets like yours, you might as well go for using a classing Dictionnary, as it's memory will not be de-alocated automatically. Using this just becomes as simple as swaping things out:
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let node = FirebaseNode()
let ref = Database.database().reference(fromURL: "Your Firebase URL")
//This is the cache, set to private, since type casting between String and NSString would add messiness to your code
private var cache2 : [String:DataSnapshot] = [:]
func getSnapshotWith(_ id: String) -> DataSnapshot? {
return cache2[id]
}
func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
cache2[id] = snapshot
}
}
Also, always make sure you got through the node strong reference to Firebasenode, this ensures that you're always using the ONE instance of Firebasenode. Ie, this is ok: Firebasenode.node.ref or Firebasenode.node.getSnapshot.., this is not: Firebasenode.ref or Firebasenode().ref

Related

Am I implementing the tableviewdatasource correctly to get downloaded data to show?

I am developing a small app to connect to my site, download data via a PHP web service, and display it in a table view. To get started I was following a tutorial over on Medium by Jose Ortiz Costa (Article on Medium).
I tweaked his project and got it running to verify the Web service was working and able to get the data. Once I got that working, I started a new project and tried to pull in some of the code that I needed to do the networking and tried to get it to display in a tableview in the same scene instead of a popup scene like Jose's project.
This is where I am running into some issues, as I'm still rather new to the swift programming language (started a Udemy course and have been picking things up from that) getting it to display in the table view. I can see that the request is still being sent/received, but I cannot get it to appear in the table view (either using my custom XIB or a programmatically created cell). I thought I understood how the code was broken down, and even tried to convert it from a UITableViewController to a UITableviewDataSource via an extension of the Viewcontroller.
At this point, I'm pretty stumped and will continue to inspect the code and tweak what I think might be the root cause. Any pointers on how to fix would be really appreciated!
Main Storyboard Screenshot
Struct for decoding my data / Lead class:
import Foundation
struct Lead: Decodable {
var id: Int
var name: String
var program: String
var stage: String
var lastAction: String
}
class LeadModel {
weak var delegate: Downloadable?
let networkModel = Network()
func downloadLeads(parameters: [String: Any], url: String) {
let request = networkModel.request(parameters: parameters, url: url)
networkModel.response(request: request) { (data) in
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
}
}
}
ViewController:
import UIKit
class LeadViewController: UIViewController {
// Buttons
#IBOutlet weak var newButton: UIButton!
#IBOutlet weak var firstContactButton: UIButton!
#IBOutlet weak var secondContactButton: UIButton!
#IBOutlet weak var leadTable: UITableView!
let model = LeadModel()
var models: [Lead]?
override func viewDidLoad() {
super.viewDidLoad()
//Make Buttons rounded
newButton.layer.cornerRadius = 10.0
firstContactButton.layer.cornerRadius = 10.0
secondContactButton.layer.cornerRadius = 10.0
//Delegate
model.delegate = self
}
//Send request to web service based off Buttons Name
#IBAction func findLeads(_ sender: UIButton) {
let new = sender.titleLabel?.text
let param = ["stage": new!]
print ("findLead hit")
model.downloadLeads(parameters: param, url: URLServices.leads)
}
}
extension LeadViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print ("number of sections hit")
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let _ = self.models else {
return 0
}
print ("tableView 1 hit")
return self.models!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an object from LeadCell
let cell = tableView.dequeueReusableCell(withIdentifier: "leadID", for: indexPath) as! LeadCell
// Lead selection
cell.leadName.text = self.models![indexPath.row].name
cell.actionName.text = self.models![indexPath.row].lastAction
cell.stageName.text = self.models![indexPath.row].stage
cell.progName.text = self.models![indexPath.row].program
print ("tableView 2 hit")
// Return the configured cell
return cell
}
}
extension LeadViewController: Downloadable {
func didReceiveData(data: Any) {
//Assign the data and refresh the table's data
DispatchQueue.main.async {
self.models = data as? [Lead]
self.leadTable.reloadData()
print ("LeadViewController Downloadable Hit")
}
}
}
EDIT
So with a little searching around (okay...A LOT of searching around), I finally found a piece that said I had to set the class as the datasource.
leadTable.dataSource = self
So that ended up working (well after I added a prototype cell with the identifier used in my code). I have a custom XIB that isn't working right now and that's my next tackle point.
You load the data, but don't use it. First, add the following statement to the end of the viewDidLoad method
model.delegate = self
Then add the following LeadViewController extension
extension LeadViewController: Downloadable {
func dicReceiveData(data: [Lead]) {
DispatchQueue.main.async {
self.models = data
self.tableView.reloadData()
}
}
}
And a couple of suggestions:
It is not a good practice to use the button title as a network request parameter:
let new = sender.titleLabel?.text
let param = ["stage": new!]
It is better to separate UI and logic. You can use the tag attribute for buttons (you can configure it in the storyboard or programmatically) to check what button is tapped.
You also have several unnecessary type casts in the LeadModel class. You can change
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
to
do {
let model = try JSONDecoder().decode([Lead].self, from: data)
self.delegate?.didReceiveData(data: model)
}
catch {}

What to do when object on realm gets invalidated

In my iOS app I am using the Realm library for storing data. It works great, until for one reason or the other, object get invalidated.
(reasons in the specific case could be that I sometimes implement background jobs on the same data that are getting visualized on view, or similar scenarios).
I know why it happens, and I understand is correct behavior but from there on: what is the correct behavior I should implement?
I would love to pull the data from Realm again and continue my job, but when an object gets invalidated, every field is inaccessible and crashes the environment (so, I don't know what is the unique+immutable id of the object).
How can I safely pull the current data from that object again, without storing the id in the ViewController?
Here is some code. I had to edit it heavily to since the structure of the app is different, but the issue is still exemplified. Assume that the table view delegate's is the view and all the technicalities are there.
// A class variable
var events: RLMResults<RLMMyEvent>
// A table view that shows all "MyEvents"
var tableview: UITableView
func initialiseView(_ predicate: NSPredicate) {
// Get the events with a predicate from Realm
events = RLMMyEvent.objects(with: predicate)
tableview.reloadData()
}
// All tableView delegates, and in particular
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MyCell
let event = self.events[UInt(indexPath.row)]
if !event.isInvalidated {
} else {
/***** HERE is the problem. What to do here?
HOW to get those data again, since I cannot
get that event's unique ID? ****/
}
cell.initialize(event)
return cell
}
Problem
As I understand, you want to access object's properties when the object is invalidated. Correct me if I'm wrong :)
First of all, let take a look at isInvalidated property
Indicates if the object can no longer be accessed because it is now invalid.
An object can no longer be accessed if the object has been deleted from the Realm that manages it, or if invalidate() is called on that Realm.
It means that an object can be invalidated only when it's managed by a Realm.
My solution
Detach object from the Realm which managers it. If object isn't managed by any Realm, of course it will never be invalidated and you can access properties as you want.
How to do
Whenever you fetch an object from Realm, detach it (create a cloned object).
Add below code to your project. It's used to detach every object in Result after fetching from Realm. I found here
protocol DetachableObject: AnyObject {
func detached() -> Self
}
extension Object: DetachableObject {
func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard let value = value(forKey: property.name) else { continue }
if property.isArray == true {
//Realm List property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else if property.type == .object {
//Realm Object property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension List: DetachableObject {
func detached() -> List<Element> {
let result = List<Element>()
forEach {
if let detachable = $0 as? DetachableObject {
let detached = detachable.detached() as! Element
result.append(detached)
} else {
result.append($0) //Primtives are pass by value; don't need to recreate
}
}
return result
}
func toArray() -> [Element] {
return Array(self.detached())
}
}
extension Results {
func toArray() -> [Element] {
let result = List<Element>()
forEach {
result.append($0)
}
return Array(result.detached())
}
}
Instead of keep events as RLMResults<RLMMyEvent>, keep it as [RLMMyEvent].
After fetching result, detach objects and convert to an array
events = RLMMyEvent.objects(with: predicate).toArray()
Now you can access properties without being afraid of invalidated objects and crash.
Note that detached objects will not be updated if the original objects or their values inside Realm are changed.
As a solution, you can observe for changes in the events array, and update/delete your cells accordingly, like
eventsObserveToken = events.observe { [weak tableView] changes in
guard let tableView = tableView else { return }
switch changes {
case .initial:
tableView.reloadData()
case .update(_, let deletions, let insertions, let updates):
tableView.applyChanges(deletions: deletions, insertions: insertions, updates: updates)
case .error: break
}
}
And an extension
extension IndexPath {
static func fromRow(_ row: Int) -> IndexPath {
return IndexPath(row: row, section: 0)
}
}
extension UITableView {
func applyChanges(deletions: [Int], insertions: [Int], updates: [Int]) {
beginUpdates()
deleteRows(at: deletions.map(IndexPath.fromRow), with: .automatic)
insertRows(at: insertions.map(IndexPath.fromRow), with: .automatic)
reloadRows(at: updates.map(IndexPath.fromRow), with: .automatic)
endUpdates()
}
}
And I believe that when an object is being invalidated it is being removed from the results array, so your app will not crash.
Or, you can use Unrealm. Unrealm enables you to easily store Swift native Classes, Structs and Enums into Realm without pain. And you don't need to worry about invalidated objects at all, because your app will not crash even if you try to access an invalidated object. Also, your app will not crash if you try to access an object from a different thread.
Have you tried using the RealmObject.isInvalidated variable to check if the object is still valid before you access it? Using this key won't cause the crash you're experiencing.
Current Scenario
realmObject.stringArray.append("")
Suggested Approach
if !realmObject.isInvalidated {
realmObject.stringArray.append("")
}

How to ensure the order of list shown in UITableView when getting data for cell from UIDocument

My app fetches data via FileManager for each cell in UITableView. The cells need data from a file which requires open UIDocument objects. However, it seems like code inside open completion handler get executed non predictably, so the cells don't get displayed in the order I wish.
How do I solve this problem? I appreciate if anyone gives any clue.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchCellsData()
}
func fetchCellsData() {
do {
let files = getUrlsOfFiles() //files in a certain order
for file in files {
let document = MyDocument(fileURL: file) //MyDocument subclassing UIDocument
//open to get data for cell
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.cellsData.append(data)
self.tableView.reloadData()
/// tried below also
// DispatchQueue.main.async {
// self.cellsData.append(data)
// self.tableView.reloadData()
// }
}
document.close(completionHandler: nil)
}
} catch {
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! MyCell
let fileInfo = cellsData[indexPath.row].fileInfo
//assign fileInfo into MyCell property
return cell
}
I expect cells get rendered in the order of 'files', however, it seems like the order is a bit unpredictable presuming that it's due to the fact that 'cellForRowAt' gets called before it knows about the full set of 'cellsData'.
From Apple documentation on UIdocument . Apple doc:
open(completionHandler:)
Opens a document asynchronously.
Which means that even if you trigger document.open in the right order, nothing guarantees that the completionHandler sequence will be in the same order, this is why the order is unpredictible.
However, you know that they will eventually all get done.
What you could do is :
1 - place all your datas from opened document into another list
2 - order this list in accordance to your need
3 - place this list into cellsData (which I assume is bound to your tableViesDatasource)
var allDatas: [/*your data type*/] = []
...
do {
// reset allDatas
allDatas = []
let files = getUrlsOfFiles()
for file in files {
let document = MyDocument(fileURL: file)
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.allDatas.append(data) // "buffer" list
self.tableView.reloadData()
}
document.close(completionHandler: nil)
}
} catch { ...
Then you change cellsData to a computed property like so :
var cellsData: [/*your data type*/] {
get {
return allDatas.order( /* order in accordance to your needs */ )
}
}
this way, each time a new data is added, the list is orderer before being redisplayed.
Discussion
this is the easier solution regarding the state of your code, however this may not be the overall prefered solution. For instance in your code, you reload your tableview each time you add a new value, knowing that there will be more data added after, which is not optimised.
I suggest you to read on Dispatch Group, this is a way to wait until all asynchronous operation your triggered are finished before executing certain actions (such as reloading your tableview in this case) (Readings: Raywenderlich tuts)

image and label in interface builder overlap my data in the TableView cell

I am a beginner in iOS development, and I want to make an instagram clone app, and I have a problem when making the news feed of the instagram clone app.
So I am using Firebase to store the image and the database. after posting the image (uploading the data to Firebase), I want to populate the table view using the uploaded data from my firebase.
But when I run the app, the dummy image and label from my storyboard overlaps the downloaded data that I put in the table view. the data that I download will eventually show after I scroll down.
Here is the gif when I run the app:
http://g.recordit.co/iGIybD9Pur.gif
There are 3 users that show in the .gif
username (the dummy from the storyboard)
JokowiRI
MegawatiRI
After asynchronously downloading the image from Firebase (after the loading indicator is dismissed), I expect MegawatiRI will show on the top of the table, but the dummy will show up first, but after I scroll down and back to the top, MegawatiRI will eventually shows up.
I believe that MegawatiRI is successfully downloaded, but I don't know why the dummy image seems overlaping the actual data. I don't want the dummy to show when my app running.
Here is the screenshot of the prototype cell:
And here is the simplified codes of the table view controller:
class NewsFeedTableViewController: UITableViewController {
var currentUser : User!
var media = [Media]()
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
// to set the dynamic height of table view
tableView.estimatedRowHeight = StoryBoard.mediaCellDefaultHeight
tableView.rowHeight = UITableViewAutomaticDimension
// to erase the separator in the table view
tableView.separatorColor = UIColor.clear
}
override func viewWillAppear(_ animated: Bool) {
// check wheter the user has already logged in or not
Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
RealTimeDatabaseReference.users(uid: user.uid).reference().observeSingleEvent(of: .value, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Any] {
self.currentUser = User(dictionary: userDict)
}
})
} else {
// user not logged in
self.performSegue(withIdentifier: StoryBoard.showWelcomeScreen, sender: nil)
}
}
tableView.reloadData()
fetchMedia()
}
func fetchMedia() {
SVProgressHUD.show()
Media.observeNewMedia { (mediaData) in
if !self.media.contains(mediaData) {
self.media.insert(mediaData, at: 0)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StoryBoard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
// to remove table view highlight style
cell.selectionStyle = .none
return cell
}
}
And here is the simplified code of the table view cell:
class MediaTableViewCell: UITableViewCell {
var currentUser: User!
var media: Media! {
didSet {
if currentUser != nil {
updateUI()
}
}
}
var cache = SAMCache.shared()
func updateUI () {
// check, if the image has already been downloaded and cached then just used the image, otherwise download from firebase storage
self.mediaImageView.image = nil
let cacheKey = "\(self.media.mediaUID))-postImage"
if let image = cache?.object(forKey: cacheKey) as? UIImage {
mediaImageView.image = image
} else {
media.downloadMediaImage { [weak self] (image, error) in
if error != nil {
print(error!)
}
if let image = image {
self?.mediaImageView.image = image
self?.cache?.setObject(image, forKey: cacheKey)
}
}
}
So what makes the dummy image overlaps my downloaded data?
Answer
The dummy images appear because your table view controller starts rendering cells before your current user is properly set on the tableViewController.
Thus, on the first call to cellForRowAtIndexPath, you probably have a nil currentUser in your controller, which gets passed to the cell. Hence the didSet property observer in your cell class does not call updateUI():
didSet {
if currentUser != nil {
updateUI()
}
}
Later, you reload the data and the current user has now been set, so things start to work as expected.
This line from your updateUI() should hide your dummy image. However, updateUI is not always being called as explained above:
self.mediaImageView.image = nil
I don't really see a reason why updateUI needs the current user to be not nil. So you could just eliminate the nil test in your didSet observer, and always call updateUI:
var media: Media! {
didSet {
updateUI()
}
Alternatively, you could rearrange your table view controller to actually wait for the current user to be set before loading the data source. The login-related code in your viewWillAppear has nested completion handers to set the current user. Those are likely executed asynchronously .. so you either have to wait for them to finish or deal with current user being nil.
Auth.auth etc {
// completes asynchronously, setting currentUser
}
// Unless you do something to wait, the rest starts IMMEDIATELY
// currentUser is not set yet
tableView.reloadData()
fetchMedia()
Other Notes
(1) I think it would be good form to reload the cell (using reloadRows) when the image downloads and has been inserted into your shared cache. You can refer to the answers in this question to see how an asynch task initiated from a cell can contact the tableViewController using NotificationCenter or delegation.
(2) I suspect that your image download tasks currently are running in the main thread, which is probably not what you intended. When you fix that, you will need to switch back to the main thread to either update the image (as you are doing now) or reload the row (as I recommend above).
Update your UI in main thread.
if let image = image {
DispatchQueue.main.async {
self?.mediaImageView.image = image
}
self?.cache?.setObject(image, forKey: cacheKey)
}

How do I async load a text file from URL, then async load images from URLs in the text file? (in swift)

I'm new to iOS development, so any help would be appreciated.
The project: I am developing an Emoji-style keyboard for iOS. When the keyboard first loads it downloads a .txt file from the Internet with image URLs (this way I can add/delete/reorder the images simply by updating that .txt file). The image URLs are entered into an Array, then the array of image URLs is used to populate a collection view.
The code I have written works, but when I switch to my keyboard the system waits until everything is loaded (typically 2-3 seconds) before the keyboard pops up. What I want is for the keyboard to popup instantly without images, then load the images as they become available - this way the user can see the keyboard working.
How can I get the keyboard to load instantly, then work on downloading the text file followed by the images? I am finding a lot of information on asynchronous downloading for images, but my problem is a little more complex with the addition of the text download and I can't quite figure it out.
more info:
I'm using SDWebImage to load the images so they are caching nicely and should be loading asynchronously, but the way I have it wrapped up the keyboard is waiting until everything is downloaded to display.
I tried wrapping the "do { } catch { }" portion of the code below inside - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { //do-catch code }) - when I do this the keyboard pops up instantly like I want, but instead of 2-3 seconds to load the images, it takes 8-10 seconds and it still waits for all images before showing any images.
Below is my code. I simplified the code by removing buttons and other code that is working. There are two .xib files associated with this code: KeyboardView and ImageCollectionViewCell
import UIKit
import MobileCoreServices
import SDWebImage
class ImageCollectionViewCell:UICollectionViewCell {
#IBOutlet var imgView:UIImageView!
}
class KeyboardViewController: UIInputViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet var collectionView: UICollectionView!
let blueMojiUrl = NSURL(string: "http://example.com/list/BlueMojiList.txt")
var blueMojiArray = NSMutableArray()
func isOpenAccessGranted() -> Bool {
// Function to test if Open Access is granted
return UIPasteboard.generalPasteboard().isKindOfClass(UIPasteboard)
}
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "KeyboardView", bundle: nil)
let objects = nib.instantiateWithOwner(self, options: nil)
self.view = objects[0] as! UIView;
self.collectionView.registerNib(UINib(nibName: "ImageCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "ImageCollectionViewCell")
if (isOpenAccessGranted()) == false {
// Open Access is NOT granted - display error instructions
} else {
// Open Access IS granted
do {
self.blueMojiArray = NSMutableArray(array: try NSString(contentsOfURL: self.blueMojiUrl!, usedEncoding: nil).componentsSeparatedByString("\n"));
self.collectionView.reloadData()
} catch {
// Error connecting to server - display error instructions
}
}
} // End ViewDidLoad
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.blueMojiArray.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let iCell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageCollectionViewCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let url = NSURL(string: self.blueMojiArray.objectAtIndex(indexPath.row) as! String)
iCell.imgView.sd_setImageWithURL(url, placeholderImage: UIImage(named: "loading"))
iCell.layer.cornerRadius = 10.0
return iCell
}
}
UPDATE:
Thanks for the answers. I was able to fix my problem with Alamofire. I installed the Alamofire pod to my project. The specific code I used to fix my issue: first I added import Alamofire at the top, then I removed the do { } catch { } code above and replaced it with
Alamofire.request(.GET, blueMojiUrl!)
.validate()
.responseString { response in
switch response.result {
case .Success:
let contents = try? NSString(contentsOfURL: self.blueMojiUrl!, usedEncoding: nil)
self.blueMojiArray = NSMutableArray(array: contents!.componentsSeparatedByString("\n"))
self.collectionView.reloadData()
case .Failure:
// Error connecting to server - display error instructions
}
}
Now the keyboard loads instantly, followed by the images loading within 2-3 seconds.
I would do something like that:
On cellForItemAtIndexPath call method to download image for current cell with completion handler. On download completion set cell's image to downloaded image and call method reloadItemsAtIndexPaths for this cell.
Example code:
func downloadPlaceholderImage(url: String, completionHandler: (image: UIImage) -> ()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
/// download logic
completionHandler(image: downloadedImage)
})
}
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
///
downloadPlaceHolderImage("url", completionHandler: { image in
cell.image = image
self.collectionView.reloadItemsAtIndexPaths([indexPath])
})
///
}
The recommend way to do this after iOS 8 is with NSURLSession.dataTaskWithURL
See:
sendAsynchronousRequest was deprecated in iOS 9, How to alter code to fix

Resources