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

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.

Related

SwiftUI Menu top-to-bottom order varies depending on context?

I think I know the answer to this, but I'll pose it as a question and give my answer, since it was a bit puzzling to me and it took me a little while to figure out.
I'm just starting to use Menu's in SwiftUI. In my first application of them I used them in the nav bar of my app.
Menu {
ForEach(pickers, id: \.mediaPickerUIDisplayName) { picker in
Button(action: {
viewModel.sheetToShow = .picker(picker)
}) {
Text(picker.mediaPickerUIDisplayName)
}.enabled(picker.mediaPickerEnabled)
}
} label: {
SFSymbolIcon(symbol: .plusCircle)
}
And it looked good.
Then, I changed the context in which this same code was used. Instead of being used from the nav bar, I used it in the center of a screen. And instead I saw:
I was disconcerted in that the top-to-bottom ordering of items in the list had changed.
[UPDATE]
In iOS16 (beta) at the time of writing you can now use the new modifier .menuOrder(.fixed). That will keep the order of your items, well, fixed and the name implies…
Yup. This is the ‘native’ behavior which I personally find extremely bad. Imagine you have the delete button on the bottom and suddenly without any warning the order changes only because the element has moved above /bellow the midline.
Muscle memory will kick in and the users will get extremely confused.
This is an omission with catastrophic consequences.
Then I noticed that the order relative to the activating button is the same. So, it seems the order of menu items is context dependent.

When I am testing a GUI, an element appears and disappears too quickly

I am testing the GUI using the tools of Webdriver.io and mocha. The tests themselves are written in CoffeeScript. Some interface elements are loaded for a long time, and a rotating loading indicator appears. In order to continue the testing process, it is necessary to wait for the data to be fully loaded (that is, to wait until the loading indicator disappears). This process was performed using function (1):
wait_for_page_load = () ->
$('... load indicator selector ...').waitForDisplayed(20000)
$('... load indicator selector ...').waitForDisplayed(20000, true)
In the first line, I expect the moment when the download indicator becomes visible. In the second term I expect the disappearance of the loading indicator.
However, in the process, I was faced with a situation in which the download indicator appears and disappears too quickly. At the same time, I simply do not have time to “catch” the loading indicator, because at that moment, when I expect it to appear, it already disappears. At the same time, an error message is displayed in the console:
element ("... load indicator selector ...") still not displayed after 20000ms
I found a way out of this situation. When a similar problem occurred, I fixed only the disappearance of the loading indicator. This process was performed using function (2):
wait_for_page_load = () ->
$('... load indicator selector ...').waitForDisplayed(20000, true)
It should be noted that with the fast disappearance of the loading indicator, it is also impossible to do without waiting at all - in this case, new data will not have time to load.
However, in some situations, I cannot determine in advance exactly how long the loading indicator will be visible: sometimes it disappears almost immediately, and I cannot track the moment it appears, in these cases I have to use function (2); sometimes it rotates for a long time, and it is possible to track the moment of its appearance using the function (1).
Is it possible to write a universal function that will fix the appearance and disappearance of a graphic element, even if the element appears and disappears very quickly?
So far I have found the following way to solve the problem. In situations where it is necessary to wait for the loading indicator to disappear, I first time out one second, and then wait for the loading indicator to disappear.
utilities.wait_for_page_load = () ->
browser.pause(1000)
$('... селектор индикатора загрузки ...').waitForDisplayed(20000, true)
Thus, if the download indicator appeared and disappeared too quickly, then at the end of the timeout, it will no longer be on the screen, respectively, we automatically waited for the download indicator to disappear. If the download indicator hangs on the screen for a long time, then at the end of the timeout, we simply continue to wait for it to disappear.
The method is not ideal. Its main drawback is an increase in the total test run time. However, the method is universal and allows you to handle both situations described in the question.

NSCollectionView error: Parameter indexPath out of bounds or nil

