TabView selection resets to first tab on sheet presentation - ios

Update:
Thanks to Harshil & Sumit for pointing me out that I was simply too dumb to realize that I was using id() instead of tag(). If there is anything you can learn from this question, it is this:
When you work alone on a project, you tend to go blind. You don't see your own mistakes. Do code reviews. Ask friends and colleagues to look over it. It's a great idea. ;)
Original Question:
In my SwiftUI project I am using a TabView with a $selection Binding for being able to switch tabs programmatically.
The problem is: When I present a sheet on, for example, the second view contained in the TabView the selection is reset to the first tab.
For me this seems like a SwiftUI bug - but is there a workaround?
Below you find a working example that demonstrates the behaviour. (Tested with Xcode 12.4)
How to test: Go to the second tab, tap the button "Two" and you will see that you get back to the first tab. As soon as you remove the selection property from the TabView this does not happen any more.
Cheers
Orlando 🍻
enum TabPosition: Hashable {
case one
case two
case three
}
struct RootView: View {
#State private var selection: TabPosition = .one
var body: some View {
TabView(selection: $selection) {
One()
.tabItem { Label("One", systemImage: "1.circle") }
.id(TabPosition.one)
Two()
.tabItem { Label("Two", systemImage: "2.circle") }
.id(TabPosition.two)
Three()
.tabItem { Label("Three", systemImage: "3.circle") }
.id(TabPosition.three)
}
}
}
struct One: View {
var body: some View {
Text("One").padding()
}
}
struct Two: View {
#State var isPresented = false
var body: some View {
Button("Two") { isPresented.toggle() }
.sheet(isPresented: $isPresented, content: {
Three()
})
}
}
struct Three: View {
var body: some View {
Text("Three").padding()
}
}

Use .tag() like this:
struct ContentView: View {
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
One()
.tabItem { Label("One", systemImage: "1.circle") }
.tag(1)
Two()
.tabItem { Label("Two", systemImage: "2.circle") }
.tag(2)
Three()
.tabItem { Label("Three", systemImage: "3.circle") }
.tag(3)
}
}
}

Insted of id .id(TabPosition.one) assign tag like this .tag(TabPosition.one)

Related

Minimize or bring back sheet in SwiftUI

