SwiftUI TabView memory footprint continuously increases when changing page - swiftui

struct ContentView: View {
#State private var selectedIdx = 0
var body: some View {
TabView(selection: $selectedIdx) {
ForEach(0..<5) { idx in
Text("\(idx)")
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
Environment: Xcode 12.2 iOS 14.2
TabView in SwiftUI memory continuously increases as I swipe between pages.
Running instruments, I do not see any leaks but the allocation and persistent memory increases continuously.
Ideally, even if the pages are being recreated every time, the total memory consumed by the 5 pages (as in the code above) should not change.
Is this a bug in SwiftUI? Or am I missing something?

This code fixes a bug for me
struct TabViewWrapper<Content: View, Selection: Hashable>: View {
#Binding var selection: Selection
#ViewBuilder let content: () -> Content
var body: some View {
TabView(selection: $selection, content: content)
}
}
Replace TabView(selection:) to TabViewWrapper(selection:)
TabViewWrapper(selection: $selection) {
tabContent
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

Related

Navigation bug when dismissing view while focussing on empty .searchable() modifier

When trying to navigate back from a view using the environment Dismiss value while also focussing on an empty searchable modifier the view you navigated back to becomes unresponsive. This is due to an empty UIView blocking any interaction with the view as seen in this screenshot:
Empty UIView blocking view after navigating back
This only occurs when the searchbar is focussed and empty when trying to navigate back. When there's a value in the searchbar everything works:
GIF of the bug
Am I doing something wrong here?
Tested on Xcode 14.2 iPhone 14 Pro (iOS 16.0) simulator.
import SwiftUI
struct MainPage: View {
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
Text("Main view")
NavigationLink(destination: DetailView()) {
Text("Click me")
}
}
}
}
}
struct DetailView: View {
#Environment(\.dismiss) private var dismiss
#State private var searchText = ""
var body: some View {
VStack {
Text("Detail view")
Button("Go back") {
dismiss()
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
This bug only seems to happen when using NavigationStack or NavigationView with a .navigationViewStyle(.stack). When using NavigationView without a navigationViewStyle it seems to work fine. Currently I can work around this using the latter but I would prefer to use NavigationStack as NavigationView has become deprecated since iOS 16.0.
Any help is appreciated.

NavigationStack and TabView in Swiftui iOS 16: bug or improper usage?

[Xcode 14.1, iOS 16.1]
I have a NavigationStack with a navigationTitle and a TabView with 2 Views. Each View has a ScrollView (see image below):
NavigationStack and TabView problem image
When I tap on Tab1 (#1 in red on the image above), then swipe up, the behavior is as expected (#2), i.e. the big navigationTitle move to the center, and my view passes below and becomes blurry. Perfect.
However, when I tap ton Tab2 (#3) and then swipe up (#4), the big title stays big, and the view doesn't become blurry.
Then I tap on Tab1 again (#5) and it works as expected.
Please help!
Here is my code:
ContentView:
import SwiftUI
struct ContentView: View {
#State private var selection: Tab = .tab1
enum Tab {
case tab1
case tab2
}
#State private var mainTitle = "Tab1"
var body: some View {
NavigationStack {
TabView(selection: $selection) {
Tab1(mainTitle: $mainTitle)
.tabItem {
Label("Tab1", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab1)
Tab2(mainTitle: $mainTitle)
.tabItem {
Label("Tab2", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab2)
} .navigationTitle(mainTitle)
}
}
}
Tab1:
import SwiftUI
struct Tab1: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 1")
.padding(.all,100)
.background(.blue)
} .onAppear {
mainTitle = "Tab1"
}
}
}
Tab2:
import SwiftUI
struct Tab2: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 2")
.padding(.all,100)
.background(.green)
} .onAppear {
mainTitle = "Tab2"
}
}
}
I tried a hack that is supposed to fix the transparency bug for Tab bars, but it doesn't work.
.onAppear {
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
}
TabViews are designed to sit at the top of the navigation hierarchy. They're intended to allow users to switch between independent sections of your app at any time.
You would generally put a separate navigation stack within each tab that then handles pushing and popping of views. And then, you can use the navigationTitle modifier to manage the screen's title.
So your structure (which might be split over multiple custom views) should look something like:
TabView {
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 1")
}
.tabItem { Label("Tab1", ...) }
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 2")
}
.tabItem { Label("Tab2", ...) }
}
This structure is by design, to align with Apple's Human Interface Guidelines. It's worth reading the HIG to get a handle on where Apple are coming from, and how working on the same principles can really help your app feel like it belongs on your users' device.

Changing EnvironmentObject published property changes TabView

I have a an app I'm writing (first one) that has developed an issue. I have a tabbed view with multiple views, one of which is settings. On the settings view, various elements (mostly Toggles) bind to the global environment object appData. When I select something on the settings view, the TabView changes to the first tab away from the settings view.
The playground example below shows the issue. It does it the first time but not subsequent times. In my application, it does it every time. Not sure why it's different. To see the issue, load the playground code, click on settings and click the "Setting" toggle. It immediately jumps to MainView. The value for isSet in the application data does appear to get set.
I also see this on the emulator (set for iPhone SE) for my application.
I'm using XCode 13.4.
import SwiftUI
import Combine
import Foundation
import PlaygroundSupport
class ApplicationData: ObservableObject
{
#Published var isSet: Bool = false
}
struct MainView: View
{
var body: some View
{
Text("Main View")
}
}
struct SettingsView: View
{
#EnvironmentObject var appData : ApplicationData
var body: some View
{
Toggle("Setting", isOn: $appData.isSet)
}
}
struct TabbedView: View
{
#StateObject var appData = ApplicationData()
#State var selection: Int = 0
var body: some View
{
TabView(selection: $selection)
{
MainView().tabItem({Label("Main", systemImage: "menucard")})
SettingsView().tabItem({Label("Settings", systemImage: "gear")})
}
.environmentObject(appData)
}
}
let view = TabbedView()
let hostingVC = UIHostingController(rootView: view)
PlaygroundPage.current.liveView = hostingVC
I think the issue comes from TabView(selection:) itself. As you don't provide tags to identify the tabs, the view looses track when it is redrawn.
Either use TabView without selection:
TabView // here
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
}
Or – if with selection – you have to provide a .tag() so selection can track what is selected:
TabView(selection: $selection)
{
MainView()
.tabItem({Label("Main", systemImage: "menucard")})
.tag(0) // here
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1) // here
}

Navigation bar title stays inline in iOS 15

I have a NavigationView that contains a ScrollView with a large title. When I navigate to a page with an inline title, then navigate back, the title stays inline rather than reverting to the large title.
This only occurs in iOS 15 - in iOS 14 the title reverts back to the large title as desired. Is there a way to achieve the desired behavior in iOS 15?
Here's an example that illustrates the behavior:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
NavigationLink("Link", destination: DestinationView())
.padding()
}
.navigationBarTitle("Home", displayMode: .large)
}
}
}
struct DestinationView: View {
var body: some View {
Text("Destination")
.navigationBarTitle("test", displayMode: .inline)
}
}

TabView with "PageTabViewStyle" does not update it's content when #State var changes

I came across a weird Issue in SwiftUI.
I created a simple View that only holds a Button
and a TabView that uses the PageViewStyle. It seems that the TabView does not update it's content
correctly depending on the State of the Variable.
It seems that the content gets updated somehow but the View wont be updated how I would expect
Here is the Code of my View:
struct ContentView: View {
#State var numberOfPages: Int = 0
#State var selectedIndex = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
self.selectedIndex = 0
})
TabView(selection: $selectedIndex){
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").background(Color.red)
}
}
.frame(height: 300)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
}.background(Color.blue)
}
}
This is how the result looks after tapping the label several Times.
The Initial State is no 0 Pages. After you tap i would expect that the content of the
TabView changes so all Pages will be scrollable and visible but just the page indicator updates it State for some reason.
TabView expects to have container of pages, but you included only one HStack (with own dynamic content), moreover chaining number of pages you have to reset tab view, so here is a fix.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var numberOfPages: Int = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
})
if self.numberOfPages != 0 {
TabView {
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").frame(width: 300).background(Color.red)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.frame(height: 300)
.id(numberOfPages) // << here !!
}
}
}
}