ViewDid Load of today Widget gets called every time - ios

I have implemented today widget for displaying recently recorded videos thumb.i am using collection view in Widget Storyboard file.
Its all working fine. I am able to get required thumbs from shared container and bind it to collection view as well. Now the issue is Whenever i look for the widget in Notification centre it calls ViewDidLoad of TodayWidgetViewController every time. Because of this collection view seems to be reloaded every time.
Is there a way to prevent this being reloaded every time?
Thanks in advance.
Below is the Source Code Implemented:
{
#IBOutlet weak var widgetCollection: UICollectionView!
#IBOutlet weak var recordButton: UIButton!
var mutArryListOfVideosImg: NSMutableArray = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
self.recordButton.layer.borderWidth = 2.0
self.recordButton.layer.borderColor = UIColor.white.cgColor
self.recordButton.layer.masksToBounds = true
self.recordButton.layer.cornerRadius = self.recordButton.frame.size.width / 2
self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded
//print("array count:\()")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.getVideoThumbImages()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func btnRecordAction(sender:UIButton)
{
let pjUrl = URL(string: "")
self.extensionContext?.open(pjUrl!, completionHandler: { (Bool) in
})
}
func getDataPath() -> URL
{
let appGroupId = “”
let appGroupDirectoryPath:URL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
let newDirectory = appGroupDirectoryPath.appendingPathComponent("VideoThumbs")
return newDirectory
}
func getVideoThumbImages()
{
let videoDirectoryPath = self.getDataPath()
if let Images = try? FileManager.default.contentsOfDirectory(atPath: videoDirectoryPath.path) as [String]
{
if((Images.count) > 0)
{
let sortedImages = (Images.sorted(by: backward)) as [String]
//print("Sorted Array:\(sortedImages)")
if((sortedImages.count) > 0)
{
for (index,element) in (sortedImages.enumerated()) {
print("\(index) = \(element)")
mutArryListOfVideosImg.add(element)
}
if(mutArryListOfVideosImg.count > 0)
{
widgetCollection.reloadData()
}
}
}
}
}
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
func getDataFromUrl(url: URL, completion: #escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) {
URLSession.shared.dataTask(with: url) {
(data, response, error) in
completion(data, response, error)
}.resume()
}
//MARK: CollectionView Delegate and Datasource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if mutArryListOfVideosImg.count > 3
{
return 3
}
else
{
return mutArryListOfVideosImg.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "WidgetCollectionCell", for: indexPath as IndexPath) as! WidgetCollectionCell
cell.imgVideoThumbImage.layer.cornerRadius = 8.0
cell.imgVideoThumbImage.layer.masksToBounds = true
let directoryPath = self.getDataPath()
let url = directoryPath.appendingPathComponent(mutArryListOfVideosImg.object(at: indexPath.item) as! String)
getDataFromUrl(url: url) { (data, response, error) in
guard let data = data, error == nil else { return }
//print(response?.suggestedFilename ?? url.lastPathComponent)
//print("Download Finished")
DispatchQueue.main.async() { () -> Void in
cell.imgVideoThumbImage.image = UIImage(data: data)
}
}
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
print("Widget thumb tapped....")
let pjUrl = URL(string: “CallBack://edit-\(mutArryListOfVideosImg.object(at: indexPath.item))")
self.extensionContext?.open(pjUrl!, completionHandler: { (Bool) in
})
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: (self.widgetCollection.frame.size.width/3) - 10, height: 70)
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
completionHandler(NCUpdateResult.newData)
}
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
if (activeDisplayMode == NCWidgetDisplayMode.compact) {
self.preferredContentSize = maxSize
}
else {
self.preferredContentSize = CGSize(width: maxSize.width, height: 120)
}
}
}

viewWillAppear and viewDidAppear will be called every time your widget is displayed.
About viewDidLoad seems strange, are you sure about it? Anyway this should not happen, it's not normal, try to check this method:
func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) {
// you could call here a custom function to update your widget
completionHandler(NCUpdateResult.NewData)
}
Your extension probably has an error and everytime view appears it is being called again.
Hope it helps.
Update: (after your corrections)
Seems your widget crash or not loaded due to size problems.
In viewWillAppear try to launch this method:
func setPreferredContentSize() {
var currentSize: CGSize = self.preferredContentSize
currentSize.height = 190.0
self.preferredContentSize = currentSize
}

