Cocoa .scrollToEndOfDocument stops working after 1st try - swift

I'm writing a small MacOS program with a WebView which loads a website. This is one of those sites that auto loads more content when you scroll to the bottom. As an expirement, I'm trying to write code that will constantly scroll to the bottom of the page so that more of it is loaded and so on.
What I have so far is this:
scrollToBottomTotalTimes = 10
self.myWebView.mainFrame.load(NSURLRequest(url: NSURL(string: urlString)! as URL) as URLRequest!)
func webView(_ sender: WebView!, didFinishLoadFor frame: WebFrame!) {
var scrollToBottomCurrentTimes = 0
while scrollToBottomCurrentTimes != scrollToBottomTotalTimes {
scrollToBottomCurrentTimes += 1
self.myWebView.scrollToEndOfDocument(self)
sleep(2) //give the page some time to load
}
}
This works well once: The page loads, it scrolls to the bottom, and more of the page loads.
However after that, the WebView doesn't scroll anymore, despite ample time for the page to reload. The position on the page stays exactly as before.
I get the feeling that .scrollToEndOfDocument(self) doesn't seem to detect the fact that the webpage has become longer.
Is there a way to 'update' .scrollToEndOfDocument(self) so it knows where the new bottom of the page is?
Thanks!

It seems to me that you are using a wrong approach. Assuming that new content arrives you may get multiple requests to didFinishLoadFor. However you are doing a loop inside the method with a sleep inside the method which makes that you block the main queue and disable the event mechanism.
I suggest you setup a timer event using Grand Central Dispatch (GCD) and then fire off the scroll event from the timer event. Please note that you have to do the scrolling in the main queue and the timer event is not in the main queue. Just check out the documentation on GCD. See https://developer.apple.com/reference/dispatch.

Related

NSOpenPanel.runModal + NSAlert.runModal over GCD cause a hang

In my Cocoa application I have some computations done in the background. The background work is runned with DispatchQueue.global(qos: .utility).async.
This background task may report errors by showing a modal NSAlert via DispatchQueue.main.async.
Also, in my application user can run NSOpenPanel to open some files (with NSOpenPanel.runModal).
The problem is that if the user opens NSOpenPanel and at the same time the background task shows NSAlert, the application may hang.
user opens modal NSOpenPanel
background task opens modal NSAlert atop the NSOpenPanel
user clicks Close inside NSOpenPanel (it really can access NSOpenPanel despite the more modal NSAlert present).
both NSAlert and NSOpenPanel being closed and application hangs with main thread blocked inside NSOpenPanel.runModal()
application will not hang if user first close NSAlert and then NSOpenPanel.
The minimal code sample (the test IBaction is binded as the action for the button in the main window)
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBAction func test(_ sender: Any) {
//run some work in background
DispatchQueue.global(qos: .utility).async
{
sleep(1) //some work
//report errors in the main thread.
DispatchQueue.main.async {
let alert = NSAlert();
alert.informativeText = "Close NSOpen panel before this alert to reproduct the hang."
alert.runModal()
}
}
//user want to open a file and opens the open file dialog
let dlg = NSOpenPanel();
dlg.runModal();
}
}
So, what's wrong with this code and why it cause the hang in particular use cases? And how can I prevent such a hang?
Additional note: I discovered, then if I replace dlg.runModal() with NSApp.RunModal(for: dlg) (which is exactly the same as per Apple documentation), this will fix hang in the usecase described above.
But it still will close NSAlert automatically right after closing NSOpenPanel. And I still can't understood why this behave as it does.
Update
I updated the code above to include full code of AppDelegate class of minimal reproducible application. To reproduce the case, just create new SwiftApp in XCode, replace AppDelegate code, add button in the main window and wire button's action with test func. I also placed full ready-to-compile project on github: https://github.com/snechaev/hangTest
The code for configuring NSOpenPanel and NSAlert along with their results handling is excluded as such a code does not affect the hang.
I think you are deadlocking the main queue because runModal() blocks the main queue in two places of your code at the same time. If your code is reentrant, that is what you get.
Possible solutions:
Please avoid the use of app modal windows and use instead window modal windows a.k.a. sheets. To know how to use NSOpenPanel as a sheet, attach it to the window it pertains to. For an example, please see this answer:
NSOpenPanel as sheet
You can set a flag that prevents the user from opening the NSOpenPanel if the alert is being shown, but that is ugly and does not solve any future problems that can cause other deadlocks because most probably your code is reentrant.
I would like to add some details in addition to #jvarela answer and make some resume about the my issue.
Looks like there is no way to solve the problem with having the NSPanel/NSAlert as modal windows with blocking the caller thread (with runModal).
The non-modal (NSPanel.begin()) or modal non-blocking (NSPanel.beginSheet, NSPanel.beginSheetModal) do not lead the hang, but still lead to to unexpected automatic closing of NSAlert if user try to close NSPanel before NSAlert. Moreover, to use such a non-blocking approach, you will be forced to refactor the whole your codebase to use callbacks/completion handlers instead of blocking operations when using NSPanel.
I do not found the reason why the NSPanel not being blocked and continue to receive user input when I show modal NSAlert atop of it. I suspect that is because of security mechanism which run NSPanel in a separate process, but I have no evidences about this. And I still interested about this.
For the my current project I decide to leave the blocking way to use of NSPanel, because I have large codebase and it will hard to change it all in moment to use of completion handlers. For particuluar case with NSPanel + NSAlert I just don't allow user to open NSPanel while this particular background work in progress. The user now should wait background work to finish or manually cancel the work to be able to run Open file functionality.

