I am having a problem at the moment, I have hooked up firebase to my app, created users able to post images to firebase, and to loop through posts to find the following users to create a feed. Only one problem, a tutorial I have been following to make the feed section, includes a void = downloadurl(with: String) and I cannot find this function. I have imported the same modules and app is nearly cloned to his. I am quite confused and was hoping someone could help out. Here is the collectionview cell at the moment:
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt
indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell",
for: indexPath) as! UserFeedCollectionViewCell
-> cell.myImage. downloadImage(from: String) <- does not exist, or
doesn't show up.
cell.myImage.layer.cornerRadius = 12.0
cell.myImage.clipsToBounds = true
return cell
}
The youtube tutorial is this link: https://youtu.be/fw7ySRFtX_M
and at exactly 35 minutes he shows you how to download the cell image. I have code for creating the feed, but this code is not imortant, and I have firebase and uikit imported.
Please give any tips if you can! Thank You (:
Here's the code for the downloadImage function. It is an extension of UIImageView. The extension should be placed outside of any classes.
class UsersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// deleted to save space
}
extension UIImageView {
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
task.resume()
}
}
This is not a built-in function or anything that is innate to Firebase or iOS. In the video, the author mentions that he created this function in his previous video, so you should probably find his previous video and/or download the final project to see what he did.
(There are also lots of great third party library like PINRemoteImage that provide some similar tools for you -- you might want to look at one of them, as well.)
Related
I am trying to recreate this thing. I've created in Storyboard skeleton. Here's the idea of my code:
Fetch images from URL's array with help of the function getThumbnailFromImage
Add UIImage's with my thumbnails in array webImages
Add in ViewController reusable cell MyCollectionView
...
But here I am with this))) (Don't mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don't know where to put it.
ViewController:
//
// ViewController.swift
// youtube-clone
//
// Created by мас on 16.08.2022.
//
import Foundation
import UIKit
import YouTubePlayer
import AVFoundation
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var url: [URL?] = [
URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
]
var webImages: [UIImage] = []
var currentPage: Int = 0
#IBOutlet var myPage: UIPageControl!
#IBOutlet weak var buttonInfo: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
myPage.currentPage = 0
myPage.numberOfPages = webImages.count
}
// MARK: - Collection View Setup
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return webImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
self.webImages.append(image!)
})
cell.myWebImage.image = webImages[indexPath.row]
cell.myWebImage.layer.cornerRadius = 20
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
myPage.currentPage = indexPath.row
}
// MARK: - Layout Setup // IGNORE IT
func setupLayout() {
buttonInfo.layer.cornerRadius = 25
buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
// MARK: - Videos Thumbnail Fetcher
func getThumbnailFromImage(url: URL, completion: #escaping ((_ image: UIImage?) -> Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
let thumbImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbImage)
}
}
catch {
print(error.localizedDescription)
}
}
}
}
Reusable Cell AKA MyCollectionCell:
import UIKit
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
}
P.s.: YouTubePlayer is custom pod from GitHub, it's not currently used.
You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
and each YouTube video has four generated images.
https://img.youtube.com/vi/{id}/0.jpg
https://img.youtube.com/vi/{id}/1.jpg
https://img.youtube.com/vi/{id}/2.jpg
https://img.youtube.com/vi/{id}/3.jpg
Example
https://img.youtube.com/vi/KhebpuFBD14/0.jpg
And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.
A couple of thoughts:
#matt is right in the comment - getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.
From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn't even be reached. (I'm not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I'm assuming you're populating webImages somewhere else before the collection view gets laid out).
One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
func configure(urlString: String) {
guard let self = self, let url = URL(string: urlString) else {
return
}
getThumbnailFromImage(url: url, completion: { [weak self] image in
self?.myWebImage.image = image
})
}
// Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
}
The cellForItemAt function in the VC would need to be changed to something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.configure(urlString: url[indexPath.row])
cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
return cell
}
An added benefit of this approach is that you're not referencing a separate array of images that could theoretically end up being in the wrong order if there's a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead - or eventually the number of elements returned from an API somewhere.
Side note - make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it's been deallocated! Currently the call to getThumbnailFromImage doesn't have that :)
Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.
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.
I would like to download multiple URLs from string in array, and display images in collectionView. I succeed to display four images (4 visible cells on my screen) and when I scroll, that begins the download of the 2 other images.
I want that these 6 images are downloaded in the same time (and I don't have to scroll for beginning the other download). After this, I want to display the total time spent to download and display images in my collectionView.
What I am doing wrong ?
Here is my code :
import UIKit
import AlamofireImage
import Alamofire
class ViewController: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var players = [
["image_Name":"https://images.unsplash.com/photo-1420768255295-e871cbf6eb81?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=4b94ef340bd7f6cac580fbc76af326af"],
["image_Name":"https://images.unsplash.com/photo-1465411801898-f1a78de00afc?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=149d27223217c0fa63c7dd8f1e8d23f6"],
["image_Name":"https://images.unsplash.com/photo-1466853817435-05b43fe45b39?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=a3b629b7e0c4f710ce119f219ae5b874"],
["image_Name":"https://images.unsplash.com/photo-1467404899198-ccadbcd96b91?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=66eef8db7a0aa4119c6c8d7ba211f79f"],
["image_Name":"https://images.unsplash.com/photo-1470322346096-ecab3914cab7?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=83863ba23662871baf6434c6000e00bd"],
["image_Name":"https://images.unsplash.com/photo-1473624566182-509e437512f4?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=54c3ec6d1fee824d62e6fa76676ddf17"]
]
var methodStart = Date()
#IBOutlet weak var mycall: UICollectionView!
var selectedIndex=[Int]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.players.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:playerCell = collectionView.dequeueReusableCell(withReuseIdentifier: "playerCell", for: indexPath) as! playerCell
let url = URL(string:self.players[indexPath.row]["image_Name"]!)
if indexPath.row == 0 {
methodStart = Date()
}
Alamofire.request(url!).responseImage { response in
// debugPrint(response)
// print(response.request)
// print(response.response)
// debugPrint(response.result)
if let image = response.result.value {
cell.playerImage.image = image
}
}
if indexPath.row == self.players.count - 1 {
let methodFinish = NSDate()
let executionTime = methodFinish.timeIntervalSince(methodStart as Date)
print("Execution time: \(executionTime)")
}
return cell
}
}
Problem in your code -
you said you want those 6 images to be downloaded at the same time and when user scrolls the 5th and 6th image should display immediately. but this is not happening because the cellForItemAtIndexPath function for the 5th and 6th cell called when you scroll. this is the default behavior and you cannot invoke this before user scrolled.
Quick fix solution -
Download all the images outside cellForItemAtIndexPath function, like in either viewDidLoad or viewDidAppear by looping the array of players and then reload the collection view after completion of download of the last one.
Permanent and best solution -
Implement lazy loading and caching of images using either Heneke or SDWebImage to display image in cells. And use prefetch option in those libraries to fetch images before they were shown in cells. To use prefetch option you need to loop through the array and pass the urls to the fetching and storing functions of the library and just load image in cell according to the syntax of that library and it will manage the displaying of image when it downloaded and you don't have to worried about that.
When you are dealing with network calls, you should update UI in main queue. One more valid point i want to add here is Alamofire has a special library for image download/resize. Please have a look at AlamofireImage Library
Alamofire.request(url!).responseImage { response in
if let image = response.result.value {
DispatchQueue.main.async {
cell.playerImage.image = image
cell.playerImage.setNeedsLayout()
}
}
}
When I open my application it asks me for permission to access my photos. That is all all right. But after accepting, the screen turns black and shows no photos and in my console it says that it has found no photos. Although I have photos on my device.
import UIKit
import Photos
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var imageArray = [UIImage]()
override func viewDidLoad() {
grabPhotos()
}
func grabPhotos(){
let imgManager = PHImageManager.defaultManager()
let requestOptions = PHImageRequestOptions()
requestOptions.synchronous = true
requestOptions.deliveryMode = .HighQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"CreationDate" , ascending: false)]
if let fetchResult : PHFetchResult = PHAsset.fetchAssetsWithMediaType(.Image, options: fetchOptions){
if fetchResult.count > 0 {
for i in 0..<fetchResult.count{
imgManager.requestImageForAsset(fetchResult.objectAtIndex(i) as! PHAsset, targetSize: CGSize(width: 200, height: 200), contentMode: .AspectFill, options: requestOptions, resultHandler: {
image, error in
self.imageArray.append(image!)
})
}
}
else{
print("You have got not photos!")
self.collectionView?.reloadData()
}
}
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
let imageView = cell.viewWithTag(1) as! UIImageView
imageView.image = imageArray[indexPath.row]
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let width = collectionView.frame.width / 3 - 1
return CGSize(width: width, height: width)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1.0
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1.0
}
}
Most likely the problem here is that you're asking for assets only once. The first time you ask, you get nothing. When you call PHAsset.fetchAssetsWithMediaType, it does two things at the same time: It asks the user for authorization, and because authorization hasn't been granted yet (the authorization prompt is still on screen), it returns an empty fetch result. (And because the fetch result is empty, you can't
If that's the problem, you should see a non-empty fetch result the second time you run your app (because authorization is a one-time thing and persists across launches).
There are a few ways to deal with this...
One is simply to request authorization directly first using requestAuthorization() and fetch assets only after that call's completion handler fires (with a positive result).
Another is to register() your view controller (or some other controller class of yours) as an observer of the Photos library before performing a fetch. Your first fetch before authorization still returns an empty result, but as soon as the user has authorized your app your observer gets a photoLibraryDidChange() call from which you can access the full, "real" fetch results.
Beyond that, there are some other things you're doing that aren't best practice here...
You're requesting images from the PHImageManager for every asset in the library, regardless of whether the user is going to see them. Consider the case of someone whose iCloud Photo Library contains tens of thousands of assets — only the first 30 or so are going to fit on your collection view at once, and the user may never scroll all the way to the end, but you're still fetching thumbnails for Every Image Ever. (And in the case of iCloud assets not on the local device, trigging network activity for lots of thumbnails the user might never see.)
You're getting thumbnails from PHImageManager synchronously and sequentially. Once you get the authorization issue dealt with, your app will probably crash because your for i in 0..<fetchResult.count { imgManager.requestImageForAsset loop blocks the main thread for too long.
Apple provides canonical example code that demonstrates how to use the Photos app to do things like displaying a collection view full of thumbnails. Take a look at AssetGridViewController.swift in that project to see some of the things they're doing:
use PHCachingImageManager to prefetch batches of thumbnails based on the visible area of a collection view
use PHPhotoLibraryObserver to react to changes in a fetch result, including animated collection view updates
handling the interaction between asynchronous fetches and the collection view data source
inserting new assets in the Photos library
1) Subclass a UICollectionViewCell:
import ...
//put this e.g. to the top of your swift file, right after import statements
class PhotoViewCell: UICollectionViewCell {
#IBOutlet weak var myImageView: UIImageView!
}
2) Create a prototype for you cell:
2.1 design it (you may have done before)
2.2 enter an identifier
2.3 enter a class
2.4 CTRL drag from storyboard: from the cell's imageView to the #IBOutlet line (from the step 1)
3) Update your cellForItem method:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! PhotoViewCell {
cell.myImageView.image = imageArray[indexPath.row]
return cell
}
use a class for cell , put imageview in your cell make it an outlet
now write this
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! 'your class name for cell'
cell.Image.image = imageArray[indexPath.row]
return cell
}
I'm using firebase realtime database and it's working fine for some parts of my app. I was going through a tutorial on youtube which populates a collectionView with users. It uses NSDictionary to get the photo URL and username and puts them in the collection view for all users. I deleted some of the users directly in the firebase console, and now have only one user. For some reason it's still pulling the users that I deleted. This is the collectionView file.
import UIKit
import FirebaseDatabase
private let reuseIdentifier = "UserSearchCell"
class UsersCollectionViewController: UICollectionViewController {
var usersDict = NSDictionary?()
var userNamesArray = [String]()
var userImagesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
DataService.ds.REF_USERS.observeEventType(.Value, withBlock :{
(snapshot) in
self.usersDict = snapshot.value as? NSDictionary
for(userId,details) in self.usersDict!{
let img = details.objectForKey("profileThumbUrl") as! String
let name = details.objectForKey("username") as! String
self.userImagesArray.append(img)
self.userNamesArray.append(name)
self.collectionView?.reloadData()
}
})
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.userImagesArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UserSearchCell
let imageUrl = NSURL(string:userImagesArray[indexPath.row])
let imageData = NSData(contentsOfURL: imageUrl!)
cell.userImage.image = UIImage(data:imageData!)
cell.userName.text = userNamesArray[indexPath.row]
return cell
}
}
Shouldn't this always sync with the database? Where are these deleted users coming from? Also I didn't include the cell code because all it is is the imageView and Label actions being declared. I ran into this problem before but my memory is bad and I don't remember why it was doing it or if I ever solved it.
Okay I figured out the answer to my own question. Apparently the simulator has a problem with firebase and seems to form some sort of cache of firebase data. I tried running it on a phone I had been testing on and it didn't have this cache and everything worked fine. Also I tried running on a different device IN the simulator and it worked fine there too. So I want to leave this up because I think a lot of people may have trouble with Firebase since it's core feature doesn't work well with the IOS simulator.