I'm using UICollectionView to show the images in my app.
The problem is that it takes very slow to show images. After 50 seconds, the images in collection view appears. :(
When I find the solution in google, mostly they write the following codes. But it is not work for me.
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
and
extension SeeAllCollectionView {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
debugPrint("seeAllLIStCell Count \(assetsTable.count)")
return assetsTable.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "seeAllListCell", for: indexPath) as! SeeAllPhotoCell
let list = assetsTable[(indexPath as NSIndexPath).row]
var imageName: String? = (list.poster_image_url)
var image: UIImage? = (images_cache[imageName!])
if image != nil {
debugPrint("Yes Image")
cell.imageView.image = image
} else{
debugPrint("NO Image")
cell.imageView.image = nil
DispatchQueue.main.async(){
let url = NSURL(string: list.poster_image_url)
let data = NSData(contentsOf:url! as URL)
var image = UIImage(data: data as! Data)
DispatchQueue.main.async(execute: {() -> Void in
cell.movieTitle.text = list.name
cell.imageView?.image = image
})
self.images_cache[imageName!] = image
}
}
return cell
}
}
// MARK: - UICollectionViewDelegate
extension SeeAllCollectionView {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
debugPrint("Selected")
let list = assetsTable[(indexPath as NSIndexPath).row]
debugPrint(list.poster_image_url)
debugPrint(list.name)
prefs.set(list.poster_image_url, forKey: "poster_image_url")
prefs.set(list.name, forKey: "name")
prefs.set(list.assets_id, forKey: "VIDEO_ID")
prefs.set(false, forKey: "FLAG")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "DetailsChannel") as UIViewController
self.present(vc, animated: true, completion: nil)
}
}
Here is my screenshot when I run the project. I got these many lines of codes when I run.
Please anyone help me how should I do?
I got the same console error when I am getting the data from the API call and reload the UITableView (as per my requirement). My issue is solved by using
DispatchQueue.global(qos: .background).async { // load data in back ground mode so that main thread can be safed.
let url = NSURL(string: list.poster_image_url)
let data = NSData(contentsOf:url! as URL)
var image = UIImage(data: data as! Data)
DispatchQueue.main.async(execute: {
cell.movieTitle.text = list.name
cell.imageView?.image = image
})
self.images_cache[imageName!] = image
}
Screen Shot of Error I got on my Console Before
You can give an attempt to SDWebimage All async Thread operations maintained well.
That can provide following advantages
Asynchronously download
Auto Purging Image Cache if memory warnings happen for the app
Image URL caching
Image Caching
Avoid Duplicate Downloads
You can directly use a single method in cell as below
[cell.storeImg sd_setImageWithURL:[NSURL URLWithString:strURL] placeholderImage:kDefaultImageForDisplay];
In your code following line should creates problem as that operation can happen on main thread
let data = NSData(contentsOf:url! as URL)
this line:
DispatchQueue.main.async()
upon downloading image is probably causing the error, it blocks the main thread and executes network requests on main (UI) queue, not to mention the fact that these requests are then performed serially (therefore slow). Try to change the snippet to:
DispatchQueue.global.async(qos: DispatchQoS.QoSClass.background){
let url = NSURL(string: list.poster_image_url)
let data = NSData(contentsOf:url! as URL)
var image = UIImage(data: data as! Data)
DispatchQueue.main.async(execute: {() -> Void in
where global stands for global queue default for such network operations.
EDIT: apart from using main queue for network calls there may be an issue with actually too many images loaded for rows which are not currently visible on the screen. If there are lot of them and connection is not-so-good you will end up with app pending downloads for onscreen cells. Consider lazy loading images only for onscreen cells - and cancelling downloads for rows which are not visible. There is a quite good tutorial (however for table view, but you can easily extend it for collection) in Swift on how to accomplish this.
Related
I've made a widget which fetches Codable data and it's working just fine in the simulator ONLY. The widget updates within 30 seconds or less after the data has changed. I've set a 5 minute update limit (I understand it's called far less frequently). It's working actually really great in the simulator without any kind of background data fetches and updates in less time than I set in getTimeline. Then I ran into an issue on a a real test device.
The data won't update anywhere between 2-10+ mins while testing a real device, in the snapshot it's updated and can see the new data changes but not in the widget on springboard. I don't understand why the simulator works just fine but not a real device. The Widget is definitely being updated when the data changes but only in the Simulator so am I suppose to fetch data in the background?
I've come across this Keeping a Widget Up To Date | Apple Developer Documentation. I'm still very new to Swift and SwiftUI so this is a little bit harder for me to grasp. I'm trying to understand the section Update After Background Network Requests Complete to update my Codeable data. My guess is the simulator is different from a real device and I need to fetch data in the background for the must up to date data?
The end goal is to have the widget update as frequently as possible with the most current data. I'm not sure I even need the background data fetch?
My data model for my widget as an example (which is working fine)
class DataModel {
var data: DataClass = DataClass(results: []))
func sessions(_ completion: #escaping (DataClass -> Void) {
guard let url = URL(string: "URL HERE") else { return }
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(DataClass.self, from: data) {
self.data = response
completion(self.data)
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
}
}
}
.resume()
}
}
My getTimeline calling the data model
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let model = DataModel()
var entries: [SimpleEntry] = []
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let entry = SimpleEntry(date: entryDate, model: model)
entries.append(entry)
model.sessions {_ in
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have this for my background network request
import Foundation
import WidgetKit
class BackgroundManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
var completionHandler: (() -> Void)? = nil
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "widget-bundleID")
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func update() {
let task = urlSession.downloadTask(with: URL(string: "SAME URL FROM DATA MODEL HERE")!)
task.resume()
}
func urlSession(_ session: URLSession ,downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print (location)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
self.completionHandler!()
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
print("Background update")
}
}
Then in my Widget I set .onBackgroundURLSessionEvents(). I never see any background updates or errors in the console. This seems very wrong, the Codable data will never be updated? How do I properly update my data in the background?
struct Some_Widget: Widget {
let kind: String = "Widget"
let backgroundData = BackgroundManager()
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SomeWidget_WidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget")
.description("Example widget.")
.onBackgroundURLSessionEvents { (sessionIdentifier, completion) in
if sessionIdentifier == self.kind {
self.backgroundData.update()
self.backgroundData.completionHandler = completion
print("background update")
}
}
}
}
The video I'm playing does not take the entire area of the UIView (named videoView), which has a gray color: iPhone 7 Plus Simulator Screenshot
Most of the answers claim that I need to either set the frame to bounds (of UIView) or set videoGravity to AVLayerVideoGravityResizeAspectFill. I've tried both, but for some reason it still does not fill the space entirely.
var avPlayer: AVPlayer!
var avPlayerLayer: AVPlayerLayer!
var paused: Bool = false
#IBOutlet weak var videoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let theURL = Bundle.main.url(forResource:"HOTDOG", withExtension: "mp4")
avPlayer = AVPlayer(url: theURL!)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayerLayer.frame = videoView.layer.bounds
videoView.layer.insertSublayer(avPlayerLayer, at: 0)
}
Any help will be appreciated. :)
After long time I found the answer.
Code below should be moved into viewDidAppear() like:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Resizing the frame
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayerLayer.frame = videoView.layer.bounds
videoView.layer.insertSublayer(avPlayerLayer, at: 0)
avPlayer.play()
paused = false
}
The layout was designed for iPhone SE (small screen), so when it was tested on a bigger screen the app was taking originally set size from the Auto-layout and shaping it according to that. By moving the code into viewDidAppear() the app resizes the window according to new constraints.
Just move the frame line avPlayerLayer.frame = videoView.layer.bounds into viewDidLayoutSubviews like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
avPlayerLayer.frame = videoView.layer.bounds
}
The rest should stick into the viewDidLoad function, just like you did.
I am working on a login view and trying to change the border color of a UITextField in Xcode/swift3 when validation of the textfield fails. The UITextField should get a red border color.
The problem is that if enter an email, then a password and then press the submit button, i have to focus email text field again before it gets a red border.
This is my LoginViewController.swift so far:
import Foundation
import UIKit
class LoginViewController : UIViewController, UITextFieldDelegate {
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
// login button action
#IBAction func loginButtonTabbed(_ sender: Any) {
// getting values from text fields
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
// set enpoind data
let requestURL = NSURL(string: Constants.apiUrl)
//creating a task to send the post request
var request = URLRequest(url: requestURL as! URL)
request.httpMethod = "POST"
let postString = "cmd=addUser&email="+userEmail!+"&password="+userPassword!
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
do {
let json = try? JSONSerialization.jsonObject(with: data, options: [])
// store json response to dictionary
if let dictionary = json as? [String: Any] {
// check if we got validation errors
if let nestedDictionary = dictionary["validation"] as? [String: Any] {
// display validation messages on device
if let emailMsg = nestedDictionary["Email"] as? String { // change color of textfield
self.userEmailTextField.errorField()
}
}
}
} catch let error as NSError {
print(error)
}
}
//executing the task
task.resume()
}
}
and the UITextField extension UITextField.swift:
import Foundation
import UIKit
extension UITextField {
func errorField(){
self.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
self.layer.borderWidth = 1.0;
}
}
When you're doing a network call, it always happens in the background...so in order to do any kind of UI updates you need to be on the main queue. Just put the self.userEmailTextField.errorField() inside DispatchQueue.main.async {...} so it would be done immediately.
Also haven't really tested your code very well. Why?
Even in your current code the border would still turn red, but it turns red after almost like 6-7 seconds (it could take less or more for you)...because it's being ran from background thread.
What I don't understand is why clicking on the textField again brings the red border right away!? Here's what I'm guessing happens:
From the background thread you update the model ie change the textField color which queues the UI/view to be updated...but since we're on a background queue, that UI updated could take a few seconds to happen
But then you tapped on the textField right away and forced a super quick read of the textField and all its properties which includes the border—from main thread (actual user touches are always handled through main thread)...which even though are not yet red on the screen, but since it's red on the model it will read from it and change color to red immediately.
I want to add a new To Do item when i press the add button,but i don't want to switch to another page.
press the add button to add a new row in the table view,input something and press the done button,it will save.
somebody suggests me to save the cells data to Model,but i don't know how to write this.
Who can help me?
import UIKit
import CoreData
class ToDoViewController: UIViewController {
var items: [NSManagedObject] = []
#IBOutlet weak var tableView: UITableView!
#IBAction func addItem(_ sender: UIBarButtonItem) {
//***How to write this code***
}
#IBAction func done(_ sender: UIBarButtonItem) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "ToDo", in: managedContext)!
let item = NSManagedObject(entity: entity, insertInto: managedContext)
//***let list = the current textView's text
//how to get the textView's text and assign it to a value.***
item.setValue(list, forKeyPath: "summary")
do {
try managedContext.save()
items.append(item)
} catch let error as NSError {
print("Could not save.\(error),\(error.userInfo)")
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self,forCellReuseIdentifier: "Cell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "ToDo")
do {
items = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch.\(error),\(error.userInfo)")
}
}
}
extension ToDoViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height))
cell.addSubview(textView)
textView.text = item.value(forKey: "summary") as? String
return cell
}
}
Ok so If my understanding is right you need a new row to be added if they create a new entry into your Core Data. So in your viewWillAppear you're doing a fetch. What I think you need is a:
var fetchResultController : NSFetchedResultsController<YourType>!
Then using this fetch controller you want to do the following when fetching:
private func GetToDoEntries(){
if let appDele = UIApplication.shared.deletgate as? AppDelegate{
let givenContext = appDele.persistantContainer.viewContex
let entryFetchRequest : NSFetchRequest<YourType> = YourType.fetchRequest()
let sortDescriptors = NSSortDescriptor(key: "yourEntrySortKey" , ascending: true)
entryFetchRequest.sortDescriptors = [sortDescriptors]
fetchResultController = NSFetchedResultsController(fetchRequest: entryFetchRequest, managedObjectContext: givenContext, sectionNameKeyPath: nil, cacheName: nil)
fetchResultController.delegate = self
do{
//Gets fetched data based on our criteria
try fetchResultController.performFetch()
if let fetchedEntries = fetchResultController.fetchedObjects{
items = fetchedEntries as? WhateverToCastTo
}
}catch{
print("Error when trying to find entries")
}
}
}
First I'm sorry but I've just written this here is stackOverflow. So what you're doing is using a fetch result controller instead of a traditional search. You are required to have the sort descriptor as well and you can try to get the results and cast them to your items or as a NSManagedObject.
Now we're not done though. Your script needs to inherit from some behaviour. At the top of your class
class ToDoViewController : UIViewController, NSFetchedResultsControllerDelegate
You need the delegate as you can see in the first block of code because we're assigning it. Now we're almost there. You just need some methods to update the table for you and these come with the delegate we just inherited from.
//Allows the fetch controller to update
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
//Allows new additions to be added to the table
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type{
case .insert:
if let _newIndexPath = newIndexPath{
tableView.insertRows(at: [_newIndexPath], with: .fade)
}
case .update:
if let index = indexPath{
tableView.reloadRows(at: [index], with: .fade)
}
default:
budgetEntryTable.reloadData()
}
if let fetchedObjects = controller.fetchedObjects{
items = fetchedObjects as! [NSManagedObject (Or your cast type)]
budgetEntryTable.reloadData()
}
}
//Ends the table adding
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
So there are 3 methods here. The first and second are very self explanatory. They begin and end the updates on your tableView. I'd also recommend that you change the name of your tableView to something other than "tableView" just for clarity.
The method in the middle uses a switch. My example is missing the "Move" and "Delete" options as I didn't required them in my project but you can add them to the switch statement.
The insert is checking the newIndexPath to see if there is one. If so then we add an array of the amount of rows required at the newIndexPath.
The update just checks the current index paths and then reloads the data on them incase you updated your data model.
I hope this is what you were looking for. Good luck! I'll try and help more if you need it but that should get you started.
After clicking on a accessoryButton in TableView, it will redirect me first to PrepareforSegue function and then to accessoryButtonTappedForRowWith Function which creating error in my code.
Please have a look into my code:
var indexPathAccessory: Int?
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
indexPathAccessory = indexPath.row
}
indexPathAccessory contains the value of row where Accessorybutton was clicked.
The second function is:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "Add" {
let nc = segue.destination as! UINavigatorController
controller = nc.topViewController as! AVC
//Comment 1
controller.name = span[indexPathAccessory]
//Comment 2
controller.delegate = self
}
}
I am getting error cause after clicking on accessorybutton, it is redirecting me to prepareforSegue function first and then to accessoryButtonTappedForRowWith and due to value of indexPathAccessory which still nil, I am getting this error
Will it be possible to jump first on accessoryButtonTappedForRowWith to get the value of indexPathAccessory and then to prepareForSegue?
if yes, problem will be solved.
I tried to add one test to return if indexPathAccessory is nil.
application run without error but not in a proper way:
I can see that controller.name is equal to "test", but when I am on AVC View controller, name became nil
Any advise?
One of the solution can be - remove the segue from the storyboard which causes the control going out of your hand and write the performSegue inside your accessoryButtonTappedForRowWith method.
Finally it worked by deleting the accessoryButtonTappedForRowWith function, and adding the following code to prepareforSegue functions:
if let indexPath = tableView.indexPath(for: sender as! UITableViewCell) {
controller.beamName = "Test"
indexSpanAccessory = indexPath.row
}