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() }
Related
Purpose
I want to update the data in the tabview automatically when I return to the RootView after I rename the tag name in the tagManagemenView.
Current Status
When I do delete operation in the TagManagementView, the RootView is able to update automatically.
However, If the Tag name is modified, the RootView display will not be updated, but clicking into the ItemDetailsView is able to display the latest modified name. Only the display of the RootView is not updated.
Background and code
Item and Tag are created using coredata and have a one-to-many relationship, where one Item corresponds to multiple Tags
// RootView
struct RootView: View {
#State private var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
ItemListView()
.tabItem {
Label ("Items",systemImage: "shippingbox")
}
.tag(0)
Settings()
.tabItem{
Label("Settings", systemImage: "gearshape")
}
.tag(1)
}
.navigationTitle(selection == 0 ? "Items" : "Settings")
}
.navigationViewStyle(.stack)
}
}
// ItemListView
struct ItemListView: View {
#FetchRequest var items: FetchedResults<Item>
#State private var itemDetailViewIsShow: Bool = false
#State private var selectedItem: Item? = nil
init() {
var predicate = NSPredicate(format: "TRUEPREDICATE"))
_items = FetchRequest(fetchRequest: Item.fetchRequest(predicate))
}
var body: some View {
ForEach(items) { item in
Button(action: {
self.selectedItem = item
self.itemDetailViewIsShow = true
}, label: {
ItemCellView(item: item)
})
}
if selectedItem != nil {
NavigationLink (
destination: ItemDetailView(item: selectedItem!, detailViewIsShow: $itemDetailViewIsShow),
isActive: $itemDetailViewIsShow
) {
EmptyView()
}
.isDetailLink(false)
}
}
}
// TagManagementView
struct TagManagementView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Tag.entity(), sortDescriptors: []) var allTags: FetchedResults<Tag>
#State var isShowDeleteAlert = false
#State var showModel = false
#State var selected: Tag?
var body: some View {
ZStack {
List {
ForEach(allTags) { tag in
TagCellView(tag: tag)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive, action: {
isShowDeleteAlert = true
selected = tag
}, label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.white)
})
Button(action: {
showModel = true
selected = tag
}, label: {
Label("Edit", systemImage: "square.and.pencil")
.foregroundColor(.white)
})
}
}
.confirmationDialog("Delete confirm", isPresented: self.$isShowDeleteAlert, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
if self.selected != nil {
self.selected!.delete(context: context)
}
}
Button(role: .cancel, action: {
isShowDeleteAlert = false
}, label: {
Text("Cancel")
.font(.system(size: 17, weight: .medium))
})
}
}
if self.showModel {
// background...
Color("mask").edgesIgnoringSafeArea(.all)
TagEditorModal(selected: self.$selected, isShowing: self.$showModel)
}
}
}
}
// TagEditorModal
struct TagEditorModal: View {
#Environment(\.managedObjectContext) var context
#State var tagName: String = ""
#Binding var isShowing: Bool
#Binding var selector: Tag?
init (selected: Binding<Tag?>, isShowing: Binding<Bool>) {
_isShowing = isShowing
_selector = selected
_tagName = .init(wrappedValue: selected.wrappedValue!.name)
}
var body: some View {
VStack{
TextField("Tag name", text: self.$tagName)
HStack {
Button(action: {
self.isShowing = false
}) {
Text("Cancel")
}
Button(action: {
self.selector!.update(name: self.tagName, context: context)
self.isShowing = false
}, label: {
Text("Submit")
})
}
}
}
}
// update tagName func
extension Tag {
func update(name: String, context: NSManagedObjectContext) {
self.name = name
self.updatedAt = Date()
self.objectWillChange.send()
try? context.save()
}
}
// ItemCellView
struct ItemCellView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var item: Item
var body: some View {
VStack {
Text(item.name)
TagListView(tags: .constant(item.tags))
}
}
}
// tagListView
struct TagListView: View {
#Binding var tags: [Tag]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geo in
VStack(alignment: .leading,spacing: 10) {
ForEach (getRows(screenWidth: geo.size.width), id: \.self) {rows in
HStack(spacing: 4) {
ForEach (rows) { tag in
Text(tag.name)
.font(.system(size: 10))
.fontWeight(.medium)
.lineLimit(1)
.cornerRadius(40)
}
}
}
}
.frame(width: geo.size.width, alignment: .leading)
.background(viewHeightReader($totalHeight))
}
}
.frame(height: totalHeight)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geo -> Color in
let rect = geo.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
func getRows(screenWidth: CGFloat) -> [[Tag]] {
var rows: [[Tag]] = []
var currentRow: [Tag] = []
var totalWidth: CGFloat = 0
self.tags.forEach{ tag in
totalWidth += (tag.size + 24)
if totalWidth > (screenWidth) {
totalWidth = (!currentRow.isEmpty || rows.isEmpty ? (tag.size + 24) : 0)
rows.append(currentRow)
currentRow.removeAll()
currentRow.append(tag)
} else {
currentRow.append(tag)
}
}
if !currentRow.isEmpty {
rows.append(currentRow)
currentRow.removeAll()
}
return rows
}
}
I added a TagCellView and then used an #ObservedObject for the tag
struct TagChipsView: View {
#ObservedObject var tag: Tag
let verticalPadding: CGFloat = 2.0
let horizontalPadding: CGFloat = 8.0
var body: some View {
Text(tag.name)
}
}
I have an issue with NavigationLinks with conditions. It doesn't react what I'm expected. When a user click on the button the function "test" must be called and give a return value. If the return value is true the "SheetView" must be openend directly without clicking on the NavigationLink text. Please could someone give me a help on this one. Thanks in advance
I made a small (sample) program for showing the issue.
import SwiftUI
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
Button(action: {
answer = test(value: 2)
if answer {
//if the return value is true then directly navigate to the sheetview
NavigationLink(
destination: SheetView(),
label: {
Text("Navigate")
})
}
}, label: {
Text("Calculate")
})
}
}
}
func test(value: Int) -> Bool {
if value == 1 {
check = false
} else {
print("Succes")
check = true
}
return check
}
}
struct SheetView: View {
var body: some View {
NavigationView {
VStack{
Text("Test")
.font(.title)
}
}
}
}
The answer from Yodagama works if you were trying to present a sheet (because you called your navigation destination SheetView), but if you were trying to navigate to SheetView instead of present a sheet, the following code would do that.
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
NavigationLink(destination: SheetView(), isActive: $answer, label: {
Button(action: {
answer = test(value: 2)
}, label: {
Text("Calculate")
})
})
}
}
}
func test(value: Int) -> Bool {
if value == 1 {
check = false
} else {
print("Succes")
check = true
}
return check
}
}
struct SheetView: View {
var body: some View {
NavigationView {
VStack{
Text("Test")
.font(.title)
}
}
}
}
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
Button(action: {
answer = test(value: 2)
//<= here
}, label: {
Text("Calculate")
})
}
.sheet(isPresented: $answer, content: { //<= here
SheetView()
})
}
}
...
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)
}
}
}
}
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()
}
}
}
I have 3 Views A, BSheet, C.
When I click on a button inside A View, B Sheet becomes visible. I have a button in B Sheet. I want to press a button in BSheet View to close/toggle BSheet and then navigate to C View. Any ideas how I can accomplish that?
A.swift
struct A: View{
#State var displayBSheet = false
var body: some View{
NavigationView{
VStack{
Button(action: {self.displayBSheet.toggle()}){
Text("Navigate to BSheet")
}.sheet(isPresented: $displayBSheet){
BSheet()
}
NavgiationLink(destination: C()){
Text("Navigate To C")
}
}
}
}
}
BSheet.swift
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
struct MainView: View {
var body: some View {
Button(action:{
self.presentationMode.wrappedValue.dismiss()//This only sheet but I want to navigate to C View also
}){
Text("Close sheet and navigate to struct C")
}
}
}
C.swift
struct C: View {
var body: some View {
Text("C")
}
}
I cleaned up your codes for a good working one:
import SwiftUI
struct ContentView: View {
var body: some View {
HomeView()
}
}
struct HomeView: View {
#State var homeView: Bool = false
#State var displayViewB: Bool = false
#State var displayViewC: Bool = false
var body: some View{
ZStack {
VStack {
HStack {
Text("Home").font(Font.largeTitle.bold()).padding()
Spacer()
}
Spacer()
}
VStack {
Button(action: { homeView = true; displayViewB = true }) {
Text("open ViewB")
}
.sheet(isPresented: $homeView) {
if displayViewB
{
ViewB(homeView: $homeView, displayViewB: $displayViewB, displayViewC: $displayViewC)
}
if displayViewC
{
ViewC(homeView: $homeView, displayViewC: $displayViewC)
}
}
}
}
.onChange(of: homeView) { _ in
if displayViewB == false && displayViewC == true {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {homeView = true}
}
else if displayViewB == false && displayViewC == false {
homeView = false
}
}
}
}
struct ViewB: View {
#Binding var homeView: Bool
#Binding var displayViewB: Bool
#Binding var displayViewC: Bool
var body: some View {
Button(action:{
displayViewB = false
displayViewC = true
homeView = false
}){
Text("close ViewB and open Viewc")
}
}
}
struct ViewC: View {
#Binding var homeView: Bool
#Binding var displayViewC: Bool
var body: some View {
Button(action:{
displayViewC = false
homeView = false
}){
Text("close ViewC go to HomeView")
}
}
}
You can do this simple - in onDismiss of sheet
struct A: View{
#State var displayBSheet = false
#State var displayCSheet = false // << this !!
var body: some View{
NavigationView{
VStack{
Button(action: {self.displayBSheet.toggle()}){
Text("Navigate to BSheet")
}.sheet(isPresented: $displayBSheet, onDismiss: {
self.displayCSheet = true // << here !!
}){
BSheet()
}
NavgiationLink(destination: C(), isActive: $displayCSheet){ // << here !!
Text("Navigate To C")
}
}
}
}
}