I've recently been playing around with OSX programming (usually an iOS guy) but I've hit a strange issue with NSCollectionView that I just can't seem to debug.
When I change the data that my data source uses to populate the collection with items, I call reloadData() on the collection view as usual and sometimes hits an assertion. The error I get is Parameter indexPath out of bounds or nil. If I don't change the data the app always crashes at the same point(s) though sometimes, usually the first time, it works just fine and only crashes subsequently.
Now, I've debugged this numerous times. The data is correct and at no point in my code do I ever see a nil index path. The assertion always occurs on the makeItemWithIdentifier call, and always on the last item in the collection. If I continue, I either see what I expect or else some of the cells from the previous collection are still there behind the new cells.
At one point I refactored the code slightly and found the error occurring in NSCollectionViews itemAtIndexPath: function instead.
Has anyone else seen this error and, if so, what caused it?
Update 2: I now know what is causing this issue, but have no idea how to prevent it. What happens is this: the user clicks an item in the collection view. That item changes the data object that the data source is reading from calls reloadData during the collection view delegate's didSelectItemsAtIndexPaths method. For some reason, after execution of this method, the collection view is queried again for the cell with that index path. If the table has been reloaded in the meantime then that cell is gone, leading to the exception. In short: you can't call 'reload' on a collection view while a mouse event is being processed on one of its cells without getting an annoying assertion. So if anyone can think of a way around this issue, I'd love to hear it!

iOS 7 Safari: OS locks up for 4 seconds when clicking/focusing on a HTML input

