I have two Modal/Popover .sheet's I would like to show based on which button is pressed by a user. I have setup an enum with the different choices and set a default choice.
Expected behaviour:
When the user selects any choice, the right sheet is displayed. When the user THEN selects the other choice, it also shows the correct sheet.
Observed behaviour:
In the example below, when the user first picks the second choice, the first sheet is shown and will continue to show until the user selects the first sheet, then it will start to switch.
Debug printing shows that the #State variable is changing, however, the sheet presentation does not observe this change and shows the sheets as described above. Any thoughts?
import SwiftUI
//MARK: main view:
struct ContentView: View {
//construct enum to decide which sheet to present:
enum ActiveSheet {
case sheetA, sheetB
}
//setup needed vars and set default sheet to show:
#State var activeSheet = ActiveSheet.sheetA //sets default sheet to Sheet A
#State var showSheet = false
var body: some View {
VStack{
Button(action: {
self.activeSheet = .sheetA //set choice to Sheet A on button press
print(self.activeSheet) //debug print current activeSheet value
self.showSheet.toggle() //trigger sheet
}) {
Text("Show Sheet A")
}
Button(action: {
self.activeSheet = .sheetB //set choice to Sheet B on button press
print(self.activeSheet) //debug print current activeSheet value
self.showSheet.toggle() //trigger sheet
}) {
Text("Show Sheet B")
}
}
//sheet choosing view to display based on selected enum value:
.sheet(isPresented: $showSheet) {
switch self.activeSheet {
case .sheetA:
SheetA() //present sheet A
case .sheetB:
SheetB() //present sheet B
}
}
}
}
//MARK: ancillary sheets:
struct SheetA: View {
var body: some View {
Text("I am sheet A")
.padding()
}
}
struct SheetB: View {
var body: some View {
Text("I am sheet B")
.padding()
}
}
With some very small alterations to your code, you can use sheet(item:) for this, which prevents this problem:
//MARK: main view:
struct ContentView: View {
//construct enum to decide which sheet to present:
enum ActiveSheet : String, Identifiable { // <--- note that it's now Identifiable
case sheetA, sheetB
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil // <--- now an optional property
var body: some View {
VStack{
Button(action: {
self.activeSheet = .sheetA
}) {
Text("Show Sheet A")
}
Button(action: {
self.activeSheet = .sheetB
}) {
Text("Show Sheet B")
}
}
//sheet choosing view to display based on selected enum value:
.sheet(item: $activeSheet) { sheet in // <--- sheet is of type ActiveSheet and lets you present the appropriate sheet based on which is active
switch sheet {
case .sheetA:
SheetA()
case .sheetB:
SheetB()
}
}
}
}
The problem is that without using item:, current versions of SwiftUI render the initial sheet with the first state value (ie sheet A in this case) and don't update properly on the first presentation. Using this item: approach solves the issue.
Related
See the code below (BTW, List has the same issue):
enum Value: String, Equatable {
case a = "a"
case b = "b"
}
struct ContentView: View {
#State var value: Value = .a
#State var showSheet = false
var body: some View {
Form {
Section {
switch value {
case .a:
Text("a")
.onTapGesture {
showSheet = true
}
.sheet(isPresented: $showSheet) {
Button("Set Value to .b") {
value = .b
}
}
case .b:
Text("b")
}
}
}
}
}
To reproduce the issue, first click on text 'a' to bring up a sheet, then click on button in the sheet to change value. I'd expect the sheet should be dismissed automatically, because the view it's attached to is gone. But it isn't.
If I remove the Form, or replace it with, say, ScrollView, the behavior is as I expected. On other other hand, however, replacing it with List has the same issue (sheet isn't dismissed).
I'm thinking to file a FB, but would like to ask here first. Does anyone know if it's a bug or a feature? If it's a feature, what's the rationale behind it?
I'm currently using this workaround. Any other suggestions other than calling dismiss() in button handler are appreciated.
.onChange(of: value) { _ in
showSheet = false
}
UPDATE: I think what I really wanted to ask is if there is an "ownership" concept for sheet? I mean:
Does a sheet belongs to the view it's attached to and hence get dismissed when that view is destroyed?
Or it doesn't matter where a sheet is defined in view hierarchy?
Since I never read about the ownership concept, I suppose item 2 is true. But if so, I don't understand why the sheet is dismissed automatically when I removed Form and Section in the above code.
Try this approach of adding showSheet = false just after value = .b in your sheet Button. Also move your .sheet(...) outside the Form, and especially if you are using a List.
struct ContentView: View {
#State var value: Value = .a
#State var showSheet = false
var body: some View {
Form {
Section {
switch value {
case .a:
Text("a")
.onTapGesture {
showSheet = true
}
case .b:
Text("b")
}
}
}
.sheet(isPresented: $showSheet) { // <-- here
Button("Set Value to .b") {
value = .b
showSheet = false // <-- here
}
}
}
}
I'm trying to dynamically show a sheet when a Menu() button is pressed. The issue I'm facing is that it seems the activeSheet state variable is not updating when I change it via a button. This causes the wrong view to show until I update the state with a different variable in which then the proper view will show.
Code for generating the Menu and setting the active tab
#State private var activeSheet = Sheets.Settings
#State private var showingSheet = false
enum Sheets: String, CaseIterable {
case Settings = "Settings",
Add = "Add"
}
Menu(content: {
ForEach(Sheets.allCases, id: \.self) { sheet in
Button(sheet.rawValue, action: {
activeSheet = sheet
showingSheet.toggle()
})
}
},
label: {
Image(systemName: "plus")
})
And my sheet modifier that will display the actual sheet
.sheet(isPresented: $showingSheet) {
switch activeSheet {
case .Settings :
SettingsView(dismiss: $showingSheet, settings: $settings)
case .Add :
AddView()
}
}
This code will present a sheet but it will only show the settings sheet until I trigger some other #State change. After that everything will work as expected.
You should use .sheet(item:) instead of .sheet(isPresented:), and to do so the enum needs to conform to Identifiable.
This should work:
#State private var activeSheet: Sheets?
enum Sheets: String, CaseIterable, Identifiable {
case Settings = "Settings",
Add = "Add"
var id: String {
return self.rawValue
}
}
var body: some View {
Menu(content: {
ForEach(Sheets.allCases, id: \.self) { sheet in
Button(sheet.rawValue, action: {
activeSheet = sheet
})
}
},
label: {
Image(systemName: "plus")
})
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .Settings:
SettingsView(dismiss: $showingSheet, settings: $settings)
case .Add:
AddView()
}
}
}
I am using coredata to save information. This information populates a picker, but at the moment there is no information so the picker is empty. The array is set using FetchedRequest.
#FetchRequest(sortDescriptors: [])
var sources: FetchedResults<Source>
#State private var selectedSource = 0
This is how the picker is setup.
Picker(selection: $selectedSource, label: Text("Source")) {
ForEach(0 ..< sources.count) {
Text(sources[$0].name!)
}
}
There is also a button that displays another sheet and allows the user to add a source.
Button(action: { addSource.toggle() }, label: {
Text("Add Source")
})
.sheet(isPresented: $addSource, content: {
AddSource(showSheet: $addSource)
})
If the user presses Add Source, the sheet is displayed with a textfield and a button to add the source. There is also a button to dismiss the sheet.
struct AddSource: View {
#Environment(\.managedObjectContext) var viewContext
#Binding var showSheet: Bool
#State var name = ""
var body: some View {
NavigationView {
Form {
Section(header: Text("Source")) {
TextField("Source Name", text: $name)
Button("Add Source") {
let source = Source(context: viewContext)
source.name = name
do {
try viewContext.save()
name = ""
} catch {
let error = error as NSError
fatalError("Unable to save context: \(error)")
}
}
}
}
.navigationBarTitle("Add Source")
.navigationBarItems(trailing: Button(action:{
self.showSheet = false
}) {
Text("Done").bold()
.accessibilityLabel("Add your source.")
})
}
}
}
Once the sheet is dismissed, it goes back to the first view. The picker in the first view is not updated with the newly added source. You have to close it and reopen. How can I update the picker once the source is added by the user? Thanks!
The issue is with the ForEach signature you're using. It works only for constant data. If you want to use with changing data, you have to use something like:
ForEach(sources, id: \Source.name.hashValue) {
Text(verbatim: $0.name!)
}
Note that hashValue will not be unique for two entity objects with the same name. This is just an example
I have a segmented picker with three options that defaults to the first option. The problem is that when I select any of the other ones, it will not change for some reason.
This is what it looks like.
#Binding var transaction: TransactionModel
var body: some View {
VStack {
DatePicker(
"Transaction Date",
selection: $transaction.date,
displayedComponents: [.date]
)
Divider()
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(SegmentedPickerStyle())
}
.padding()
}
This is the code for the view so far, and below is the code for TransactionModel.
struct TransactionModel {
var date = Date()
var type = TransactionType.income
static let `default` = TransactionModel()
enum TransactionType: String, CaseIterable, Identifiable {
case income = "Income"
case expense = "Expense"
case transfer = "Transfer"
var id: String {self.rawValue}
}
}
One thing to note, it will work with the inline picker style. That will let me change between those three options, however, I want to use the segmented one. Can anyone help me figure out what's going on with this?
Edit: I actually found that the inline picker style doesn't seem to be working either. I added some code to display a text view based on what was selected, and it never changed from what it said when Income was selected. But that could be due to my code itself. Below is the code for that.
Picker("Type", selection: $transaction.type) {
ForEach(TransactionModel.TransactionType.allCases, id:\.self) { tType in
Text(tType.rawValue.capitalized)
}
}.pickerStyle(InlinePickerStyle())
Divider()
if(transaction.type == TransactionModel.TransactionType.income) {
Text("Income Selected")
Divider()
}else if(transaction.type == TransactionModel.TransactionType.expense) {
Text("Expense Selected")
Divider()
}
I want to basically make didSelectRow like UITableView in SwiftUI.
This is the code:
struct ContentView: View {
var testData = [Foo(name: "1"),
Foo(name: "2"),
Foo(name: "3"),
Foo(name: "4"),
Foo(name: "5")]
#State var selected: Foo?
var body: some View {
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
}
}
I was thought if I click the cell then selected should contains the selected value, but it's not. The selected has no value. And the cell not clickable.
So I added a Button.
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
Button("Test") {
print("\(foo) is selected.")
print(selected?.name)
}
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
Now, click works, but actually foo is the one I want there's no need selected why selection of the List is here.
Not sure anything I missed. Should the Button is necessary for the List "didSelectRow"? thanks!
EDIT
After a bit more investigation, my current conclusion is:
For single selections, no need call List(.. selection:). But you have to use Button or OnTapGesture for clickable.
List(.. selection:) is only for edit mode, which is multiple selection, as you can see the selection: needs a set. My example should be
#State var selected: Set<Foo>?
On iOS selection works in Edit mode by design
/// Creates a list with the given content that supports selecting multiple
/// rows.
///
>> /// On iOS and tvOS, you must explicitly put the list into edit mode for
>> /// the selection to apply.
///
/// - Parameters:
/// - selection: A binding to a set that identifies selected rows.
/// - content: The content of the list.
#available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
so you need either add EditButton somewhere, or activate edit mode programmatically, like
List(selection: $selection) {
// ... other code
}
.environment(\.editMode, .constant(.active)) // eg. persistent edit mode
Update: Here is some demo of default SwiftUI List selection
struct DemoView: View {
#State private var selection: Set<Int>?
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List(selection: $selection) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
}.onDelete(perform: {_ in})
}
.environment(\.editMode, .constant(.active))
}
}