Related

Why are 2 network calls using the same search string required before the UICollectionView decides there is data that needs to be refreshed?

I have a strange problem in a UIViewController that the user uses to search for a GIF.
There are essentially 2 issues:
The user has to enter the same search term twice before the UICollectionView triggers the cellForRowAt data source method after a call is made to reloadData().
After you enter the search term the first time, heightChanged() is called, but the self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height comes back as 0 even though I've confirmed that data is being received back from the server. The second time you enter the search term, the height is a non-zero value and the collection view shows the cells.
Here is an example of how I have to get the data to show up:
Launch app, go this UIViewController
Enter a search term (ie. "baseball")
Nothing shows up (even though reloadData() was called and new data is in the view model.)
Delete a character from the search term (ie. "basebal")
Type in the missing character (ie. "baseball")
The UICollectionView refreshes via a call to reloadData() and then calls cellForRowAt:.
Here is the entire View Controller:
import UIKit
protocol POGIFSelectViewControllerDelegate: AnyObject {
func collectionViewHeightDidChange(_ height: CGFloat)
func didSelectGIF(_ selectedGIFURL: POGIFURLs)
}
class POGIFSelectViewController: UIViewController {
//MARK: - Constants
private enum Constants {
static let POGIFCollectionViewCellIdentifier: String = "POGIFCollectionViewCell"
static let verticalPadding: CGFloat = 16
static let searchBarHeight: CGFloat = 40
static let searchLabelHeight: CGFloat = 24
static let activityIndicatorTopSpacing: CGFloat = 10
static let gifLoadDuration: Double = 0.2
static let gifStandardFPS: Double = 1/30
static let gifMaxDuration: Double = 5.0
}
//MARK: - Localized Strings
let localizedSearchGIFs = PALocalizedStringFromTable("RECOGNITION_IMAGE_SELECTION_GIF_BODY_TITLE", table: "Recognition-Osiris", comment: "Search GIFs") as String
//MARK: - Properties
var viewModel: POGIFSearchViewModel?
var activityIndicator = MDCActivityIndicator()
var gifLayout = PAGiphyCellLayout()
var selectedGIF: POGIFURLs?
//MARK: - IBOutlet
#IBOutlet weak var GIFCollectionView: UICollectionView!
#IBOutlet weak var searchGIFLabel: UILabel! {
didSet {
self.searchGIFLabel.text = self.localizedSearchGIFs
}
}
#IBOutlet weak var searchField: POSearchField! {
didSet {
self.searchField.delegate = self
}
}
#IBOutlet weak var activityIndicatorContainer: UIView!
//MARK: - Delegate
weak var delegate: POGIFSelectViewControllerDelegate?
//MARK: - View Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
self.setupActivityIndicator(activityIndicator: self.activityIndicator, activityIndicatorContainer: self.activityIndicatorContainer)
self.viewModel = POGIFSearchViewModel(data: PAData.sharedInstance())
if let viewModel = self.viewModel {
viewModel.viewDelegate = self
viewModel.viewDidBeginLoading()
}
self.gifLayout.delegate = self
self.gifLayout.isAXPGifLayout = true;
self.GIFCollectionView.collectionViewLayout = self.gifLayout
self.GIFCollectionView.backgroundColor = .orange
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// This Patch is to fix a bug where GIF contentSize was not calculated correctly on first load.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {
self.viewModel?.viewDidBeginLoading()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.heightChanged()
}
//MARK: - Helper Methods
func heightChanged() {
guard let delegate = self.delegate else { return }
let height = self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height + Constants.verticalPadding * 3 + Constants.searchLabelHeight + Constants.searchBarHeight + activityIndicatorContainer.frame.size.height + Constants.activityIndicatorTopSpacing
print("**** Items in Collection View -> self.viewModel?.gifModel.items.count: \(self.viewModel?.gifModel.items.count)")
print("**** self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height: \(self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height); height: \(height)")
delegate.collectionViewHeightDidChange(height)
}
func reloadCollectionView() {
self.GIFCollectionView.collectionViewLayout.invalidateLayout()
self.GIFCollectionView.reloadData()
self.GIFCollectionView.layoutIfNeeded()
self.heightChanged()
}
func imageAtIndexPath(_ indexPath: IndexPath) -> UIImage? {
guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else { return nil }
var loadedImage: UIImage? = nil
let imageManager = SDWebImageManager.shared()
imageManager.loadImage(with: previewURL, options: .lowPriority, progress: nil) { (image: UIImage?, data: Data?, error: Error?, cacheType: SDImageCacheType, finished: Bool, imageURL: URL?) in
loadedImage = image
}
return loadedImage
}
func scrollViewDidScrollToBottom() {
guard let viewModel = self.viewModel else { return }
if viewModel.viewDidSearchMoreGIFs() {
self.activityIndicator.startAnimating()
} else {
self.activityIndicator.stopAnimating()
}
}
}
extension POGIFSelectViewController: POSearchFieldDelegate {
func searchFieldTextChanged(text: String?) {
guard let viewModel = self.viewModel else { return }
viewModel.viewDidSearchGIFs(withSearchTerm: text)
}
}
extension POGIFSelectViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("**** CELL FOR ROW AT -> self.viewModel?.gifModel.items.count: \(self.viewModel?.gifModel.items.count)")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.POGIFCollectionViewCellIdentifier, for: indexPath) as! POGIFCollectionViewCell
guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else {
return cell
}
var cellState: POGIFCollectionViewCell.CellState = .dimmedState
if self.selectedGIF == nil {
cellState = .defaultState
} else if (self.selectedGIF?.previewGIFURL?.absoluteString == previewURL.absoluteString) {
cellState = .selectedState
}
cell.setupUI(withState: cellState, URL: previewURL) { [weak self] () in
UIView.animate(withDuration: Constants.gifLoadDuration) {
guard let weakSelf = self else { return }
weakSelf.GIFCollectionView.collectionViewLayout.invalidateLayout()
}
}
if cell.GIFPreviewImageView.animationDuration > Constants.gifMaxDuration {
cell.GIFPreviewImageView.animationDuration = Constants.gifMaxDuration
}
cell.backgroundColor = .green
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let viewModel = self.viewModel else { return 0 }
return viewModel.gifModel.items.count
}
}
extension POGIFSelectViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let selectedGIF = self.viewModel?.gifModel.items[indexPath.row],
let delegate = self.delegate else {
return
}
self.selectedGIF = selectedGIF
delegate.didSelectGIF(selectedGIF)
self.reloadCollectionView()
}
}
extension POGIFSelectViewController: POGIFSearchViewModelToViewProtocol {
func didFetchGIFsWithSuccess() {
self.activityIndicator.stopAnimating()
print("**** didFetchGIFsWithSuccess() -> about to reload collection view")
self.reloadCollectionView()
}
func didFetchGIFsWithError(_ error: Error!, request: PARequest!) {
self.activityIndicator.stopAnimating()
}
}
extension POGIFSelectViewController: PAGiphyLayoutCellDelegate {
func heightForCell(givenWidth cellWidth: CGFloat, at indexPath: IndexPath!) -> CGFloat {
guard let image = self.imageAtIndexPath(indexPath) else {
return 0
}
if (image.size.height < 1 || image.size.width < 1 || self.activityIndicator.isAnimating) {
return cellWidth
}
let scaleFactor = image.size.height / image.size.width
let imageViewToHighlightedViewSpacing: CGFloat = 4 // this number comes from 2 * highlightedViewBorderWidth from POGIFCollectionViewCell
return cellWidth * scaleFactor + imageViewToHighlightedViewSpacing
}
func heightForHeaderView() -> CGFloat {
return 0
}
}
You'll see that the heightChanged() method calls a delegate method. That method is in another UIViewController:
func collectionViewHeightDidChange(_ height: CGFloat) {
self.collectionViewHeightConstraint.constant = height
}
So, I can't figure out why I need to either delete a character from the search term and re-add it in order for the data to refresh even though the very first call populated the view model with new data.
It's bizarre. Please help.