Navigation lags as more remote data is loaded and added to the screen in SwiftUI

I'm working on a screen in my app that allows users to leave comments. Additionally, they can view previous comments by tapping a button. The most recent comments are always at the end of the comments list, and the oldest at the top.
When the view more button is tapped, I fetch 10 previous comments from the database. My fetching code looks like this:
func fetchMoreComments(for gameId: Int, isLoadingMoreComments: Binding<Bool>) {
guard self.canLoadMoreComments else { return }
APIManager
.CommentsAPI
.getMoreComments(for: gameId, lastCommentId: self.lastCommentId!)
.sink(receiveCompletion: { completion in
print("load more comments completion:", completion)
},
receiveValue: {[weak self] response in
if response.statusCode == 200 {
// self?.comments.reserveCapacity((self?.comments.count)! + response.items.count)
let comments = response.items
if let lastCommentId = comments.map({ $0.id! }).min() {
self?.lastCommentId = lastCommentId
}
self?.comments.insert(contentsOf: comments.sorted(), at: 0)
isLoadingMoreComments.wrappedValue = false
self?.canLoadMoreComments = response.items.count >= 1
}
})
.store(in: &subscriptions)
}
Also, I should mention, when the screen is first loaded, there is another function I use to pull the 3 most recent comments to display on the screen the first time the user loads that particular screen.
The problem
As more comments are loaded, the navigation slows down. What I mean is, if I tap the back button to go back to the previous screen, there is a delay of about 4-5 seconds before I'm actually taken back to that screen. The reverse also has the same problem. It takes about 4-5 seconds to load the screen with the comments.
What I've tried
1)
I spent days sorting out memory leaks and cleaning up my code as I tried to find the problem. What I found was that, if I commented out the forloop that reads the array from my view model (the one function I pasted above lives in), then the laggy navigation issue disappears. I've pasted the forloop below. I've tried using .id from my commments object and also .self to improve the performance, but it didn't make a difference.
ForEach(self.liveGameViewModel.comments, id: \.self) { comment in
LiveGameCommentCell(comment: comment)
}
.id(UUID())
2)
I further narrowed things down to a NavigationLink inside the LiveGameCommentCell. That navigation link was a link to the commenter's profile screen. I noticed, once I commented out this navigation link, the navigation lag was partially cured. I could load up to 100 comments (10 at a time), then leave the screen (there would still be a few seconds of lag), however, once navigating back into the screen with the comments, the lag would be completely gone.
3)
Since I could no longer use the navigation, I decided to use a sheet to present a user's profile screen instead. Here's where I noticed another issue, or maybe just the same issue I've had all along. I would load the screen with the comments, and tap on a commenter's image, and the sheet would present itself with no lag. I'd then dismiss the sheet and load a few more comments and tap another commenter's profile image. I noticed, the more comments I loaded, the longer the sheet would take to present itself.
4)
Next, I decided to tweak how many comments are pulled from the database initially. I upped the limit to 100. Without loading more comments, I tapped on a commenter's profile image again, and the lag was there again. The problem is definitely related to the comments.
5)
I tried setting Swift array's reserveCapacity(_:) to 10 on the initial load of comments, then each time more comments were loaded, I'd set the capacity to the current number of comments in the array + the number received in the response. This didn't make much of a noticable difference.
I can't seem to track down this issue, but I know it's directly related to the number of comments on the screen. The more comments there are on the screen, the longer the sheet takes to present, or the longer it takes to navigate back to the previous screen.
I've refactored my code and fixed memory leaks I encountered. Most of the view models related to each screen only live for the life of that screen. For example, when a screen is dismissed, the view model is deinitialised.
Inside instruments, LiveGameCommentCell has the highest count and total duration. The count increases as I load more comments.
What could be causing this issue? I use the same type of functionality on other screens and don't have this issue. The only difference is, the other screens are entirely lists which contain foreach loops, whereas this problematic screen, is made up of the 2 images (host, challenger), some game info, such as, a short descriptive sentence, a deadline, comment count, the category, and then underneath all of this, a section for the list of comments and a text view to post new comments.
I even changed things around so that the API response for the comments would provide all of the info I would need for the comments, rather than utilising foreign keys and making multiple requests from the app. Everything that is needed is delivered in the getMostRecentComments and getMoreComments requests.
There were a few memory leaks I didn't get around to solving because they are most likely related to something else because they show regardless of visiting the screen that shows comments.
Also, I noticed when I first arrive the screen that shows the comments and tap a commenter's profile image, there are no errors. If I tap the next set of 10 comments that are loaded, there are no errors, but as the lagging increases, I notice an error starts to show when I tap on commenter's images. The error is:
[Common] _BSMachError: port 1c433; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"
I'd really appreciate some clues that may help me solve this issue. At present, it's not at all obvious to me what is causing this issue.
Thanks in advance
I had a ForEach loop inside a ScrollView, and not inside a List. Replacing the ScrollView with a list, and wrapping my NavigationLinks inside a LazyView solved the issue.

