I building message app with meteor-iOS, everything is going great but I'm trying to make a load more feature but I can't maintain/save the position of scroll view(When I scroll to top I want to load more items but keep the scroll view in the same position as before the insert)
My Code:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if type == NSFetchedResultsChangeType.Insert {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItemsAtIndexPaths([newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItemsAtIndexPaths([indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Move {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
self.blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItemsAtIndexPaths([indexPath!])
}
})
)
}
}
self.collectionView!.performBatchUpdates({ () -> Void in
for operation: NSBlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepCapacity: false)
})
I'm using core data and meteor-ios. those anyone know what I can do? I also tried how to set UITableView's scroll position to previous location when click "load earlier items" button any many other without success :(
Thank you very much!!
Yep, I made if statement between range 20-50 to contentOffset.y. If it between those values so call loadMore() once.
Make bool value to save isLoadFinish and wait until the cell update. If you wouldn't do it the loadMore() will call multiple time.
For UX experience you have to save the old contentOffset.y before calling loadMore() and reset it after the load.
I will need to improve it a lot, simple hack for now.
Good luck.
Related
Let's say we have this pseudocode representing a network request call and show/hide an activity indicator, using RxSwift:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest()
.subscribe(onNext: {
self.isLoading.accept(false)
}, onError: {
self.isLoading.accept(false)
})
}
The function executeRequest returns either an Observable or Single.
I am not feeling comfortable with having to write twice the same code, for onNext/onSuccess and onError, basically doing the same.
I am looking for suggestions to minimize/improve turning off the activity indicator, like for example handling all events of the request in a single statement and avoid using the subscribe function. Or maybe there are other suggestions?
I use ActivityIndicator from RxSwift Example app, which makes it really convenient, especially if your loading multiple things in parallel as it maintains a count of running subscriptions and emit false only when this count is equal to 0:
let isLoading = ActivityIndicator()
func performRequests() {
self.network
.executeFirstRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
self.network
.executeSecondRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
}
You can use another method to subscribe, which passes Event in case of Observer or SingleEvent in case of Single:
subscribe(on: (Event<T>) -> Void)
subscribe(observer: (SingleEvent<T>) -> Void)
Observer Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
case .completed:
print("also good")
}
isLoading.accept(false)
}
}
Single Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
}
isLoading.accept(false)
}
}
I am using NSFetchedResultsController with UITableView, and my core-data class have field #NSManaged public var avatar_data: Data? , and image data is getting download when cell is visible(lazy loading). when download is completed and data is set into avatar_data field. it's trigger
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){
}
I Wish to ignore any changes for avatar_data field. notification should not trigger for particular field i.e avatar_data field.
problem is it's create cycle of reload tableview. i don't have download status for image. i simply want to ignore data change notification for this field.
extension UIImageView{
func downloadImageFrom(link:String, contentMode: UIViewContentMode, forContact:ContactDB, forCell:UserListTableViewCell) {
URLSession.shared.dataTask( with: NSURL(string:link)! as URL, completionHandler: {
(data, response, error) -> Void in
DispatchQueue.main.async() {
self.contentMode = contentMode
if let data = data {
self.image = UIImage(data: data)
forContact.avatar_thumb_data = data
//should not call didchange content of fetechresultcontroller
forContact.setPrimitiveValue(data, forKey: "avatar_thumb_data")
CoreDataInterface.sharedInstance.saveContext()
forCell.imageUserProfile?.backgroundColor = UIColor.clear
forCell.labelUserImage.text = ""
}
}
}).resume()
}
}
SetPrimitiveValue is applied after image download completed.
I'm trying to live search at my PHP API with Swift. Until now i've done this thing.
var filteredData = [Products]()
func getSearch(completed: #escaping DownloadComplete, searchString: String) {
let parameters: Parameters = [
"action" : "search",
"subaction" : "get",
"product_name" : searchString,
"limit" : "0,30"
]
Alamofire.request(baseurl, method: .get, parameters: parameters).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let result = responseData.result
if let dict = result.value as? Dictionary<String, AnyObject>{
if let list = dict["products_in_category"] as? [Dictionary<String, AnyObject>] {
if self.filteredData.isEmpty == false {
self.filteredData.removeAll()
}
for obj in list {
let manPerfumes = Products(productDict: obj)
self.filteredData.append(manPerfumes)
}
}
}
completed()
}
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
if (searchController.searchBar.text?.characters.count)! >= 3 {
self.getSearch(completed: {
self.searchResultTable.reloadData()
self.searchResultTable.setContentOffset(CGPoint.zero, animated: true)
}, searchString: searchController.searchBar.text!)
} else {
self.searchResultTable.reloadData()
}
}
}
And the table view is being updated with the filteredData.
How can i throttle the search so lets say when the user writes
"example" -> shows the results with example
then he erase the "le" ->
"examp" -> if the previous request is not completed, cancel it -> make request for "examp" and show the data in table view!
P.S. from another answer i found
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload), object: nil)
self.perform(#selector(self.reload), with: nil, afterDelay: 0.5)
}
func reload() {
print("Doing things")
}
Although if I try to replace "self.reload" with my function, I get an error
cannot convert value of type () to expected argument type selector
Use DispatchWorkItem with Swift 4 !
// Add a searchTask property to your controller
var searchTask: DispatchWorkItem?
// then in your search bar update method
// Cancel previous task if any
self.searchTask?.cancel()
// Replace previous task with a new one
let task = DispatchWorkItem { [weak self] in
self?.sendSearchRequest()
}
self.searchTask = task
// Execute task in 0.75 seconds (if not cancelled !)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.75, execute: task)
Hope it helps !
Your error was because you probably forgot the #selector() part.
Here's how it should look:
func searchBar() {
NSObject.cancelPreviousPerformRequests(withTarget: self,
selector: #selector(self.getSearch(completed:searchString:)),
object: nil)
perform(#selector(self.getSearch(completed:searchString:)),
with: nil, afterDelay: 0.5) }
You get the error because you didn't enclose your function in #selector
Now, as for the arguments, here's a function for that:
perform(#selector(getSearch:completion:searchString), with: <some completion>, with: "search string")
disclaimer: I am a writer.
Throttler could be the right tool to get it done.
You can do debounce and throttle without going reactive programming using Throttler like,
import Throttler
// advanced debounce, running a first task immediately before initiating debounce.
for i in 1...1000 {
Throttler.debounce {
print("debounce! > \(i)")
}
}
// debounce! > 1
// debounce! > 1000
// equivalent to debounce of Combine, RxSwift.
for i in 1...1000 {
Throttler.debounce(shouldRunImmediately: false) {
print("debounce! > \(i)")
}
}
// debounce! > 1000
Throttler also can do advanced debounce, running a first event immediately before initiating debounce that Combine and RxSwift don't have by default.
You could, but you may need a complex implementation yourself for that.
I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!
A UIView needs to change a warning label depending on the completion handler of a custom control:
voucherInputView.completionHandler = {[weak self] (success: Bool) -> Void in
self?.proceedButton.enabled = success
self?.warningLabel.alpha = 1.0
if success
{
self?.warningLabel.text = "Code you entered is correct"
self?.warningLabel.backgroundColor = UIColor.greenColor()
}
else
{
self?.warningLabel.text = "Code you entered is incorrect"
self?.warningLabel.backgroundColor = UIColor.orangeColor()
}
UIView.animateWithDuration(NSTimeInterval(1.0), animations:{ ()-> Void in
self?.warningLabel.alpha = 1.0
})
The final animation block shows an error in the form.
Cannot invoke 'animateWithDuration' with an argument list of type '(NSTimeInterval), animations: ()-> Void)'
If i call this somewhere outside of the completion closure it works.
The problem is that the closure is implicitly returning the result of this expression:
self?.warningLabel.alpha = 1.0
but the closure itself is declared as returning Void.
Adding an explicit return should resolve the problem:
UIView.animateWithDuration(NSTimeInterval(1.0), animations: { ()-> Void in
self?.warningLabel.alpha = 1.0
return
})
Antonio's solution also applies with nested closures, like doing an AFNetworking request within UITableViewRowAction handler.
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
let cleanRowAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Do Stuff", handler: {[weak self](action: UITableViewRowAction!, indexPath: NSIndexPath!) in
AFHTTPSessionManager(baseURL: NSURL(string: "http://baseurl")).PUT("/api/", parameters: nil, success: { (task: NSURLSessionDataTask!, response: AnyObject!) -> Void in
// Handle success
self?.endEditing()
return
}, failure: { (task: NSURLSessionDataTask!, error: NSError!) -> Void in
// Handle error
self?.endEditing()
return
})
return
})
cleanRowAction.backgroundColor = UIColor.greenColor()
return [cleanRowAction]
}