SwiftUI: Unable to access Edit Mode within TabView - swift

I currently have a view HostView that provides HostSummaryView and EditHostSummaryView, where the former responds to editMode.wrappedValue? == .inactive. HostView looks like this:
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}
I have a RootView that contains a TabView, which looks like this:
struct RootView: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
View1()
.onTapGesture { self.selectedTab = 0 }
.tag(0)
View2()
.tag(1)
HostView()
.tag(2)
}
}
}
I tried passing the #Environment(\.editMode) var editMode to HostView, but that did not fix the problem. The EditButton does not toggle editMode in the HostView. However, HostView works when I access it through a non-TabView view.
How can I get this to work?

I couldn't find a previous question about this but it is known that some things don't work from within a TabView you have to push it down a View.
I think it is considered a bug.
struct EditableHost: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
Text("View 1")
.onTapGesture { self.selectedTab = 0 }
.tag(0)
Text("View 2")
.tag(1)
ParentHostView()
.tabItem { Text("host") }
.tag(2)
}
}
}
struct ParentHostView: View {
#State var active: Bool = true
var body: some View {
NavigationView{
NavigationLink(
destination: HostView(),
isActive: $active,
label: {
Text("HOST")
})
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
VStack{
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
Text("HostSummary")
} else {
Text("EditHostSummary")
}
}.navigationBarBackButtonHidden(true)
}
}

I encounter the same case(Xcode 13.3, iOS 15.4). You can customize an edit button and everything should be ok.
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
Button {
if editMode != nil{
if editMode!.wrappedValue == .inactive{
editMode!.wrappedValue = .active
}else{
if editMode!.wrappedValue == .active{
editMode!.wrappedValue = .inactive
}
}
}
} label: {
Text(editMode?.wrappedValue.isEditing == true ? "Done" : "Edit")
}
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}

Related

How to make Sidebar row remember its previous detail view progress for split view SwiftUI Mac catalyst?