Where can I found documentation about page lifecycle events?

I am using cdp (https://github.com/mafredri/cdp) in order to use devtools protocol to generate a pdf of a page. But first I need to know when the page is completely loaded. I found that the networkIdle event can help me to know when this occurs. But, I have troubles because the networkIdle event sometimes fired twice. Then I need to know when this one is fired
There are two parts for what you're looking for.
First of all, the reason the event is fired twice. When a new tab (target) is created, the first page it loads is about:blank. You get lifecycle events for this page as well. The second time the load event is fired is the one you're looking for (if you're using Page.lifecycleEvent).
Now, to handle the second matter - there are also other events you can use. The basic one for page loading is Page.loadEventFired, which, as far as I recall, will only be fired for the actual page (but I could be wrong about this one).
Important note: If you're using lifecycle events, they are fired for each frame separately, meaning that the main frame might finish loading before the sub frames are loaded. Page.loadEventFired has a different behavior and waits for all frames to fire their load event.
Here is a good article on the page lifecycle api.
Another possible solution could be:
document.onreadystatechange = function () {
if (document.readyState === 'complete') {
run the screenshot code...
}
}

How do I implement "Cancel" button inside the method building data source in ModelController.m for Page View Controller

I have a Page View Controller that's implemented in 3 files. One of them, the ModelController.m implements "generateData" method, which basically just builds and formats all the pages for this Page View Controller. Inside this method, the first thing I do is I create a Progress Bar popup with "Cancel" button. Then, inside the while() loop, I keep building the pages and at the same time, every 10th page, I update the progress bar for the user to see that the application is still generating the output.
Once the "generateData" method completes, i.e. builds all the pages (and there may be over 1,000 or even 10,000 pages depending on user input), it returns "self" to a method inside RootViewController.m, which in turn passes that generated data in "dataObject" to "viewWillAppear" in DataViewController.m, which finally displays pages in that object to the user.
It all works great, except the "Cancel" button. Because "generateData" method runs on the main thread, it blocks and the "Cancel" button becomes totally unresponsive. By "blocks" I of course mean it takes all CPU cycles, not allowing anyting else to execute, and it may take over a minute to generate 10,000 pages so I really want to allow the user to cancel the operation.
I tried to create a separate thread for the Progress Bar popup which contains Cancel button, but that won't work because all UI operations need to be performed on the application's main thread.
What almost seems to work, is when I put the "generateData" method on a separate thread and keep the Progress Bar on the main thread. This would work just fine, except that now "generateData" is put on another thread to execute in the background and returns immediately, hence returning empty "self" or rather empty "dataObject", causing a crash because there is nothing in that object yet to display.
So how can I check if the "generateData" thread is done, and "return self" ONLY when it's done? I can't really have some BOOL true/false variable and do it in a loop, because that loop on the main thread would be again blocking the "Cancel" button.
Or, alternatively, what is the correct way to implement the "Cancel" button on a lengthy method in iOS? Maybe I'm doing it all wrong, but for the life of me I can't find the right recipe for this.
Thank you in advance for the help, the code is rather extensive and that's why I didn't include any, but I hope my description gives you a good idea of what's going on. I can provide code if that would help.
The correct way to do this is to load the data asynchronously (on a background thread) and update the main (UI) thread to update the progress bar, etc... But you probably don't need a progress bar since you can probably load those pages faster than a user can flip them, so load 100 and let the user play while you continue to load the next 99,900... Except those may not be the exact numbers you should use....
If you continue to use the progress view and cancel button, then you would cancel the background thread when the user presses "Cancel", which would respond since your data generation is on a background thread...
Look into either Grand Central Dispatch or NSInvocationOperations
Here's a good tutorial by Ray Wenderlich to get you started.

