SwiftUI Picker in Forms (iOS 16) - ios

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.

Related

SwiftUI throwing 'Fatal error: Index out of range' when adding element for app with no explicit indexing

Why, in the following app when clicking through to 'Nice Restaurant' and trying to add a contributor, does the app crash with the error:
Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range?
The error, in the Xcode debugger, has no obviously useful stack trace and points straight at the '#main' line.
There are no explicit array indices used in the code nor any uses of members like .first.
I'm using Xcode Version 13.4.1 (13F100)
I'm using simulator: iPhone 13 iOS 15.5 (19F70)
import SwiftUI
struct CheckContribution: Identifiable {
let id: UUID = UUID()
var name: String = ""
}
struct Check: Identifiable {
var id: UUID = UUID()
var title: String
var contributions: [CheckContribution]
}
let exampleCheck = {
return Check(
title: "Nice Restaurant",
contributions: [
CheckContribution(name: "Bob"),
CheckContribution(name: "Alice"),
]
)
}()
struct CheckView: View {
#Binding var check: Check
#State private var selectedContributor: CheckContribution.ID? = nil
func addContributor() {
let newContribution = CheckContribution()
check.contributions.append(newContribution)
selectedContributor = newContribution.id
}
var body: some View {
List {
ForEach($check.contributions) { $contribution in
TextField("Name", text: $contribution.name)
}
Button(action: addContributor) {
Text("Add Contributor")
}
}
}
}
#main
struct CheckSplitterApp: App {
#State private var checks: [Check] = [exampleCheck]
var body: some Scene {
WindowGroup {
NavigationView {
List {
ForEach($checks) { $check in
NavigationLink(destination: {
CheckView(check: $check)
}) {
Text(check.title).font(.headline)
}
}
}
}
}
}
}
I've noticed that:
If I unroll the ForEach($checks) the crash doesn't occur (but I need to keep the ForEach so I can list all the checks)
If I don't take a binding to the CheckContribution (ForEach($check.contributions) { $contribution in then the crash doesn't occur (but I need the binding so subviews can modify the CheckContribution
If I don't set the selectedContributor then the crash doesn't occur (but I need the selectedContributor in the real app for navigation purposes)
The cleanest way I could find that actually works is to further separate the nested ForEach into a subview and bind the contributors array to it.
struct CheckView: View {
#Binding var check: Check
#State private var selectedContributor: CheckContribution.ID? = nil
func addContributor() {
let newContribution = CheckContribution()
check.contributions.append(newContribution)
selectedContributor = newContribution.id
}
var body: some View {
List {
ContributionsView(contributions: $check.contributions)
Button(action: addContributor) {
Text("Add Contributor")
}
// Test that changing other properties still works.
Button("Change title", action: changeTitle)
}
.navigationTitle(check.title)
}
func changeTitle() {
check.title = "\(Int.random(in: 1...100))"
}
}
struct ContributionsView: View {
#Binding var contributions: [CheckContribution]
var body: some View {
ForEach($contributions) { $contribution in
TextField("Name", text: $contribution.name)
}
}
}
I'm still not sure about the internals of SwiftUI, and why it works this way. I hope it helps. And maybe another more experienced user can provide a clear explanation to this.
If you really want the Button to be in the List, then you could try this approach using a separate view, works well for me:
struct CheckView: View {
#Binding var check: Check
#State private var selectedContributor: CheckContribution.ID? = nil
var body: some View {
List {
ForEach($check.contributions) { $contribution in
TextField("Name", text: $contribution.name)
}
AddButtonView(check: $check) // <-- here
}
}
}
struct AddButtonView: View {
#Binding var check: Check
func addContributor() {
let newContribution = CheckContribution(name: "new contribution")
check.contributions.append(newContribution)
}
var body: some View {
Button(action: addContributor) {
Text("Add Contributor")
}
}
}
I had the same error but with tabview. Moreover, the fall was only on iOS 15, but on iOS 16 it worked perfectly and there were no falls.
I tried both through indexes, and through checking for finding the index inside the range, but nothing helped.
The solution was found in the process of debugging: I noticed that it was falling even before the predstavlenie appeared (it worked Appear).
I did a simple check to see if the data array is empty
if !dataArray.isEmpty {
TabView(selection: $selection) {
ForEach(dataArray, id: \.self) { item in
...
}
}
}
And it worked - there were no more crashes on iOS 15. Apparently there was some problem with the processing of empty arrays before iOS 16.

Why does this SwiftUI List require an extra objectWillChange.send?

Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}

SwiftUI Popover doesn't detect Forms

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

Complete list is recreates all views even if just one item has changed

I have a very simple schoolbook example of a SwiftUI List view that renders items from data in an array. Data in the array is Identifiable. But, when I change the the data in the array, add or remove a item then all rows in the list view are recreated. Is that correct? My understanding was that Identifiable should make sure that only the view in the list that are changed are recreated.
My list is inside a navigation view and each row links to a detail view. The problem is that since all items in the list are removed and recreated every time the data is changed then if that that happens when Im in a detail view (it's triggered by a notification) then Im thrown out back to the list.
What am I missing?
Edit: Added code example
This is my data struct:
struct Item: Identifiable {
let id: UUID
let name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
This is my ItemView
struct ItemView: View {
var item: Item
init(item: Item) {
self.item = item
print("ItemView created \(self.item.id)")
}
var body: some View {
Text(self.item.name)
}
}
An finally my list view:
struct KeyList: View {
#State var items = [Item(name: "123"), Item(name: "456"), Item(name: "789")]
var body: some View {
VStack {
List(self.items) { item in
ItemView(item: item)
}
Button(action: {
self.items.append(Item(name: "New"))
}) {
Text("Add")
}
}
}
}
When I press add it will print "ItemView created" 4 times. My understanding is that it should only do it 1 time?
Here is an example of how this could work. Tested and working on iOS 13.5
The List doesn't get recreated again when only one item is being removed. So this was accomplished.
About the poping of the View this has already been answered here:
SwiftUI ForEach refresh makes view pop
I have here a small workaround for this problem. Add the items you want to remove to an array. Then when going back, remove these items (Which will make the view pop) or go back programmatically and nothing gets removed
struct ContentView: View {
#State var text:Array<String> = ["a", "b", "c"]
var body: some View {
NavigationView() {
VStack() {
List() {
ForEach(self.text, id: \.self){ item in
NavigationLink(destination: SecondView(textItem: item, text: self.$text)) {
Text(item)
}
}
}
Button(action: {
self.text.remove(at: 0)
}){
Text("Remove \(self.text[0])")
}
}
}
}
}
struct SecondView: View {
#State var textItem: String
#Binding var text: Array<String>
#State var tmpArray: Array<String> = []
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack() {
Text(self.textItem)
Button(action: {
//Append to a tmp array which will later be used to determine what to remove
self.tmpArray.append(self.text[0])
}){
Text("Remove \(self.text[0])")
}
Button(action: {
if self.tmpArray.count > 0 {
//remove your stuff which will automatically pop the view
self.text.remove(at: 0)
} else {
// programmatically go back as nothing has been deleted
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("Go Back")
}
}
}
}

Views do not update inside the ForEach in SwiftUI

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()
}
}
}
}
}

Resources