How to cache and retrieve Feed data from Firebase Storage in swift?

So I'm making an app where one of the view has a canvas view where you can draw images and upload to Firebase Storage image database (under a folder named Feed Images), and in the other view you get all the images that was shared by others as well as you. So first thing I gotta do is ask Firestore to get me the list of all items from the child Folder and then use those URL's from the result array it provided me to download the images.
My problem is most of the solutions here tell how to cache on the assumption that you have the urls to images but how do you cache the array of images when you do not have their urls such as in this case?
Also I would like to know a better way to download all files from my 'FeedImages' folder and then listen to my folder for changes and Update the feed accordingly and Cache!!! those Images.
Here is my code for my Feed View Controller:
import UIKit
import Firebase
let imageCache = NSCache<AnyObject, AnyObject>()
class FeedVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var folderList: [StorageReference]?
var storage = Storage.storage()
var downloadedImages : [UIImage] = []
let cellId = "FeedCell"
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.collectionView.reloadData()
self.storage.reference().child("FeedImages").listAll(completion: {
(result,error) in
print("result is \(result.items)")
self.folderList = result.items
DispatchQueue.main.async {
self.folderList?.forEach({ (refer) in
self.load(storageRef: refer)
})
self.collectionView.reloadData()
}
})
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.backgroundColor = .offWhite
collectionView.delegate = self
// collectionView.datasource = self
collectionView.register(FeedCell.self, forCellWithReuseIdentifier: cellId)
}
//
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return downloadedImages.count ?? 5
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FeedCell
if(downloadedImages.count > 0){
if(downloadedImages[indexPath.row] != nil){
print("Loading downloaded Image....")
cell.img.image = downloadedImages[indexPath.row]
}
}
else{
cell.img.image = UIImage(named: "Placeholder")
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (view.frame.width / 3) - 20, height: 100)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10 )
}
func load(storageRef: StorageReference) {
storageRef.downloadURL(completion: {(downloadURL,error) in
// retrieves image if already available in cache
if let imageFromCache = imageCache.object(forKey: downloadURL as AnyObject) as? UIImage {
if(self.downloadedImages.count == 0){
self.downloadedImages.append(imageFromCache)
}
return
}
if(error != nil){
print(error.debugDescription)
return
}
// print("url is \(downloadURL!)")
print("Download Started")
self.getData(from: downloadURL!) { data, response, error in
guard let data = data, error == nil else { return }
// print(response?.suggestedFilename ?? downloadURL!.lastPathComponent)
print("Download Finished")
DispatchQueue.main.async() { [weak self] in
let imageToCache = UIImage(data: data)
self!.downloadedImages.append(imageToCache!)
imageCache.setObject(imageToCache as AnyObject, forKey: downloadURL as AnyObject)
self?.collectionView.reloadData()
}
}
})
}
func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
class FeedCell : UICollectionViewCell{
var img = UIImageView()
override init(frame: CGRect) {
super.init(frame : frame)
Setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func Setup(){
self.layer.cornerRadius = 25
self.contentView.addSubview(img)
img.image = UIImage(named: "Placeholder")
img.translatesAutoresizingMaskIntoConstraints = false
img.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
img.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
img.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
img.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
}
}

Swift4 - Pagination

I'm new in developing ios app. Still dont know much. I successfully can display the articles well from first page only but fail to show other articles from other page when I scroll-up to view more. I really need help if somebody knows how to do pagination from my collection View. Below is the picture of my api that I use.
And below is my code where i get the data from api -- (Artikel.swift)
import Foundation
import Alamofire
import SwiftyJSON
import os.log
struct Artikel: ServiceCompletionHandler {
static func getCategories (completionHandler: #escaping ArrayCompletionHandler) {
let headers: HTTPHeaders = [
"Accept": "application/json"
]
let parameters: Parameters = [
"secret_key": Config.api_key
]
Alamofire.request("\(Config.server_address)/article-categories", method: .post, parameters: parameters, headers: headers)
.responseJSON { (response) -> Void in
switch response.result {
case .success:
let json = JSON(response.result.value!)
if json["data"].exists() {
if let data = json["data"]["data"].array {
completionHandler(data, nil)
} else {
completionHandler([], nil)
}
} else {
completionHandler([], nil)
}
case .failure(let error):
print(error)
completionHandler([], "Network error has occured.")
}
}
}
static func getArticles(_ category_id: Int, completionHandler: #escaping ArrayCompletionHandler) {
let headers: HTTPHeaders = [
"Accept": "application/json",
"secret-key": Config.api_key]
let value: Int = category_id
let newcategory = String(describing: value)
// var nextpages = NewsSectionViewController.url
let new_api = Config.server_addforarticle + newcategory
Alamofire.request(new_api, method: .get, headers: headers)
.responseJSON { (response) -> Void in
switch response.result {
case .success:
let json = JSON(response.result.value!)
// dump(json, name: "testing")
if let articles = json["articles"]["data"].array {
completionHandler(articles, nil)
}else{
completionHandler([], nil)
}
case .failure(let error):
print(error)
completionHandler([], "Network error has occured.")
}
}
}
}
and here where i display my articles. But yeah, I cant do pagination it stuck to 4-5 articles only. I just dont have any idea how to do what I did in my android version.
NewsSectionViewController.swift
import UIKit
import SwiftyJSON
import AlamofireImage
protocol NewsSectionViewControllerDelegate {
func updateArrowPosition()
}
class NewsSectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
// MARK: - Variables
var itemIndex: Int = 0
var category:JSON!
var stories:[JSON] = []
var delegate: NewsSectionViewControllerDelegate?
// MARK: - Views
#IBOutlet var titleLabel: UILabel!
#IBOutlet var collectionView: UICollectionView!
#IBOutlet var loadingIndicator: UIActivityIndicatorView!
// MARK: - Constraint
#IBOutlet var titleTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.titleLabel.text = category["name"].stringValue.uppercased()
self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 114, 0)
if #available(iOS 11.0, *) {
if ((UIApplication.shared.keyWindow?.safeAreaInsets.top)! > CGFloat(0.0)) {
self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 173, 0)
self.collectionView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, 173, 0)
}
}
self.loadingIndicator.startAnimating()
Artikel.getArticles(category["id"].intValue) {
(articles, error) in
self.loadingIndicator.stopAnimating()
if error == nil {
self.stories = articles
self.collectionView.reloadData()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Collection View
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.stories.count - 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell: NewsItemCollectionViewCell?
let item = stories[indexPath.row + 1]
if indexPath.row == 0 {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BigCell", for: indexPath) as? NewsItemCollectionViewCell
} else {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SmallCell", for: indexPath) as? NewsItemCollectionViewCell
}
cell!.titleLabel.text = item["title"].stringValue
if let thumbUrlString = item["banner_url_large"].string {
if let thumbUrl = URL(string: thumbUrlString) {
cell?.coverImageView.af_setImage(withURL: thumbUrl)
}
}
let wpDateFormatter = DateFormatter()
wpDateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dayDateFormatter = DateFormatter()
dayDateFormatter.dateStyle = .medium
dayDateFormatter.doesRelativeDateFormatting = true
let date = wpDateFormatter.date(from: item["date_publish_web"].stringValue)!
cell!.timestampLabel.text = dayDateFormatter.string(from: date)
return cell!
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let story = self.stories[indexPath.row + 1]
let newsContents = self.storyboard?.instantiateViewController(withIdentifier: "NewsContent") as! NewsContentViewController
newsContents.story = story
newsContents.title = self.category["name"].string
self.navigationController?.pushViewController(newsContents, animated: true)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.row == 0 {
let width = UIScreen.main.bounds.width - 30
return CGSize(width: width, height: 385 - 20)
} else {
let width = (UIScreen.main.bounds.width - 45) / 2
return CGSize(width: width, height: 210)
}
}
// MARK: - ScrollView
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.collectionView {
self.titleTopConstraint.constant = 30 - scrollView.contentOffset.y
}
if let delegate = self.delegate {
delegate.updateArrowPosition()
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Any help I really appreciated and thank you in advance :)
In NewsSectionViewController:
1) add global variables:
a) var nextPageUrl: String? ;
b) let numberOfPagination = 5 (5 it's a number of items in one url page);
c) add variable hasMoreItems
var hasMoreItems: Bool {
get {
guard let count = nextPageUrl?.count, count > 0 else {
return false
}
return true
}
}
d) add variable numberOfItems
var numberOfItems: Int {
get {
return secrets.count
}
}
2) change this line self.stories = articles to self.stories += articles
3) add function for pagination:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if (indexPath.item + 1 == numberOfItems) && ((indexPath.item + 1) % numberOfPagination == 0) {
if self.presenter.hasMoreItems {
self.loadData()
}
}
}
f) add func loadData()
func loadData() {
Artikel.getArticles(category["id"].intValue, nextPageUrl: nextPageUrl) {
(articles, error) in
self.loadingIndicator.stopAnimating()
if error == nil {
self.stories = articles
self.collectionView.reloadData()
}
}
}
In Artikel:
1) change getArticles function
static func getArticles(_ category_id: Int, nextPageUrl: String?, completionHandler: #escaping ArrayCompletionHandler) {
//...
let new_api = nextPageUrl ?? Config.server_addforarticle + newcategory
//...
}
2) in this function you can return nextPageUrl value in ArrayCompletionHandler

