I've used this popover to show a form in my application, in this popover there is a Form with 2 sections, one with a PickerView and one with aButton; for some reason the popover is not detecting the size of the form like it does for every other view like Text and so on, I've tried setting the size manually but this is not the solution for my problem because I want it to automatically get the size
This is the problem on iPadOS
Also it looks very bad on iOS too##
Code
import SwiftUI
struct FilterView: View {
struct Category {
var name: String
var filterCategory: FilterCategory
}
#State private var category: FilterCategory = .event
let categories = [Category(name: "Eventi", filterCategory: .event), Category(name: "Compiti", filterCategory: .homework), Category(name: "Voti", filterCategory: .grade)]
var body: some View {
// NavigationView {
Form {
Section(footer: Text("Seleziona la categoria che vuoi filtrare")) {
Picker("Categoria", selection: $category) {
ForEach(categories, id: \.filterCategory) { category in
Text(category.name).tag(category.filterCategory)
}
}
}
Section {
Button("Button") {
print(category)
}
}
}
// .navigationTitle("Filtra")
// }
}
}
Popover Code
Button(action: {
self.showFilterView.toggle()
}, label: {
Image(systemName: "slider.horizontal.3")
.imageScale(.large)
})
.popover(isPresented: $showFilterView, arrowEdge: .top) {
FilterView()
}
I'm using Xcode 12 beta and iOS/iPadOS 14 on the simulators
Related
Description
I've got simple Combat model which stores name and list of actors. When I delete the Combat from List using onDelete it looks like it's working. It removes the Combat from Realm (checked with RealmStudio) and updates the view. However, if view gets redrawn (for instance, when switching Apps), the "old" data is loaded again (the very first loaded on app initialization), so all deleted rows are back again. Of course, removing them again crashes the app, because they are not present in #ObservedResults combats anymore. Restarting the app fixes the issue, because new data is loaded to #ObservedResults combats and to List, but then again, when I removed something it will be back on review draw...
What I discovered is that removing .sheet() fixes the issue! (EDIT: clarification; it doesn't matter what's inside of the sheet, it may be even empty) The view is updated correctly on redraw! The Sheet is used to display form to add new Combat (nether to say that adding new combats or editing them does not update the view as well, but let's focus on deletion). I have no idea what adding sheet() changes in behaviour of the List and "listening" to #ObservedResults combats.
As a test I used simple array of Combat classes and everything worked. So it points me to issue with #ObservedResults.
I was using the Alert before and all changes to #ObservedResults combats were seen at glance. Now I wanted to replace Alert with Sheet and… That happened.
Also, I have subview where I have almost identical code for actor and there everything works, however I use #ObservedRealmObject var combat: Combat there, and I pass the combat #ObservedResults combats, like so:
NavigationLink(destination: CombatView(combat: combat)) { Text(combat.name) }
I removed unecessary code from below examples to keep it at minimum.
Model
The Combat model:
class Combat: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
Actual View Code (broken using Sheet)
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
var body: some View {
List {
ForEach(combats) { combat in
Text(combat.name)
}.onDelete(perform: $combats.remove)
}
.sheet(isPresented: $showAddCombat) {
AddCombatView( showAddCombat: $showAddCombat)
}
}
}
Old View Code (works using Alert)
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
#State private var showAddCombat = false
#State private var addCombatNewName = ""
var body: some View {
List(combats) { combat in
Text(combat.name)
.onDelete(perform: $combats.remove)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAlert = true
}) {
Image(systemName: "plus" )
.font(.title)
Text("New Combat")
}.alert("New Combat", isPresented: $showAlert) {
TextField("write name", text: $addCombatNewName)
Button("Close", role: .cancel) {
addCombatNewName = ""
}
Button("Add") {
addNewCombat(name: addCombatNewName)
addCombatNewName = ""
}
}
}
}
}
private func addNewCombat(name: String) {
let newCombat = Combat()
newCombat.name = name
do {
try self.realm.write {
realm.add(newCombat)
}
} catch {
fatalError("Error: \(error)")
}
}
}
EDITED
I just found some new behaviour. I made a new simple view which lists elements of Collection list and you can delete or add new Collection. It works just fine, but if I include this CollectionsView under the TabView, then the effect is exactly the same as in the example above. The view stops working properly: deleted items are added back on view redraw and adding new objects doesn't refresh the View.
This makes me think more of a bug in #ObservedResults().
Below is the source code.
class Collection: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
#main
struct CombatTrackerApp: App {
var body: some Scene {
WindowGroup {
Tabber() // will not work
// CollectionsView() // will work
}
}
}
struct CollectionsView: View {
#ObservedResults( Collection.self ) var collections
#State private var showNewCollectionForm = false
var body: some View {
NavigationStack {
List {
ForEach(collections) { collection in
Text(collection.name)
}.onDelete(perform: $collections.remove)
}
.listStyle(.inset)
.padding()
.navigationTitle("Collections")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button() {
self.showNewCollectionForm.toggle()
} label: {
Image(systemName: "plus")
Text("Add New Collection")
}
}
}
.sheet(isPresented: $showNewCollectionForm) {
NewCollectionView( showNewCollectionForm: $showNewCollectionForm )
}
}
}
}
struct NewCollectionView: View {
let realm = try! Realm()
#Binding var showNewCollectionForm: Bool
#State private var newCollectioName: String = ""
var body: some View {
NavigationStack {
VStack {
Text("Create new Collection").font(.title).padding()
Form {
TextField("Name", text: $newCollectioName)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close", role: .cancel) {
showNewCollectionForm.toggle()
}
}
ToolbarItem {
Button("Create") {
addCollection()
} .disabled(newCollectioName.isEmpty)
}
}
}
}
private func addCollection() {
let newCollection = Collection()
newCollection.name = newCollectioName
do {
try realm.write {
realm.add(newCollection)
}
} catch {
print("Cannot add new Collection", error)
}
showNewCollectionForm.toggle()
}
}
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
NavigationStack {
CollectionsView()
}
.tabItem {
Text("Collections")
}
SettingsView()
.tabItem {
Text("Settings")
}
}
}
}
I found out the solution (but I still don't understand why it's working).
The solution was to move NavigationStack from my TabView to the subviews. So instead of:
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
//...
I should do:
struct Tabber: View {
var body: some View {
TabView() {
CombatsListView()
.tabItem {
Text("Combats")
}
//...
struct CombatsListView: View {
var body: some View {
NavigationStack {
Confusing part was that all online tutorials and Apple Documentation suggests to wrap subviews with NavigationStack in TabView directly instead of adding NavigationStack in subviews. Maybe it's a bug, maybe it's a feature.
Before iOS 16 picker views take on special behavior when inside forms. They looked like a navigation link which takes you to a new screen where you can choose an option.
Since iOS 16 it seems, that this behavior was removed.
Is there a possibility to get the "old" behavior?
e.g. this code
struct ContentView: View {
#State private var selectedValue = "One"
let counts = ["One", "Two", "Three"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Selection", selection: $selectedValue) {
ForEach(counts, id: \.self) {
Text($0)
}
}
}
}
}
}
}
results in this behavior (since iOS 16)
instead of this (before iOS 16)
Thanks!!!
iOS 16 added NavigationLinkPickerStyle which has the pre iOS 16 behavior.
struct ContentView: View {
#State private var selectedValue = "One"
let counts = ["One", "Two", "Three"]
var body: some View {
NavigationView {
Form {
Section {
if #available(iOS 16.0, *) {
Picker("Selection", selection: $selectedValue) {
ForEach(counts, id: \.self) {
Text($0)
}
}
.pickerStyle(.navigationLink)
} else {
Picker("Selection", selection: $selectedValue) {
ForEach(counts, id: \.self) {
Text($0)
}
}
}
}
}
}
}
}
I'm in a similar boat, I'm currently downloading the 14.1 Beta of Xcode, however it WOULD be nice if you could have the menu style picker but also have it display nothing. It's almost as if you already have something selected, but you don't. It causes confusion with the user.
I have a SwiftUI Form with a custom chart view (not Swift Charts). A long press toggles to a different type of chart. These charts use the .transition(.slide) modifier. In iOS 15 these transitioned as expected on a long press, but in iOS 16 they do not.
Persisted state property (an enum):
#AppStorage("chartType") var chartType: ChartType = .chartA
The Form part of the body property:
Form {
// Other sections
Section {
switch chartType {
case .chartA:
ChartViewA()
.transition(.slide)
case .chartB:
ChartViewB()
.transition(.slide)
}
.onLongPressGesture {
if chartType == .chartA {
withAnimation {
summaryChartType = .chartB
}
} else {
withAnimation {
summaryChartType = .chartA
}
}
}
Unfortunately adding animation modifiers like .animation(.spring(), value: chartType) makes no difference.
I would be grateful for advice on why this might have worked in iOS 15 but not in iOS 16, and what I could do to restore animation here.
In iOS 16, there appears to be a problem with #AppStorage vars and animation. Here is one possible workaround. Use #State var for animation, and save it to an #AppStorage variable with .onChange():
enum ChartType: String {
case chartA, chartB
}
struct ChartViewA: View {
var body: some View {
Color.red
}
}
struct ChartViewB: View {
var body: some View {
Color.blue
}
}
struct ContentView: View {
#AppStorage("chartType") var chartTypeAS: ChartType = .chartA
#State private var chartType: ChartType = .chartA
init() {
// load initial value from persistent storage
_chartType = State(initialValue: chartTypeAS)
}
var body: some View {
Form {
// Other sections
Section {
VStack {
switch chartType {
case .chartA:
ChartViewA()
.transition(.slide)
case .chartB:
ChartViewB()
.transition(.slide)
}
}
.onLongPressGesture {
if chartType == .chartA {
withAnimation {
chartType = .chartB
}
} else {
withAnimation {
chartType = .chartA
}
}
}
}
.onChange(of: chartType) { value in
// persist chart type
chartTypeAS = value
}
}
}
}
Tested in Xcode 14.0 with iPhone 14 simulator running iOS 16.
Alternatively, you could perform the saving to/restoring from UserDefaults manually.
I'm using SwiftUI 3.0, Swift 5.5 and Xcode 13.2, tested on iOS 15.3 iPhone device, and iOS 15.2 iPhone simulator.
I have tested the following.
This is a view, with a TextField, a focused state and a .toolbar
import SwiftUI
struct test: View {
#State private var name = "Taylor Swift"
#FocusState var isInputActive: Bool
var body: some View {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.focused($isInputActive)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(name) {
isInputActive = false
}
}
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
It works perfectly as expected and it shows a button, with whatever text is typed in the TextField.
Then, when it's displayed in a sheet, there is no toolbar, though it is the same code. This is the sheet example:
import SwiftUI
struct test: View {
#State private var name = "Taylor Swift"
#FocusState var isInputActive: Bool
#State var isSheetPresented: Bool = false
var body: some View {
VStack {
Button {
self.isSheetPresented = true
} label: {
Text("Open Sheet")
}
}
.sheet(isPresented: $isSheetPresented) {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.focused($isInputActive)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(name) {
isInputActive = false
}
}
}
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
Toolbar needs a
NavigationView
And one at the top level. Surrounding the text field.
Today I also experienced the same thing. I had to spent many hours until finally got the solution after reading this question. I wanted to put "Done" button over my keyboard for dismissing it after editing is finish and I used ToolbarItem(placement: .keyboard).
In my case, I mistakenly put more than one .toolbar() in different places. And it causing the "Done" button in my sheet becoming disabled, something like this (in simulator):
In order to solve the problem, please DO NOT do this:
struct SettingsView: View {
var body: some View {
NavigationView {
Form {
// Some other codes..
}.navigationBarTitle("Settings", displayMode: .large).toolbar() { // <--- This one is a .toolbar()
ToolbarItem{
Button("Cancel"){
self.mode.wrappedValue.dismiss()
}
}}
}.toolbar { // <--- This one another .toolbar() (-_-")
ToolbarItem(placement: .keyboard) { // <--- This one is in the WRONG place!
Button("Done") {
focusedField = nil
}
}
}
}
}
Instead, do the following:
struct SettingsView: View {
var body: some View {
NavigationView {
Form {
// Some other codes..
}.navigationBarTitle("Settings", displayMode: .large).toolbar() { // Make it into a single .toolbar() 👍🏼
ToolbarItem{
Button("Cancel"){
self.mode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
}
}
Hope it helps.
I'm using a ForEach to parse a list of models and create a view for each of them, Each view contains a Button and a Text, the Button toggles a visibility state which should hide the text and change the Button's title (Invisible/Visible).
struct ContentView: View {
var colors: [MyColor] = [MyColor(val: "Blue"), MyColor(val: "Yellow"), MyColor(val: "Red")]
var body: some View {
ForEach(colors, id: \.uuid) { color in
ButtonColorView(color: color.val)
}
}
}
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
if visible {
return AnyView( HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
})
} else {
return AnyView(
Button("Visible") {
self.visible.toggle()
}
)
}
}
}
class MyColor: Identifiable {
let uuid = UUID()
let val: String
init(val: String) {
self.val = val
}
}
Unfortunately it's not working, the views inside the ForEach do not change when the Button is pressed. I replaced the Foreach with ButtonColorView(color: colors[0].val) and it seems to work, so I'd say the problem is at ForEach.
I also tried breakpoints in ButtonColorView and it seems the view is called when the Button is triggered returning the right view, anyways the view does not update on screen.
So, am I using the ForEach in a wrong way ?
This problem occurs in a more complex app, but I tried to extract it in this small example. To summarize it: I need ButtonColorView to return different Views depending of its state (visibility in this case)
PS: I'm using Xcode 11 Beta 6
You are using ForEach correctly. I think it's the if statement within ButtonColorView's body that's causing problems. Try this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
Button(visible ? "Invisible" : "Visible") {
self.visible.toggle()
}
if visible {
Text(color)
}
}
}
}
You can also try something like this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
if visible {
HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
}
} else {
Button("Visible") {
self.visible.toggle()
}
}
}
}
}