I am creating Split View with NavigationView in SwiftUI MacCatalyst. In sidebar (master view), I have two rows: 'Add' and 'Profile'. Tapping on them changes the detail view.
Suppose I click on 'Add' row, I see AddView() in detail view.
Then I tap 'Next 1' button to show 'TempView 1' on navigation stack.
Now, I tap on 'Profile' row in sidebar.
And when I tap one 'Add' row in sidebar again, I see this:
instead of this:
Does anyone know how to preserve row stages for SplitView in SwiftUI? My sample code below:
import SwiftUI
struct AddView: View {
#State var showNext = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: TempView1(), isActive: $showNext) { EmptyView() }
Button("Next 1") {
showNext = true
}
Text("Add View")
.padding()
}
}
.navigationViewStyle(.stack)
}
}
struct ProfileView: View {
#State var showNext = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: TempView2(), isActive: $showNext) { EmptyView() }
Button("Next 2") {
showNext = true
}
Text("Profile View")
}
}
.navigationViewStyle(.stack)
}
}
struct TempView1 : View {
var body: some View {
Text("Temp View 1")
}
}
struct TempView2 : View {
var body: some View {
Text("Temp View 2")
}
}
struct ContentView: View {
#State var showAddView = false
#State var showProfileView = false
#State var selectedRow = -1
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: AddView(), isActive: $showAddView) { EmptyView() }
NavigationLink(destination: ProfileView(), isActive: $showProfileView) { EmptyView() }
List {
Text("Add")
.padding()
.background(selectedRow == 0 ? Color.yellow : Color.clear)
.onTapGesture {
selectedRow = 0
showAddView = true
}
Text("Profile")
.padding()
.background(selectedRow == 1 ? Color.yellow : Color.clear)
.onTapGesture {
selectedRow = 1
showProfileView = true
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI: Dismisses View when Binding of List state changes

I have the following example in SwiftUI:
import SwiftUI
struct DetailView: View {
var element:Int
#Binding var favList:[Int]
var body: some View {
Button(action: {
if !favList.contains(element){
favList.append(element)
}
else{
favList.removeAll(where: {$0 == element})
}
}){
HStack {
Image(systemName: (favList.contains(element)) ? "star.slash" : "star")
Text((favList.contains(element)) ? "Remove from favorites" : "Add to favorites")
}
.frame(maxWidth: 300)
}
}
}
struct MainView: View {
let elements = [1,2,3,4]
#State var favList:[Int] = []
var body: some View {
NavigationView {
List{
if !favList.isEmpty{
Section("Favorits"){
ForEach(elements, id: \.self){element in
if favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
Section("All elements"){
ForEach(elements, id: \.self){element in
if !favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
}
}
}
If I change the favList in the DetailView the view gets automatically dismissed. I guess this because the List structure changes.
Am I doing something wrong? Is this the intended behavior? How can I avoid this?
Best regards
i tried with this, it's the same code just fixing the section header. using the fav button in the view doesn't dismiss the view
import SwiftUI
struct DetailView: View {
var element:Int
#Binding var favList:[Int]
var body: some View {
Button(action: {
if !favList.contains(element){
favList.append(element)
}
else{
favList.removeAll(where: {$0 == element})
}
}){
HStack {
Image(systemName: (favList.contains(element)) ? "star.slash" : "star")
Text((favList.contains(element)) ? "Remove from favorites" : "Add to favorites")
}
.frame(maxWidth: 300)
}
}
}
struct MainView: View {
let elements = [1,2,3,4]
#State var favList:[Int] = []
var body: some View {
NavigationView {
List{
if !favList.isEmpty{
Section(header:Text("Favorits")){
ForEach(elements, id: \.self){element in
if favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
Section(header:Text("Favorits")){
ForEach(elements, id: \.self){element in
if !favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
}
}
}
Tried it on xcode 12,5 with iOS 14 as target

Modify state in nested object

im new to Swift and i am having this issue. I want to make a play/pause button in the toolbar and i decided to move the toolbar code to its own object Toolbar. The button should change its image when pressed but when i press it the state doesn't change. What am i doing wrong?
struct ContentView: View {
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar()
}
}
}
}
struct Toolbar: ToolbarContent {
#State var started = false
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
self.started.toggle()
}) {
Image(systemName: self.started == true ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}
Use two-way binding.
struct ContentView: View {
#State private var started = false //<-- Here
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar(started: $started) //<-- Here
}
}
}
}
struct Toolbar: ToolbarContent {
#Binding var started: Bool //<-- Here
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
self.started.toggle()
}) {
Image(systemName: self.started ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}
#State only works inside Views, and ToolbarContent isn't a View.
You should keep the #State started inside ContentView, and pass in its wrapped value to the toolbar. Then, use a closure to update it.
struct ContentView: View {
#State var started = false
var body: some View {
NavigationView {
List{
Text("asdf")
}
.toolbar {
Toolbar(started: started) {
started.toggle() /// executed when `pressed` is called
}
}
}
}
}
struct Toolbar: ToolbarContent {
var started: Bool
var pressed: (() -> Void) /// closure here!
var body: some ToolbarContent {
ToolbarItem(id:"start-button", placement: .primaryAction) {
Button(action: {
pressed() /// call the closure
}) {
Image(systemName: self.started == true ? "pause.fill" : "play.fill")
.foregroundColor(.white)
}
}
}
}

Navigation Bar Buttons sometimes are not tappable

I have FavoritesView set with a navigationBarItem Button to display SettingsView as a modal sheet. However, both this button, and the Done button in the sheet only register and respond to some taps. I can't see any pattern to when the buttons will respond, but sometimes it can take 4 or 5 taps before the app responds! Any way to fix this behaviour?
FavoritesView:
import SwiftUI
enum ActiveSheet {
case details, settings
}
struct FavoritesView: View {
let speakers: [Speaker] = Bundle.main.decode("SpeakerTestData.json")
#State private var selectedModel: Speaker?
#State private var showingSheet = false
#State private var activeSheet: ActiveSheet = .settings
#EnvironmentObject var favorites: Favorites
#EnvironmentObject var settings: UserSettings
var filteredFavorites: [Speaker] {
let allSpeakers = speakers
var filteredItems: [Speaker] = []
for entry in allSpeakers {
if favorites.contains(entry) {
filteredItems.append(entry)
}
}
let sorted = filteredItems.sorted {
$0.model.localizedStandardCompare($1.model) == .orderedAscending
}
return sorted
}
var body: some View {
NavigationView {
List {
if filteredFavorites.count == 0 {
Text("Items you favourite will appear here.")
.foregroundColor(.secondary).padding(5)
} else {
Section(header: Text("Speakers")) {
ForEach(filteredFavorites) { speaker in
HStack {
Button(action: {
self.activeSheet = .details
self.selectedModel = speaker
self.showingSheet = true
}) {
SpeakerModelRow(speaker: speaker).contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button(action: {
if self.favorites.contains(speaker) {
self.favorites.remove(speaker)
} else {
self.favorites.add(speaker)
}
}, label: {
if self.favorites.contains(speaker) {
Image(systemName: "star.fill")
.foregroundColor(.blue)
.font(Font.title.weight(.ultraLight))
} else {
Image(systemName: "star")
.foregroundColor(.gray)
.font(Font.title.weight(.ultraLight))
}
}
).padding(5)
}
}
}
}
}
.navigationBarTitle("Favourites")
.navigationBarItems(trailing:
Button(action: {
self.activeSheet = .settings
self.showingSheet = true
}){
Image(systemName: "gear").font(Font.title.weight(.ultraLight)).padding(.trailing, 5).foregroundColor(.primary)
})
.sheet(isPresented: self.$showingSheet) {
if self.activeSheet == .details {
SpeakerDetailView(speaker: self.selectedModel!, showSheet: self.$showingSheet).environmentObject(self.favorites).environmentObject(self.settings)
} else {
SettingsView(showSheet: self.$showingSheet).environmentObject(self.settings)
}
}
}
}
}
SettingsView presented as a sheet:
struct SettingsView: View {
#EnvironmentObject var settings: UserSettings
#Binding var showSheet: Bool
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
NavigationView {
VStack {
//SettingsView Window
}.navigationBarTitle("Settings")
.navigationBarItems(leading: Button("Done") {
self.showSheet = false
})
}
}
}
It is known issue, try to use internal button content padding (either default or more), like
.navigationBarItems(leading: Button(action: {
self.showSheet = false
}) { Text("Done").padding() }

Pop to root view using Tab Bar in SwiftUI

Is there any way to pop to root view by tapping the Tab Bar like most iOS apps, in SwiftUI?
Here's an example of the expected behavior.
I've tried to programmatically pop views using simultaneousGesture as follow:
import SwiftUI
struct TabbedView: View {
#State var selection = 0
#Environment(\.presentationMode) var presentationMode
var body: some View {
TabView(selection: $selection) {
RootView()
.tabItem {
Image(systemName: "house")
.simultaneousGesture(
TapGesture().onEnded {
self.presentationMode.wrappedValue.dismiss()
print("View popped")
}
)
}.tag(0)
Text("")
.tabItem {
Image(systemName: "line.horizontal.3")
}.tag(1)
}
}
}
struct RootView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Tapping the house icon should pop back to root view")
}
}
But seems like those gestures were ignored.
Any suggestions or solutions are greatly appreciated
We can use tab bar selection binding to get the selected index. On this binding we can check if the tab is already selected then pop to root for navigation on selection.
struct ContentView: View {
#State var showingDetail = false
#State var selectedIndex:Int = 0
var selectionBinding: Binding<Int> { Binding(
get: {
self.selectedIndex
},
set: {
if $0 == self.selectedIndex && $0 == 0 && showingDetail {
print("Pop to root view for first tab!!")
showingDetail = false
}
self.selectedIndex = $0
}
)}
var body: some View {
TabView(selection:selectionBinding) {
NavigationView {
VStack {
Text("First View")
NavigationLink(destination: DetailView(), isActive: $showingDetail) {
Text("Go to detail")
}
}
}
.tabItem { Text("First") }.tag(0)
Text("Second View")
.tabItem { Text("Second") }.tag(1)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
I messed around with this for a while and this works great. I combined answers from all over and added some stuff of my own. I'm a beginner at Swift so feel free to make improvements.
Here's a demo.
This view has the NavigationView.
import SwiftUI
struct AuthenticatedView: View {
#StateObject var tabState = TabState()
var body: some View {
TabView(selection: $tabState.selectedTab) {
NavigationView {
NavigationLink(destination: TestView(titleNum: 0), isActive: $tabState.showTabRoots[0]) {
Text("GOTO TestView #1")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
.onAppear(perform: {
tabState.lastSelectedTab = TabState.Tab.first
}).tabItem {
Label("First", systemImage: "list.dash")
}.tag(TabState.Tab.first)
NavigationView {
NavigationLink(destination: TestView(titleNum: 0), isActive: $tabState.showTabRoots[1]) {
Text("GOTO TestView #2")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}.navigationTitle("")
.navigationBarTitleDisplayMode(.inline).navigationBarTitle(Text(""), displayMode: .inline)
}
.navigationViewStyle(.stack)
.onAppear(perform: {
tabState.lastSelectedTab = TabState.Tab.second
}).tabItem {
Label("Second", systemImage: "square.and.pencil")
}.tag(TabState.Tab.second)
}
.onReceive(tabState.$selectedTab) { selection in
if selection == tabState.lastSelectedTab {
tabState.showTabRoots[selection.rawValue] = false
}
}
}
}
struct AuthenticatedView_Previews: PreviewProvider {
static var previews: some View {
AuthenticatedView()
}
}
class TabState: ObservableObject {
enum Tab: Int, CaseIterable {
case first = 0
case second = 1
}
#Published var selectedTab: Tab = .first
#Published var lastSelectedTab: Tab = .first
#Published var showTabRoots = Tab.allCases.map { _ in
false
}
}
This is my child view
import SwiftUI
struct TestView: View {
let titleNum: Int
let title: String
init(titleNum: Int) {
self.titleNum = titleNum
self.title = "TestView #\(titleNum)"
}
var body: some View {
VStack {
Text(title)
NavigationLink(destination: TestView(titleNum: titleNum + 1)) {
Text("Goto View #\(titleNum + 1)")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
NavigationLink(destination: TestView(titleNum: titleNum + 100)) {
Text("Goto View #\(titleNum + 100)")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView(titleNum: 0)
}
}
You can achieve this by having the TabView within a NavigationView like so:
struct ContentView: View {
#State var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
FirstTabView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(0)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct FirstTabView: View {
var body: some View {
NavigationLink("SecondView Link", destination: SecondView())
}
}
struct SecondView: View {
var body: some View {
Text("Second View")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}