I have a Voip calling app using CallKit and when call received, it will open a view call IncomingView in a sheet in my SwiftUI app. So far so good. But i want to minimize the sheet and can navigate to other pages and preferably shows a green bar at the navigation (similar to WhatApp) that indicates the call is going on and when i tap there, it should bring back my "IncomingView".
here is my code:
struct MainView: View {
let acceptPublishser = NotificationCenter.default
.publisher(for: Notification.Name.DidCallAccepted)
let endPublisher = NotificationCenter.default
.publisher(for: Notification.Name.DidCallEnd)
var body: some View {
NavigationView {
TabView {
TabListView()
.tabItem {
Label("Home", systemImage: "house.fill")
}
ContactListView()
.tabItem {
Label("Contacts", systemImage: "person.crop.circle")
}
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.crop.circle")
}
}
.padding(0)
.onAppear(){
self.showModal = MyCallDelegate.shared.isIncomingCall
}
.sheet(isPresented: $showModal){
IncomingCallView() // -> Present IncomingCallview() as sheet
}
.accentColor(Color(.green))
}.onReceive(self.acceptPublishser, perform: { output in
showModal = true
})
.onReceive(self.endPublisher, perform: { output in
showModal = false
})
}
}
struct IncomingCallView: View {
var body: some View {
VStack{
Spacer()
Text("callerId").foregroundColor(.white)
Text("Timer").foregroundColor(.white)
}
}

How can I manually turn off edit mode when the last item in a List gets deleted?

I have a simple view in my app that displays a list of checklists. When there are no checklists I display a placeholder view.
The issue I'm having is that when I delete the last item in the list, the EditButton() doesn't switch back to "Edit"; it still says "Done," even thought the list is empty If I then add a new item, the list appears again but in edit mode (a bad UX).
Is there a way to manually "turn off" edit mode after the last item in the list gets deleted?
Image: https://i.stack.imgur.com/lE1yu.png
import SwiftUI
struct HomeView: View {
// MARK: - PROPERTIES
#EnvironmentObject var checklistsVM: Store
#StateObject var homeVM: HomeViewModel = HomeViewModel()
// MARK: - BODY
var body: some View {
NavigationView {
Group {
if(checklistsVM.checklists.isEmpty) {
EmptyList()
.environmentObject(homeVM)
} else {
HomeListResults()
.environmentObject(homeVM)
}
}
.navigationBarTitle("Checklists", displayMode: .large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
You can turn off the edit mode using the Environment value:
#Environment(\.editMode) private var editMode
var body: some View {
Button("Turn off edit mode") {
editMode?.wrappedValue = .inactive
}
}

App freezes when I change EnvironmentObject value

I want to login my user when they click a button. When they click the button, it calls self.auth.login() which changes loggedIn to true.
When this change happens, ContentView is meant to reload with HomeView() as the body View. However it just crashes the app.
Here's my code:
ContentView.swift
struct ContentView: View {
#EnvironmentObject var settings: UserSettings
#EnvironmentObject var auth: UserAuth
var body: some View {
if (!auth.loggedIn){
return AnyView(LoginView())
} else {
return AnyView(HomeView())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
class UserAuth: ObservableObject {
#Published var loggedIn: Bool = false
func login(){
self.loggedIn = true
}
}
struct LoginView: View {
#EnvironmentObject var auth: UserAuth
var body: some View {
ZStack {
Color.orange
.edgesIgnoringSafeArea(.all)
VStack {
Image("launcher_logo").resizable()
.scaledToFit()
.frame(height: 100)
.padding(.top, 100)
Spacer()
Button(action: {
self.auth.login()
print("loggedIn: ", self.auth.loggedIn) // prints "loggedIn: true" then doesn't print again when pressed
}) {
Text(String(auth.loggedIn))
}
...
Any idea what the problem is?
EDIT:
If I change my ContentView View to just LoginView() then it doesn't freeze. The app works. But I wan ContentView to be dynamic based on whether the user is loggedIn or not.
EDIT2: HomeView is causing it to crash:
struct HomeView: View {
// #EnvironmentObject var auth: UserAuth
var body: some View {
TabView {
HomeView()
.tabItem {
Image(systemName: "1.square.fill")
Text(String("4"))
}.onTapGesture {
// self.auth.login()
}
HomeView()
.tabItem {
Image(systemName: "3.square.fill")
}
Text("The Last Tab")
.tabItem {
Image(systemName: "3.square.fill")
Text("Profile")
}
}
.font(.headline)
}
func goOnline(){
print("went online")
}
}
console log:
[Firebase/Crashlytics][I-CLS000000] Failed to download settings Error Domain=FIRCLSNetworkError Code=-5 "(null)" UserInfo={status_code=404, type=2, request_id=, content_type=text/html; charset=utf-8}
2020-06-04 19:53:04.408869+1000 App[6718:4035694] Connection 5: received failure notification
2020-06-04 19:53:04.409001+1000 App[6718:4035694] Connection 5: failed to connect 3:-9816, reason -1
2020-06-04 19:53:04.409111+1000 App[6718:4035694] Connection 5: encountered error(3:-9816)
Your HomeView calls itself, causing an infinite loop.

NavigationLink in contextMenu

I've been experimenting with NavigationLink in a contextMenu, and have run into this issue:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("foo")
.contextMenu {
NavigationLink(destination: Text("foo context destination")) { //works
Text("foo context")
}
}
.padding(.all)
NavigationLink(destination: Text("bar destination")) { //works
Text("bar")
.contextMenu {
NavigationLink(destination: Text("bar context destination")) { //does not work
Text("bar context")
}
}
}
.padding(.all)
} //VStack
} //NavigationView
} //body
} //ContentView
As shown in the code, NavigationLink within a contextMenu seems to work for 'foo context' but not for 'bar context'. The difference is that 'foo' is wrapped in a NavigationLink, but 'bar' is not. I would appreciate any suggestions for solving the issue with 'bar context' navigation.
Edit: To clarify, I would like to find a way to navigate to "bar destination" by tapping "bar", OR navigate to "bar context destination" by tapping "bar context" in the contextMenu. The problem seems to be that when "bar" is wrapped in NavigationLink, then NavigationLink in the contextMenu attached to "bar" is not working.
Thanks!
I have made changes to your code so that both "foo" and "bar" works.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("foo")
.contextMenu {
NavigationLink(destination: Text("foo context destination")) { //works
Text("foo context")
}
}
.padding(.all)
Text("bar")
.contextMenu {
NavigationLink(destination: Text("bar context destination")) { //does not work
Text("bar context")
}
}
.padding(.all)
} //VStack
} //NavigationView
} //body
} //ContentView

NavigationLink freezes when trying to revisit previously clicked NavigationLink in SwiftUI

I am designing an app that includes the function of retrieving JSON data and displaying a list of retrieved items in a FileBrowser type view. In this view, a user should be able to click on a folder to dive deeper into the file tree or click on a file to view some metadata about said file.
I've observed that while this is working, when I click on a file or folder then go back and click on it again, the NavigationLink is not triggered and I am stuck on the view until I click into a different NavigationLink.
Here is a gif demonstrating this problem.
As seen here, when I click on BlahBlah I am activating the NavigationLink and taken to BlahBlah, then when I navigate back and try to renavigate to BlahBlah, it becomes grey, registering that I clicked on it... but then never transports me there. Clicking on TestFile fixes this and allows me to navigate back to BlahBlah.
The list items are made with the following structs
private struct FileCell{
var FileName: String
var FileType: String
var FileID: String = ""
var isContainer: Bool
}
private struct constructedCell: View{
var FileType: String
var FileName: String
var FileID: String
var body: some View {
return
HStack{
VStack(alignment: .center){
Image(systemName: getImage(FileType: FileType)).font(.title).frame(width: 50)
}
Divider()
VStack(alignment: .leading){
Text(FileName).font(.headline)
.multilineTextAlignment(.leading)
Text(FileID)
.font(.caption)
.multilineTextAlignment(.leading)
}
}
}
}
and called into view with navigationLinks as follows
List(cellArray, id: \.FileID) { cell in
if (cell.isContainer) {
NavigationLink(destination: FileView(path: "/\(cell.FileID)", displaysLogin: self.$displaysLogin).navigationBarTitle(cell.FileName)){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}
} else {
NavigationLink(destination: DetailView(FileID: cell.FileID).navigationBarTitle(cell.FileName)){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}
}
}
My NavigationView is initialized in the view above (the app has a tab view) this as follows
TabView(selection: $selection){
NavigationView{
FileView(displaysLogin: self.$displaysLogin)
.navigationBarTitle("Home", displayMode: .inline)
.background(NavigationConfigurator { nc in
nc.navigationBar.barTintColor = UIColor.white
nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.black]
})
}
.font(.title)
.tabItem {
VStack {
Image(systemName: "folder.fill")
Text("Files")
}
}
.tag(0)
}
The NavigationConfigurator is a struct I use for handling the color of the navigationBar. It is set up like so
struct NavigationConfigurator: UIViewControllerRepresentable {
var configure: (UINavigationController) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationConfigurator>) {
if let nc = uiViewController.navigationController {
self.configure(nc)
}
}
}
I do not think my NavigationConfigurator is causing this? This bug also happens in other navigationLinks in the app, but it was easiest to demonstrate it here in the FileBrowser view.
This might be a bug in SwiftUI? If it is does anyone know a way to work around this? If it isn't, what am I doing wrong?
Had the same issue - try this. I would call this a hack to be removed when the bug in swiftUI is corrected.
struct ListView: View {
#State private var destID = 0
...
var body: some View {
...
NavigationLink(destination: FileView(path: "/\(cell.FileID)", displaysLogin: self.$displaysLogin)
.navigationBarTitle(cell.FileName)
.onDisappear() { self.destID = self.destID + 1 }
){
constructedCell(FileType: cell.FileType, FileName: cell.FileName, FileID: cell.FileID)
}.id(destID)
Essentially it seems that in some circumstances (iOS 13.3 - Simulator?) the NavigationLink is not reset when the destination view is removed from the navigation stack. As a work around we need to regenerate the Navigation Link. This is what changing the id does. This corrected my issue.
However if you have NavigationLinks that are chained, that is a link that leads to another list of links, then this solution will create side effects; the stack returns to the origin at the second attempt to show the last view.

Resources