Scroll Event inside Facebook Canvas Applications

I'm looking to use a LazyLoad technique for images and infinite scroll to load new content into my page. Both these things make use of the $(window).scrollTop() function that inside a Facebook canvas application doesn't work. I know i can use FB.Canvas.getPageInfo() to get the current scrollTop value however I'm encountering some performance issues with this, my code is as follows:
var oldScroll = 0; // current scroll
var newScroll = null; // new scroll (fetched from FB.Canvas)
// Override the normal function to work within fb
// !!This seems to kill the browser
$.fn.scrollTop = function() {
return FB.Canvas.getPageInfo().scrollTop;
};
// poll to check for any changes in scroll
setInterval(function() {
newScroll = FB.Canvas.getPageInfo().scrollTop;
if(oldScroll != newScroll) { // have we scrolled at all?
oldScroll = newScroll;
$(window).trigger($.Event('scroll')); // fire a scroll event to keep the rest of the application that is listening
}
}, 1000);
It appears to be fine if i don't override the scrollTop function but once I do my browser soon crashes.
Am i going about this completely wrong? Has someone already created a way to do this inside FB canvas? Cheers.
Im trying to solve the same problem and my solution is a additional element (div#xscroll, pos:abs, L) inside Facebook application + setInterval:
function wininfo() {
FB.Canvas.getPageInfo(
function(info) {
$('#xscroll').height(info.clientHeight+info.offsetTop+info.scrollTop).trigger('scroll');
}
);
};
now you can use this #xscroll for LazyLoad:
$(".img").lazyload({threshold:400,effect:"fadeIn",skip_invisible:true,container:'#xscroll'});
the problem is - huge CPU usage while FB.Canvas.getPageInfo, my interval is 2000 - and CPU usage is hight. After hour of script work - Safari slows down and memoryleaked...
Do you insist on having the scrollTop() function overridden? Originally it works for any element that you supply via the jQuery selector, while you seem to restrict it to only the facebook canvas. If any other javascript tries to use scrollTop(), it will fail miserably, won't it?
As for the solution, I've done infinite scrolling pretty much the same way - setInterval() to see if you've reached the bottom, then load content, then check scroll again, and so on. But I'm not triggering the original scroll event of the window at any time, as there is no need to - at least in my case.