I'm trying to parse all the Image provided by downloaded JSON on my App. So, I've heard many ways to do that correctly. Which API should I used to manage the Images on my App? How should I do that, can I have an example?
I also wanted to take care correctly of delays between:
Run the app --> Load data --> Populate UI elements
What should I do to minimize this delay, I think a professional app shouldn't take that long to load all components.
That's the part where I'll populate a UITableView with Images.
var arrCerveja = [Cerveja]()
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
//TableView DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrCerveja.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! TableViewCell
let model = arrCerveja[indexPath.row]
cell.labelName.text = model.name
cell.labelDetail.text = "\(model.abv)"
cell. imageViewCell.image = ???? //How should I do that?
return cell
}
//TableView Delegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
override func viewDidLoad() {
super.viewDidLoad()
getApiData { (cerveja) in
arrCerveja = cerveja
self.tableView.reloadData()
}
}
Model Folder:
import Foundation
struct Cerveja:Decodable{
let name:String
let abv:Double
let image_url:String
}
Networking Folder:
import Alamofire
func getApiData(completion: #escaping ([Cerveja]) -> ()){
guard let urlString = URL(string: "https://api.punkapi.com/v2/beers") else {
print("URL Error")
return
}
Alamofire.request(urlString).responseJSON { response in
if response.data == response.data{
do{
let decoder = try JSONDecoder().decode([Cerveja].self, from: response.data!)
completion(decoder)
}catch{
print(error)
}
}else{print("API Response is Empty")}
}
}
What you can do is to cache the downloaded images, many libraries exist to help you do that, here is a list of some of them:
Kingfisher
HanekeSwift
Cache
Kingfisher is a good one that also allow you to download the images and explain you how to use the library with table views.
Caching the images will also reduce the loading time the next time the app is open.
You can also use this library to display a user friendly loading to the user during loading.
Related
I am trying to load my tableview without using dequeueReusableCell, but it just crashes my app, cant figure out what i am doing wrong?
let cell = Tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SökrutaCell // THIS CODE WORKS FINE
let cell = SökrutaCell() // THIS CODE CRASHES - Unexpectedly found nil while unwrapping an optional
Would love if anyone could point me in the right direction so i can understand what is failing.
import UIKit
import Alamofire
class Sökruta: UIViewController {
var sökresultat = [Object2]()
#IBOutlet weak var Tableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
GetRequest(Url: "https://randomurl.se")
Tableview.delegate = self
Tableview.dataSource = self
Tableview.backgroundColor = UIColor.white
// Do any additional setup after loading the view.
}
}
// MARK: - Delegate extension 1
extension Sökruta: UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("you tapped me!")
}
}
// MARK: - Delegate extension 2
extension Sökruta: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sökresultat.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let cell = Tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SökrutaCell // THIS CODE WORKS FINE
let cell = SökrutaCell() // THIS CODE CRASHES - Unexpectedly found nil while unwrapping an optional
cell.Söknamn.text = sökresultat[indexPath.row].displayname
if let url = URL(string: dontmindthiscode) {
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
cell.Sökbild.image = UIImage(data: data)
}
} catch let err {
print("error: \(err.localizedDescription)")
}
}
}
else
{
DispatchQueue.main.async {
cell.Sökbild.image = UIImage(systemName: "eye.slash")
}
}
return cell
}
}
// MARK: - Extension functions
extension Sökruta{
// MARK: - GET REQUEST
func GetRequest(Url: String) {
// MARK: - Login details
let headers: HTTPHeaders = [
.authorization(username: "username", password: "password"),
.accept("application/json")]
// MARK: - Api request
AF.request(result, headers: headers).validate().responseJSON { response in
// MARK: - Check for errors
if let error = response.error
{
print (error)
return}
// MARK: - Print response
if response.response != nil
{ }
// MARK: - Print data
if response.data != nil
{
let decoder = JSONDecoder()
do
{
let api = try decoder.decode(Main2.self, from: response.data!)
self.sökresultat = api.objects
self.Tableview.reloadData()
}
catch {
print(error.localizedDescription)
print("Error in JSON parsing")
}
}
}
}// MARK: - END
}
This is my cell.swift code:
import UIKit
class SökrutaCell: UITableViewCell {
#IBOutlet weak var Sökbild: UIImageView!
#IBOutlet weak var Söknamn: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
You have a class with #IBOutlet references to a prototype cell in your storyboard. You can't use that cell prototype (with its custom layout) and have the outlets hooked up for you unless you use the dequeueReusableCell(withIdentifier:for:).
If your goal was to compare this against a non-reused cell, you could instantiate a UITableViewCell programmatically and use the built in textLabel and imageView properties. For example, you could:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "...")
cell.textLabel?.text = ...
cell.imageView?.image = ... // use a placeholder image here or else it won't show the image view at all!
someMethodToFetchImageAsynchronously { image in
// make sure this cell hasn't scrolled out of view!!!
guard let cell = tableView.cellForRow(at: indexPath) else { return }
// if still visible, update its image
cell.imageView?.image = image
}
return cell
}
Or you could consider more complicated programmatic patterns (where you add controls and configure them manually, which probably would be a more fair comparison), too. But the above is a fairly minimal implementation.
Needless to say, this pattern is not recommended, as you lose the storyboard cell prototype benefits and the performance/memory benefits of cell reuse. But if your goal was just to compare and contrast the memory, performance, and software design considerations, perhaps this helps you get your arms around it.
While I've attempted to answer your question, before I worried about the inherent performance of dequeued cells, your image retrieval mechanism is a far greater performance concern. You are fetching images using Data(contentsOf:), a non-cancelable network request. So, if you have hundred rows, and you quickly scroll down to rows 90..<100, the image retrieval for those 10 rows will be backlogged being the network requests for the first 90 rows! Also, are your images appropriately sized for the small image view in the cell? That can also observably impact performance and the smoothness of the scrolling.
There are a number of decent asynchronous image retrieval libraries out there. E.g. since you are already using Alamofire, I would suggest you consider AlamofireImage. This offers nice asynchronous image retrieval mechanisms, nice UIImageView extensions, the ability to cancel requests that are no longer needed, image caching, etc.
But proper asynchronous image retrieval for a table view is a non-trivial problem. An image processing library like AlamofireImage, KingFisher, SDWebImage, etc., can simplify this process for you.
Got stuck with displaying the data from Realm in the table view, embedded in a view controller. Here's some code.
Data model class that handles the scheme of how the data is stored (this is a finance tracking app):
class Entry: Object {
#objc dynamic var name: String = ""
#objc dynamic var amount: Int = 0
#objc dynamic var date: Date?
var isExpense: Bool = false
// initialization block dropped for concise post purposes
}
Cell class:
class FinanceOverviewCell: UITableViewCell {
#IBOutlet weak var entryNameLabel: UILabel!
#IBOutlet weak var entryAmountLabel: UILabel!
func updateData(name: String, amount: String) {
entryNameLabel?.text = name
entryAmountLabel?.text = amount
}
}
Finally, view controller:
import UIKit
import RealmSwift
class FinanceOverviewController: UIViewController {
#IBOutlet weak var financeOverviewTableView: UITableView!
#IBOutlet weak var currentBalanceLabel: UILabel!
var realm = try! Realm()
let tableEntries = try! Realm().objects(Entry.self)
let entriesManager = EntriesManager()
var notificationToken: NotificationToken?
override func viewDidLoad() {
super.viewDidLoad()
print(tableEntries)
self.financeOverviewTableView.reloadData()
financeOverviewTableView.delegate = self
financeOverviewTableView.dataSource = self
// notification token code block dropped for concise post purposes
// outlets code block removed for concise post purposes
extension FinanceOverviewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
realm.beginWrite()
realm.delete(tableEntries[indexPath.row])
try! realm.commitWrite()
}
}
}
extension FinanceOverviewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableEntries.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = financeOverviewTableView.dequeueReusableCell(withIdentifier: "FinanceOverviewCell", for: indexPath) as! FinanceOverviewCell
let tableData = tableEntries[indexPath.row]
cell.updateData(name: tableData.name, amount: String(tableData.amount))
return cell
}
}
The problem is in cellForRowAt func in the view controller. Somehow, though tableData each time holds something proper to add a row, it simply adds nothing. In the app screen I see it as yet another row with placeholder labels comes in.
What could be the issue? I read some reference as well as looked through official example but didn't see any special solution for this case.
Thank you for the support.
Actually working with Realm is not hard, I tried to reproduce your code and everything is work fine. I think it's related to NotificationToken, because every time your collection changed, notification token run block of code and I don't know how you update your tableView. I suggest you to use this extension, I'm using it for months and it's the best way to adopt UITableView with Realm's NotificationToken:
extension UITableView {
func applyChanges<T>(changes: RealmCollectionChange<T>) {
switch changes {
case .initial: reloadData()
case .update(let results, let deletions, let insertions, let updates):
let fromRow = { (row: Int) in return IndexPath(row: row, section: 0) }
beginUpdates()
insertRows(at: insertions.map(fromRow), with: .automatic)
reloadRows(at: updates.map(fromRow), with: .automatic)
deleteRows(at: deletions.map(fromRow), with: .automatic)
endUpdates()
case .error(let error): fatalError("\(error)")
}
}
}
example of usage:
notificationToken = tableEntries.addNotificationBlock { changes in
financeOverviewTableView.applyChanges(changes)
}
source: https://academy.realm.io/posts/meetup-jp-simard-mastering-realm-notifications/
I would like to retrieve data from my simple Firestore database
I have this database:
then I have a model class where I have a method responsible for retrieving a data which looks like this:
func getDataFromDatabase() -> [String] {
var notes: [String] = []
collectionRef = Firestore.firestore().collection("Notes")
collectionRef.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
print("inside notes: \(notes)")
}
print("outside notes: \(notes)")
return notes
}
and as a UI representation I have tableViewController. Let's take one of the methods, for example
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableview numberOfRowsInSection called")
return model.getDataFromDatabase().count
}
Then numberOfRows is 0 and the output in the console is:
and I am ending up with no cells in tableView. I added a breakpoint and it doesn't jump inside the listener.
And even though I have 3 of them, they are kinda "late"? They are loaded afterwards. And then the tableView doesn't show anything but console says (later) that there are 3 cells.
If needed, there is also my method for showing the cells names:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("Cells")
let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)
cell.textLabel!.text = String(model.getDataFromDatabase()[indexPath.row].prefix(30))
return cell
}
but this method is not even loaded (no print in the console) and this method is written below the method with numberOfRowsInSection.
I have also 2 errors (I don't know why each line is written twice) and these are:
but I don't think it has something to do with the problem.
Thank you for your help!
As #Galo Torres Sevilla mentioned, addSnapshotListener method is async and you need to add completion handler to your getDataFromDatabase() function.
Make following changes in your code:
Declare Global variable for notes.
var list_notes = [String]()
Add completion handler to getDataFromDatabase() method.
func getDataFromDatabase(callback: #escaping([String]) -> Void) {
var notes: [String] = []
collectionRef = Firestore.firestore().collection("Notes")
collectionRef.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
print("inside notes: \(notes)")
callback(notes)
}
}
Lastly, call function on appropriate location where you want to fetch notes and assign retrieved notes to your global variable and reload TableView like below:
self.getDataFromDatabase { (list_notes) in
self.list_notes = list_notes
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Changes in TableView:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableview numberOfRowsInSection called")
return self.list_notes.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("Cells")
let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)
cell.textLabel!.text = String(self.list_notes[indexPath.row].prefix(30))
return cell
}
All you need to do is refresh the table cell every time you retrieve the data. Put this code after you set your data inside the array.
self.tableView.reloadData()
Recently in my app I have been using Firebase to store information for my app and it has worked well. Now I am using it to stream videos with a web view being used in the tableview to display Youtube videos. When trying to link the WebView to the database, I get an error that says:
Type 'video' has no subscript members
What would be causing this?
Here is the code:
import UIKit
import Firebase
class videoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var ref = DatabaseReference()
var video = [UIWebView]()
var databaseHandle:DatabaseHandle = 0
#IBOutlet var videoController: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref.child("Videos").observe(.childAdded) { (snapshot) in
let post = snapshot.value as? UIWebView
if let actualPost = post {
self.video.append(actualPost)
self.videoController.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return video.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
video.videos.allowsInlineMediaPlayback = video[indexPath.row]
return(video)
}
}
This line:
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
is your problem. This creates a new, local variable named video and it hides your video array property. Change it to:
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
Here's the whole method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
videoCell.videos.allowsInlineMediaPlayback = video[indexPath.row]
return videoCell
}
But on top of all of that, why do you have an array of web views? You certainly are not getting web views from Firebase.
And please fix your naming conventions. Class, struct, and enum names should start with uppercase letters. Variable, function, and case names start with lowercase letters. And use descriptive names. Naming everything simply video is confusing.
And change your video array to videos.
I create a TableView via StoryBoard. I connect all the element to a UITableViewCell class call PostTableCell. I already set identifier for the tableCell in StoryBoard. My code is as below:
class PopViewController: UIViewController ,IndicatorInfoProvider ,UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak var popTableView: UITableView!
var posts = [Post]()
override func viewDidLoad() {
super.viewDidLoad()
popTableView.dataSource = self
popTableView.delegate = self
fetchInitialPost()
}
func fetchInitialPost(){
Alamofire.request(MyURL, method: .get).responseJSON{
response in
switch response.result{
case .success(let result):
let json = JSON(result)
guard let feedArr = json["feed"].array else{
return
}
for post in feedArr {
if let post = post.dictionary,let feed = Post.init(dict: post){
self.posts.append(feed)
}
}
self.popTableView.reloadData()
break
case .failure(let error):
print(error)
}
}
}
func indicatorInfo(for pagerTabScripController : PagerTabStripViewController ) -> IndicatorInfo {
return IndicatorInfo (title : "Pop")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostTableCell", for: indexPath) as! PostTableCell
cell.posts = self.posts[indexPath.row]
return cell
}
}
I think I do all I should do in order to make it work already. But when I run the project I gives me this error, but I don't have any error in console. The app is just blank and this error below pops out.
So what cause this problem? And how to solve this?
This usually happens when there is a problem when loading the Storyboard.
I think (this has happend to me multiple times) you maybe have User Defined Runtime Attributes on popTableView in your Storyboard. If there are any that can not be set, because popTableView does not have the respective property, the debugger will show the behavior that you are seeing.
You can check if there are any User Defined Runtime Attributes by selecting the table view in Interface Builder an checking the Identity Inspector.
Try deleting any attributes that might cause problems.
Alternatively you forgot to connect the #IBOutlet in your Storyboard. You can check how to do that in the official documentation.