Freezes when scrolling UICollectionView

I'm developing photo editor app, but I have a problem with render filtered images in UICollectionView. I'm using Operation and OperationQueue. When I start scrolling the collectionView, filtered images are being updated. How can I fix it?
ImageFiltration:
class ImageFiltration: Operation {
private let image: CIImage
private let filterName: String!
var imageWithFilter: CIImage?
init(image: CIImage, filterName: String) {
self.image = image
self.filterName = filterName
}
override func main() {
if self.isCancelled {
return
}
if let name = filterName {
let filter = CIFilter(name: name, withInputParameters: [kCIInputImageKey: image])
imageWithFilter = filter?.outputImage!
}
} }
class PendingOperation {
lazy var filtrationInProgress = [IndexPath: Operation]()
lazy var filtrationQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "Filtration Operation"
return queue
}() }
UIScrollViewDelegate:
extension PhotoEditorViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
presenter.suspendAllOperations()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
presenter.loadImagesForOnScreenCells(collectionView: filtersToolsView.collectionView) { (indexPath) in
self.filtersToolsView.collectionView.reloadItems(at: [indexPath])
}
presenter.resumeAllOperations()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
presenter.loadImagesForOnScreenCells(collectionView: filtersToolsView.collectionView) { (indexPath) in
self.filtersToolsView.collectionView.reloadItems(at: [indexPath])
}
presenter.resumeAllOperations()
} }
UICollectionView Data Source:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: PhotoEditorFilterCollectionViewCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
cell.filteredImage.contentMode = .scaleAspectFill
presenter.startFiltration(indexPath: indexPath) { (image, filterName) in
cell.filteredImage.inputImage = image
cell.filterNameLabel.text = filterName
}
return cell
}
Implementation start filtration method:
func startFiltration(indexPath: IndexPath, completion: #escaping (CIImage?, String) -> ()) {
if let _ = pendingOperations.filtrationInProgress[indexPath] {
return
}
let filteredImage = ImageFiltration(image: model.image,
filterName: model.image.filters[indexPath.row].filterName)
filteredImage.completionBlock = {
if filteredImage.isCancelled {
return
}
DispatchQueue.main.async {
self.pendingOperations.filtrationInProgress.removeValue(forKey: indexPath)
completion(filteredImage.imageWithFilter, self.model.image.filters[indexPath.row].filterDisplayName)
}
}
pendingOperations.filtrationInProgress[indexPath] = filteredImage
pendingOperations.filtrationQueue.addOperation(filteredImage)
}
Operation methods:
func suspendAllOperations() {
pendingOperations.filtrationQueue.isSuspended = true
}
func resumeAllOperations() {
pendingOperations.filtrationQueue.isSuspended = false
}
func loadImagesForOnScreenCells(collectionView: UICollectionView,
completion: #escaping (IndexPath) -> ()) {
let pathsArray = collectionView.indexPathsForVisibleItems
let allPendingOperations = Set(Array(pendingOperations.filtrationInProgress.keys))
let visiblePaths = Set(pathsArray as [IndexPath])
var toBeCancelled = allPendingOperations
toBeCancelled.subtract(visiblePaths)
var toBeStarted = visiblePaths
toBeStarted.subtract(allPendingOperations)
for indexPath in toBeCancelled {
if let pendingFiltration = pendingOperations.filtrationInProgress[indexPath] {
pendingFiltration.cancel()
}
pendingOperations.filtrationInProgress.removeValue(forKey: indexPath)
}
for indexPath in toBeStarted {
let indexPath = indexPath as IndexPath
completion(indexPath)
}
}
I'm rendering a picture in GLKView.
Video
On scrollViewDidScroll cancel all of your requests. And in end scrolling get visible cells and start your operation queue. This way it will only use your visible cell memory and UI will not stuck.
See this tutorial it's same as your requirement - https://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift

