SwiftUI PageTabView in iOS 14.2 performance issues lagging - ios

I'm using TabView with PageTabViewStyle, and each child view comprises a list view with a large data set.
Only on iOS 14.2, the page transitions seem to be very laggy.
However, page transitions are not delayed in list views with a small amount of data.
It's my guess that the performance of TabView comprises list would be independent of the amount of data, because of the list row display is lazy.
So, I believe it is bugs or default view style changes.
I look forward to your help to solve this problem. Thank you
#available(iOS 14.0, *)
struct ContentView: View {
#State var showHeart: Bool = false
var body: some View {
TabView{
self.contents
self.contents
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View{
List(0..<1000){_ in
Text("HELLO WORLD HELLOWORLD")
}
}
}

I have been playing with this and just a discovery - when you use TabView() it is laggy, but if you add a binding passed as TabView(selection: $selection) and just don't do anything with the selection binding it somehow doesn't lag anymore? Hacky, but a solution.

Try using lazy loading. Something like this: https://www.hackingwithswift.com/quick-start/swiftui/how-to-lazy-load-views-using-lazyvstack-and-lazyhstack
As you can see in the video: https://streamable.com/7sls0w
the List is not properly optimized. Create your own list, using LazyVStack. Much better performance, much smoother transition to it.
I don't think you understood the idea. Code to solve the issue:
#State var showHeart: Bool = false
var body: some View {
TabView {
contents
contentsSecond
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View {
List(0..<10000) { _ in
Text("HELLO WORLD HELLOWORLD")
}
}
var contentsSecond: some View {
return ScrollView {
Divider()
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Luke, I am your father \(value)")
.padding(.all, 5)
Divider()
}
}
}
}

I updated to iOS 14.2 yesterday and have the same issue (using Scrollview instead of List btw). I believe this is a bug.
One possible Workaround is to fallback to UIKits PageViewController by using UIViewControllerRepresentable as shown in the accepted answer here:
How can I implement PageView in SwiftUI?
This has solved the lagginess problem.

Related

SwiftUI - ScrollView Data is Disappearing

I have a ScrollView that loops through data and passes it into a view. In the iOS simulator, the data persists and remains in the screen. But when using it on a device, the views load but disappear after a few seconds.
struct MessageThreadView: View {
#EnvironmentObject var messagesManager: MessagesManager
let thread: MessageThread
var body: some View {
VStack {
VStack {
ScrollView {
ScrollViewReader { value in
ForEach(Array(messagesManager.messages.enumerated()), id: \.offset) { index, message in
ChatMessageBubble(message: message)
}.onAppear {
value.scrollTo(messagesManager.messages.last?.messageId, anchor: .center)
}
}
}.id(UUID().uuidString)
}
MessageFieldView(threadUid: thread.uid)
.environmentObject(messagesManager)
}
.task {
print("Getting messages")
messagesManager.getMessagesForThread(threadId: thread.uid)
}
}
}
I've avoided using a List because when using a List there's a border around it. I'm using a ScrollViewReader so I can scroll to the bottom of the messages when a conversation is opened.
And I had read on a previous question that adding .id then it resolves a caching issue. But this hasn't occurred for me.
I switched the MessagesManager class that held the data that was disappearing from an #EnvironmentObject to a #StateObject and that fixed the problem.

SwiftUI View Code is seemingly getting called twice. What is the issue here?

I have a view that is being called using .sheet in SwiftUI. However, when this sheet is presented, I'm getting the debug "Tapped" print in console followed by this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c9bdf5c22SheetHostingControllerVS_7AnyView_: 0x7f8f1d7400f0> on <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8f17e0dae0> which is already presenting (null)
I'm not exactly sure what is causing this error, but from what I understand it's due to the view getting called twice. I'm not sure how the view is being called twice, or even if it is, which is why I'm asking here. Below is the main view that actually houses the NavigationView in which my List is being housed view
struct AllTablesView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State private var tableTapped: Table = Table()
#State private var isModal = false
var body: some View {
NavigationView {
List {
ForEach(self.tables) { table in
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
.onTapGesture {
self.tableTapped = table
self.isModal = true
print("tapped")
}
}
.onDelete { indexSet in
let deletedTable = self.tables[indexSet.first!]
self.managedObjectContext.delete(deletedTable)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.sheet(isPresented: $isModal){
TableStatusView(table: self.tableTapped)
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
.navigationBarTitle("All Tables")
.navigationBarItems(leading: EditButton(), trailing:
HStack {
DeleteAllTablesButton()
AddTableButton()
})
}
}
}
And, if for whatever reason this is the issue, here is the code for my view titled "TableStatusView"
struct TableStatusView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State var table: Table
var body: some View {
VStack {
Form {
Section {
Text("Table \(table.number)")
}.padding(.all, 10.0)
Section {
Toggle(isOn: $table.inUse) {
Text("In Use")
}.padding(.all, 10.0)
Toggle (isOn: $table.clean) {
Text("Clean")
}.padding(.all, 10.0)
}
}
}
}
}
Is there a reason I'm getting this error, or why my view is being called twice? Is it actually that it's being called twice or am I doing something wrong here? I know certain parts of my code could probably be shrunk down or written better but this is just my first dive into SwiftUI, I haven't had much experience with it outside of basic stuff. Thanks!
I never did find a solution to the question, but I did solve the NavigationLink issue and swapped back to using that. I was using ".onTapGesture" to get a tap gesture and then generate a NavigationLink, thinking it was an action. NavigationLink, I've learned, is actually more or less a container to house content, so I replaced the .onTapGesture and TableCellView() function call with the following:
NavigationLink(destination: TableStatusView(table: table)) {
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
}
And fixed the issue for me. I'm still seeing some errors but from some google-fu found out that they are current bugs of SwiftUI. SwiftUI 2.0 may fix some of these issues

Why is SwiftUI picker in form repositioning after navigation?

After clicking the picker it navigates to the select view. The item list is rendered too far from the top, but snaps up after the animation is finished. Why is this happening?
Demo: https://gfycat.com/idioticdizzyazurevase
I already created a minimal example to rule out navigation bar titles and buttons, form sections and other details:
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
This happens in preview mode, simulator and on device (Xcode 11.2, iOS 13.2 in simulator, 13.3 beta 1 on device).
The obviously buggy behavior can be worked around when forcing the navigation view style to stacked:
NavigationView {
…
}.navigationViewStyle(StackNavigationViewStyle())
This is a solution to my problem, but I won‘t mark this as accepted answer (yet).
It seems to be a bug, even if it may be triggered by special circumstances.
My solution won‘t work if you need another navigation view style.
Additionally, it won‘t fix the horizontal repositioning mentioned by DogCoffee in the comments.
In my opinion, it has something to do with the navigation bar. In default (no mention of .navigationBarTitle extension), the navigation display mode is set to .automatic, this should be amend to .inline. I came across another post similar to this and use their solution to combine with yours, by using .navigationBarTitle("", displayMode: .inline) should help.
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
.navigationBarTitle("", displayMode: .inline)
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Until this bug is resolved another way to work around this issue while retaining the DoubleColumnNavigationViewStyle for iPads would be to conditionally set that style:
let navView = NavigationView {
…
}
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(navView.navigationViewStyle(DoubleColumnNavigationViewStyle()))
} else {
return AnyView(navView.navigationViewStyle(StackNavigationViewStyle()))
}
Thanks for this thread everyone! Really helped me understand things more and get a hold of one of my problems. To share with others, I was having this problem to but I was also having this problem when I set a section to appear in a if/else statement set on a section with a toggle. When the toggle was activated it would shift the section header horizontally a few pixels.
The following is how I fixed it
Section(header: Text("Subject Identified").listRowInsets(EdgeInsets()).padding(.leading)) {
Picker(selection: $subIndex, label: Text("Test")) {
ForEach(0 ..< subIdentified.count) {
Text(self.subIdentified[$0]).tag($0)
}
}
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
I'm still having horizontal shift on my picker selection view and not sure how to fix. I created another thread to received input. Thanks again! SwiftUI Shift Picker Text Horizontal

How to disable SwiftUI's default behavior?

SwiftUI makes it very easy to build declarative UIs. However, sometimes they assume defaults that are not necessarily what we want.
Example:
When adding two buttons inside a list row, SwiftUI automatically makes the whole row touchable, and both button's actions are called on row tap. This is the default behavior, as demonstrated in their WWDC videos.
But I do not want this behavior. I want both buttons to work properly, and the row to not be tappable.
Question:
How can we tell our Guacamole expert (to use the WWDC reference) to stop assuming how I want my list (or any other behavior) to work?
Any help would be appreciated.
If the List's default behavior is not required, you could use a VStack:
struct ContentView: View {
var body: some View {
VStack {
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
Button(action: {
print("bar")
}) {
Image(systemName: "photo")
}
}
}
}
However if List is really required, then it could be customized by writing a custom ListStyle.
(Also take a look at this question: How to change ListStyle in List.)
It seems that SomethingStyle protocol is the way Apple wants developers to use to modify native SwiftUI elements/behavior. Another example would be ButtonStyle or TextFieldStyle, etc.
I am under the impression that Apple wants to enforce their style guidelines. Also to add onto #backslash-f answer you could also just use a for each instead of a list, this will give you a similar effect and allow much more customization.
struct doubleList: View {
var body: some View {
VStack{
ForEach(1 ..< 10) {index in
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
}
}
}
}
Another option to try would be to wrap an UITableView into an UIViewRepresentable and try to enable buttons that way
It seems there might be another way around this by using tap gestures
Image(systemName: "photo")
.gesture(TapGesture().onEnded() {
print("action2")
})

SwiftUI: NavigationDestinationLink deprecated

After installing Xcode 11 beta 5 this morning, I noticed that NavigationDestinationLink was deprecated in favor of NavigationLink.
Also, that's what Apple says about it in the release notes:
NavigationDestinationLink and DynamicNavigationDestinationLink are deprecated; their functionality is now included in NavigationLink. (50630794)
The way I use NavigationDestinationLink is to programmatically push a new view into the stack via self.link.presented?.value = true. That functionality doesn't seem to be present in NavigationLink.
Any idea anyone?
I would rather not use NavigationDestinationLink anymore as it's deprecated...
Thank you!
UPDATE:
Actually, the NavigationDestinationLink way does not work anymore so I guess we have no way of pushing programmatically anymore?
UPDATE 2:
NavigationLink(destination: CustomView(), isActive: $isActive) {
return Text("")
}
This works but when you pass isActive to true, any state update will trigger this code and push over and over... Also, if you pass it back to false, it will pop the view.
Not only updates, if you set isActive to true, it will push the view (good) and if we press the back button, it will go back then immediately push again since it's still true.
Playing with onAppear was my hope but it's not called when going back to it...
I'm not sure how we're supposed to use this.
After spending some time with NavigationLink(destination:isActive), I am liking it a lot more than the old NavigationDestinationLink. The old view was a little confusing, while the new approach seems much more elegant. And once I figure out how to push without animations, it would make state restoration at application launch very easy.
There is one problem though, a big and ugly bug. :-(
Pushing a view programatically works fine, and popping it programatically does too. The problem starts when we use the BACK button in the pushed view which behaves oddly every other time. The first time the view is popped, the view pops and pushes again immediately. The second time around it works fine. Then the third time it starts all over again.
I have created a bug report (number here). I recommend you do the same and reference my number too, to help Apple group them together and get more attention to the problem.
I designed a workaround, that basically consists of replacing the default Back button, with our own:
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
// view pushed programmatically
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back (programatically)") {
// view popped programmatically
self.model.pushed = false
}
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
To improve the workaround without replacing the back button with a custom one, you can use the code above :
NavigationLink(destination: Test().onAppear(perform: {
self.editPushed = nil
}), tag: 1, selection: self.$editPushed) {
Button(action: {
self.editPushed = 1
}) {
Image(systemName: "plus.app.fill")
.font(.title)
}
}
The onAppear block will erase the selection value preventing to display the detail view twice
You can also use NavigationLink(destination:tag:selection)
NavigationLink(destination: MyModal(), tag: 1, selection: $tag) {
EmptyView()
}
So programmatically you can set tag to 1 in order to push MyModal. This approach has the same behaviour as the one with the Bool binding variable, so when you pop the first time it pushes the view immediately, hopefully they'll fix it in next beta.
The only downside I see with this approach, compared to DynamicNavigationDestinationLink is that you need to provide a View to NavigationLink, even if you don't need one. Hopefully they'll find a cleaner way to allow us to push programmatically.
The solution is to create custom back button for your detail view and pop detail view manually.
.navigationBarItems(leading:
Button(action: {
self.showDetail = false
}) {
Image(systemName: "chevron.left").foregroundColor(.red)
.font(.system(size: 24, weight: .semibold))
Text("Back").foregroundColor(.red)
.font(.system(size: 19))
}
)
The method used in the selected answer has been deprecated again. Here's the solution copied from this answer in this post.
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationStack {
VStack {
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
.navigationDestination(isPresented: $readyToNavigate) {
MyTargetView()
}
}
}

Resources