UPDATE: The issue seems to stem from having many select elements on a page. How random is that?
So here's the issue. On iOS 7 Safari, when tapping the a text input on my site, the keyboard opens then freezes the OS for about 2-5 seconds then finally scrolls to the input. After this happens once, it never happens again until you refresh the page. I've looked all over the place, and yes, iOS 7 Safari is super buggy, but lets try and see if we can figure this out.
Note: This does not happen in any other mobile browser or any previous iOS Safari. It happens both on the ios 7 iphone and ios 7 ipad.
I will list everything my friend and I have tried so far:
Removed the ability to add event handlers in jQuery. (Note: all our event handlers are assigned through jQuery except for unload and onpageshow).
Removed the jQuery autocomplete script from the inputs.
Removed all JavaScript from the inputs.
Removed all third-party libraries being added on the page by rejecting the domains on the Mac.
Switched back to previous jQuery versions. The last one we could actually use before nothing worked was 1.7.0.
Switched back to previous jQuery UI versions.
Changed input event handling to delegate and live, instead of on('click')
Removed all CSS classes.
Removed all CSS from the page. Note: The response time for the OS this went down to 1-2 seconds but still happened.
Does anyone have any ideas?
Thanks a bunch!
(There are some somewhat-effective solutions, see near the end of the list)
At my company we are also suffering from this. We filed an issue with Apple but have heard mum.
Here are some interesting jsfiddles to help illustrate some of the issues, it definitely seems to revolve around the number of hidden fields, and textareas do not seem to be affected.
From debugging efforts, my guess is that there is some functionality trying to detect if an input is a credit card or phone number or some special kind which seems to cause the locking behavior. This is just one hypothesis though..
Summary:
On a page with a form containing named input elements inside containers that are marked "display: none", the first press on an input in that form has a very noticeable delay (20sec-2min) between the keyboard coming up and the input being focused. This prevents users from using our web app due to the enormous time spent with the ui frozen waiting for the keyboard to respond. We have debugged it in various scenarios to try and discern what is going on, and it appears to be from a change in how iOS7 parses the DOM versus how it did on iOS6, which has none of these issues.
From debugging within Safari's Inspector with the iPad connected, we found that iOS7 provides much more information about the (program)'s activities, to the point that we found that _CollectFormMetaData is the parent of the problem. Searching for meta data causes massive churn that increases more than linearly along with the number of hidden containers containing inputs. We found that _isVisible and _isRenderedFormElement are called far more than they reasonably should be. Additionally, if it helps, we found some detection functions relating to credit cards and address books were large time consumers.
Here are some jsFiddles for illustration. Please view them in Safari on an iPad running iOS6 and then on an iPad running iOS7:
http://jsfiddle.net/gUDvL/20/ - Runs fine on both
http://jsfiddle.net/gUDvL/21/ - Just noticeable delay on iOS 7
http://jsfiddle.net/gUDvL/22/ - More noticeable delay on iOS 7
http://jsfiddle.net/gUDvL/29/ - VERY noticeable delay on iOS 7
http://jsfiddle.net/gUDvL/30/ - Same as 29 but with none hidden - no delay on iOS 7
http://jsfiddle.net/gUDvL/38/ - Same as 29 but further exacerbated
http://jsfiddle.net/gUDvL/39/ - 99 hidden inputs, one visible, one separately visible
http://jsfiddle.net/gUDvL/40/ - 99 hidden textareas, one visible, one separately visible
http://jsfiddle.net/gUDvL/41/ - 99 hidden inputs, one visible, one separately visible, all
with the autocomplete="off" attribute
http://jsfiddle.net/gUDvL/42/ - 99 hidden inputs, one visible, one separately visible. Hidden by position absolute and left instead of display.
http://jsfiddle.net/gUDvL/63/ - Same as gUDvL/43/ but with autocomplete, autocorrect, autocapitalize, and spellcheck off
http://jsfiddle.net/gUDvL/65/ - Same as gUDvL/63/ but with cleaned up indentation (seems slower on iPad)
http://jsfiddle.net/gUDvL/66/ - Same as gUDvL/65/ but with display none via css again instead of DOMReady jQuery
http://jsfiddle.net/gUDvL/67/ - Same as gUDvL/66/ but with TedGrav's focus/blur technique
http://jsfiddle.net/gUDvL/68/ - Same as gUDvL/66/ but with css driven text-indent instead of display:block again (noticeable improvement - reduction to 2-3 secs for initial focus)
http://jsfiddle.net/gUDvL/69/ - Same as gUDvL/68/ but with TedGrav's focus/blur re-added
http://jsfiddle.net/gUDvL/71/ - Same as gUDvL/66/ but with js adding a legend tag before each input. (noticeable improvement - reduction to 2-3 secs for initial focus)
<input type="text" autocomplete="off" /> (links to jsfiddle.net must be accompanied by code..)
(We should note that having the iPad connected to a Mac with Safari's debugger engaged dramatically emphasizes the delays.)
Steps to Reproduce:
Load any of the above jsfiddles on the iPad
Press an input to gain focus
Watch screen until you can type
Expected Results:
Expect to be able to type as soon as the keyboard pops up
Actual Results:
Watch the keyboard pop up and the screen freeze, unable to scroll or interact with Safari for a duration. After the duration, focus is given as expected. From then on no further freezes are experienced when focusing on inputs.
tl;dr technique summary
So overall there are a couple proposed fixes from various answers:
Don't hide the divs with display: none - use something like text-indent
Short circuit Apple's metadata scanning logic - many form tags or legend tags seem to do the trick
Auto focus/blur - Did not work for me but two people reported it did
Related threads at Apple:
https://discussions.apple.com/thread/5468360
There seems to be a problem with how IOS handles the touch-event for inputs and textareas. The delay gets larger when the DOM gets larger. There is however not a problem with the focus event!
To work around this problem you can override the touchend event and set focus to the input/textarea.
document.addEventListener("touchend", function (e) {
if (e.target.nodeName.toString().toUpperCase() == 'INPUT' || e.target.nodeName.toString().toUpperCase() == 'TEXTAREA') {
e.preventDefault();
e.target.focus();
}
});
This will however create a new problem. It will let you scroll the page while touching the input/textarea, but when you let go, the site will scroll back to the original position.
To fix this, you just need to check if any scrolling has occured, and surround the preventDefault and target.focus with an if statement.
To set the original position, you can use the touchstart event.
document.addEventListener("touchstart", function (e) {
... //store the scrollTop or offsetHeight position and compare it in touchend event.
}
EDIT Me and a colleague have improved it a little bit, and it works like a charm.
var scroll = 0;
document.addEventListener("touchstart", function (e) {
scroll = document.body.scrollTop;
});
document.addEventListener("touchend", function (e) {
if (scroll == document.body.scrollTop) {
var node = e.target.nodeName.toString().toUpperCase();
if (node == 'INPUT' || node == 'TEXTAREA' || node == 'SELECT') {
e.preventDefault();
e.target.focus();
if(node != 'SELECT') {
var textLength = e.target.value.length;
e.target.setSelectionRange(textLength, textLength);
}
}
}
});
Struggled with this issue as well within an ios fullscreen which was inserting /removing pages containing a single input element. Was experiencing delays up to 30 seconds with only a single visible text input element on the page (and within the entire DOM). Other dynamically inserted pages with single or multiple text inputs in the same webapp were not experiencing the input delay. Like others have mentioned, after the initial delay, the input field would behave normally on subsequent focus events (even if the dynamic page containing the input element was removed from the DOM, then dynamically re-rendered/inserted back into the DOM).
On a hunch based on the above behaviour, tried the following on page load:
$("#problem-input").focus();
$("#problem-input").blur();
While the above executes immediately with no delay, the end result is no subsequent delays when the input gets focus via user interaction. Can't explain the reason behind this working, but it appears to work consistently for my app while other suggested fixes have failed.
I have the same freezeing problem.
I am not sure we're in the same situation.
here is my demo:http://tedzhou.github.io/demo/ios7sucks.html
In my page, i use a <p> element with onclick attribute as a button.
When user click on the button, page change to a textarea.
Then a click on it will freezes the browser.
The time freezing spent relevent to the numbers of the dom elements.
In my pages, there are 10000 elements, which make it freeze by 10+ seconds.
We can solve the problem by switching the <p> element to the real <button>, or reducing the nums of dom elements.
ps: sorry for my poor english. LOL
The main issue for me was with hidden fields. Made the form hang for 10-15 seconds.
I managed to get around by positioning the hidden form fields off the screen.
To hide:
position: absolute;
left: -9999px;
To show:
position: relative;
left: 0;
Met the same problem in quite complex application having many inputs.
Attached debugger to Safari iOS7 via USB and logged UI events. I see "touchend" event coming as soon as I am clicking on textarea (or any input) and in 10-20 seconds after that I see "click" being dispatched.
Clearly it is a bug in Safary as on other devices like Android or iOS6 there is no problem with the very same application.
It happens not only in iOS but in safari 7 for MAC OS (Maverics) too, I have found that the problem happens when you use a lot of div tags to contain inputs (or selects) within a form:
<div> <select>...</select> </div>
<div> <select>...</select> </div>
...
I changed the layout of my selects to use ul/li and fieldsets instead of divs and the freezze time was reduced drastically.
<ul>
<li><select>...</select></div>
<li><select>...</select></div>
</ul>
Here are two examples in jsfiddle:
freezze for 5 seconds
http://jsfiddle.net/k3j5v/5/
freeze for 1 second
http://jsfiddle.net/k3j5v/6/
I hope it might help someone
For me, this issue was being caused by user inputs being hidden on the page with display:none.
The workaround I used: instead of hiding inputs with display:none, I used jQuery's detach() method on document ready to 'hide' all the user inputs that were not being used. Then append() the inputs when they were needed.
That way no inputs had display:none on when the page was first loaded and so no delay occurred on the initial user interaction.
We had the same or a similar problem at my company. Whenever we displayed a large number of drop down lists and then a user clicked on a drop down, IOS 7 would freeze the page for a minute or two. After it unfroze, everything would work properly from that point forward.
This affected all input types. The large number of drop downs were actually hidden on first load - the user would initiate the display of the drop downs. Until the drop downs were displayed - everything would work fine. As soon as they were displayed, the next input click, even an input that had been working properly, now would cause the browser to freeze.
As others have noted, it seems that IOS 7 has a problem when parsing the visible inputs in the DOM after the user first interacts with an input. When the number and/or complexity of the elements/options/DOM are higher, the freeze is more pronounced.
Because it always froze on the initial user interaction, we decided to initiate a hidden user action as soon as we displayed the list of drop downs. We created a transparent button (it could not be hidden - it had to be "displayed") and initiated a click on it as soon as the user opened the drop down list. We thought that this would make IOS start parsing the DOM quicker, but found that it actually fixed the problem completely.
I have encountered this problem as well since I noticed many people are still having a problem with this I thought I'd put my solution.
Basically my solution is server side hiding of elements.
My page is ASP.NET so I wrapped my divs with the inputs with Panels and set these panels as Visible false.
This way if I click on an input the safari can't see all the other controls since they are hidden server side.
Of course if you want to make this work a little like clientside jquery you'll need automatic postback and an updatepanel somewhere.
This solution requires an effort but still its better than actually trying to fix a safari bug.
Hope this helps.
My answer might be slightly off the main topic, but I did arrive here after some searching as the scenario "feels" similar.
Issue:
My issue felt like a lockup in iOS, but not quite, since other elements on the page were still interactive. I had an <input type="search" /> element, that would not focus when I clicked into the field. But it would eventually catch focus after about 4-5 taps on the screen.
Additional Info:
My project is a hybrid app: WebView inside of an iOS app. The site is built with Twitter Bootstrap.
Solution:
I happened to also have the autofocus attribute set on the element. I tried removing that and it worked... no more consecutive taps to get the field to focus.
iOS 12.1.1 - December 2018
Here is a simple fix that worked in my case:
window.scrollTo(0,0) // attached to 'blur' event for the input fields
While it may not be ideal in terms of UX (especially if you have a form with many fields), it's definitely better than having 10+ second freezing time.
have you tried to turn off "Password & Autofill" > "Credit Cards" into Safari settings ?
After this operation it works fine. This isn't a final solution but maybe the problem's reason on iOS.

UITableView Add Rows At The Bottom Using Parsing Another Page

I am working on an example project for learning purposes. I have successfully parsed a wordpress site by parsing RSS feed using NSXMLParser. The custom cell includes thumbnail, title, author and published date of posts.
Currently its showing the first page of feed containing 10 posts. I can access the second page of the feed http://sitename.com/feed/?paged=2 and and third page by putting /?paged=3, if i type them in the browser.
What i want to do is when user scrolls down to the bottom, it uses the second page feed url and show the posts at the bottom and so on. Can anyone guide me how to accomplish this task.
Coneybeare has the right idea on how to set it up.
Use AFNetworking, it'll make your life way easier.
I would have an int counter that keeps track of which URL has been last called. Then, as soon as the user gets to the bottom most cell (which you know via a delegate method), I would increase the int by 1, and then append that to the String that represents the URL. You can use the stringByAppendingString method for that.
Now you have a new String with the correct url, and you can fire off a new request, and append the data to what you already have. Then you can do [self.tableView reloadData] to refresh the table.
To make an infinite scroller, you need to know when to start fetching the new items. There are many ways to do this, but the best place is to hook into the UITableViewDelegate's methods. When cellForRowAtIndexPath: is called, check if the row is the last in your data store. If it is, make the call for the new data, then append it to the old data and refresh your view. You can also hook into the table views scroller to see when it it X percent scrolled to the bottom to make this call.