Cells reload wrong images when scrolled in UI Collection View

I'm downloading images using DropBox's API and displaying them in a Collection View. When the user scrolls, the image either disappears from the cell and another is loaded or a new image is reloaded and replaces the image in the cell. How can I prevent this from happening? I've tried using SDWebImage, this keeps the images in the right order but still the images disappear and reload each time they are scrolled off screen. Also, I'm downloading the images directly, not from a URL, I'd prefer to not have to write a work-a-round to be able to use SDWebImage.
I'd post a gif as example but my reputation is too low.
Any help would be welcomed :)
var filenames = [String]()
var selectedFolder = ""
// image cache
var imageCache = NSCache<NSString, UIImage>()
override func viewDidLoad() {
super.viewDidLoad()
getFileNames { (names, error) in
self.filenames = names
if error == nil {
self.collectionView?.reloadData()
print("Gathered filenames")
}
}
collectionView?.collectionViewLayout = gridLayout
collectionView?.reloadData()
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
}
func getFileNames(completion: #escaping (_ names: [String], _ error: Error?) -> Void) {
let client = DropboxClientsManager.authorizedClient!
client.files.listFolder(path: "\(selectedFolder)", recursive: false, includeMediaInfo: true, includeDeleted: false, includeHasExplicitSharedMembers: false).response { response, error in
var names = [String]()
if let result = response {
for entry in result.entries {
if entry.name.hasSuffix("jpg") {
names.append(entry.name)
}
}
} else {
print(error!)
}
completion(names, error as? Error)
}
}
func checkForNewFiles() {
getFileNames { (names, error) in
if names.count != self.filenames.count {
self.filenames = names
self.collectionView?.reloadData()
}
}
}
func downloadFiles(fileName: String, completion:#escaping (_ image: UIImage?, _ error: Error?) -> Void) {
if let cachedImage = imageCache.object(forKey: fileName as NSString) as UIImage? {
print("using a cached image")
completion(cachedImage, nil)
} else {
let client = DropboxClientsManager.authorizedClient!
client.files.download(path: "\(selectedFolder)\(fileName)").response { response, error in
if let theResponse = response {
let fileContents = theResponse.1
if let image = UIImage(data: fileContents) {
// resize the image here and setObject the resized Image to save it to cache.
// use resized image for completion as well
self.imageCache.setObject(image, forKey: fileName as NSString)
completion(image, nil) // completion(resizedImage, nil)
}
else {
completion(nil, error as! Error?)
}
} else if let error = error {
completion(nil, error as? Error)
}
}
.progress { progressData in
}
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.filenames.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
cell.backgroundColor = UIColor.lightGray
let fileName = self.filenames[indexPath.item]
let cellIndex = indexPath.item
self.downloadFiles(fileName: fileName) { (image, error) in
if cellIndex == indexPath.item {
cell.imageCellView.image = image
print("image download complete")
}
}
return cell
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
gridLayout.invalidateLayout()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
imageCache.removeAllObjects()
}
Because TableView's and CollectionView's use the
dequeueReusableCell(withReuseIdentifier: for indexPath:) function when you configure a new cell, what swift does under the table is use a cell that is out of the screen to help the memory of your phone and probably that cell already has a image set and you have to handle this case.
I suggest you to look at the method "prepareCellForReuse" in this case what I think you have to do is set the imageView.image atribute to nil.
I have pretty sure that it will solve your problem or give you the right direction, but if it doesn't work please tell me and I will try to help you.
Best results.
I fixed it. It required setting the cell image = nil in the cellForItemAt func and canceling the image request if the user scrolled the cell off screen before it was finished downloading.
Here's the new cellForItemAt code:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let fileId = indexPath.item
let fileName = self.filenames[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
cell.backgroundColor = UIColor.lightGray
if cell.request != nil {
print("request not nil; cancel ", fileName)
}
cell.request?.cancel()
cell.request = nil
cell.imageCellView.image = nil
print ("clear image ", fileId)
self.downloadFiles(fileId:fileId, fileName: fileName, cell:cell) { (image, error) in
guard let image = image else {
print("abort set image ", fileId)
return
}
cell.imageCellView.image = image
print ("download/cache: ", fileId)
}
return cell
}
Use SDWebImage and add a placeholder image :
cell.imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
I post this in case it may help someone.
I have a collection view (displayed as a vertical list) whose items are collection views (displayed as horizontal single-line grids). Images in the child-collection views were repeated when the list was scrolled.
I solved it by placing this in the class of the cells of the parent collection view.
override func prepareForReuse() {
collectionView.reloadData()
super.prepareForReuse()
}

Resources