Dual Line Form Picker - swiftui

I am trying to create a form picker that displays two rows of data / entry. The top row displays correctly showing 2 items with slightly different fonts. The third item is displaying on the second line separated by a horizontal form line. I'm also getting two check marks--one for each line.
...
var currCountry: [String] = ["con 1", "con 2", "con 3", "con 4"]
var currSymbol: [String] = ["sym 1", "sym 2", "sym 3", "sym 4"]
var currName: [String] = ["name 1", "name 2", "name 3", "name 4"]
#ObservedObject var userData = UserData()
var body: some View {
NavigationView {
Form {
Picker("select", selection: $userData.entry) {
ForEach(0 ..< self.currCountry.count) { i in
HStack {
Text(currCountry[i])
.font(.caption)
Text(currSymbol[i])
.font(.caption2)
}
Text(currName[i])
.font(.caption2)
}
}
}.navigationBarTitle("important info")
}
}
...

Unsure what your question is, so I'll take a leap. Are you referring to that you want to have two items horizontally, and then the third vertical to that? Such as:
VStack {
HStack {
Text(currCountry[i])
.font(.caption)
Text(currSymbol[i])
.font(.caption2)
}
Text(currName[i])
.font(.caption2)
}
This way there is only one checkmark at this point? If this isn't want you were referring to, can you please give an example of what type of output you are looking for.

Related

ProgressView inside SwiftUI List disappearing after list is updated

The circular (default) SwiftUI ProgressView disappears within a List whenever the list gets expanded with new content because of a .onAppear modifier on the ProgressView. 
The ProgressView becomes invisible, although its allocated cell (and its label, if assigned) within the list remain visible.
I want to keep the ProgressView at the bottom of the list so that the list gets automatically updated with new content being downloaded from the server when the user reaches the end of the list.
Possible duplicate of this article but the accepted answer does not tackle the reason why the view disappears.
Reprex:
struct ContentView: View {
#State var items: [String] = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
.frame(height: 100)
}
ProgressView {
Text("Loading")
}
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .center)
.onAppear(perform: endOfListReached)
}
}
func endOfListReached() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
items.append("Item \(items.count + 1)")
}
}
}
You could try this alternative approach using a ScrollViewReader and ScrollView
to keep the ProgressView at the bottom of the list
so that the list gets automatically updated with new content being
downloaded from the server when the user reaches the end of the list.
struct ContentView: View {
#State var items: [String] = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"]
#Namespace var bottomID // id for the ProgressView
var body: some View {
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false){
ForEach(items, id: \.self) { item in
Text(item).frame(height: 100)
}
ProgressView { Text("Loading") }.id(bottomID)
}
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .center)
.onAppear(perform: endOfListReached)
.onChange(of: items) { _ in
withAnimation {
proxy.scrollTo(bottomID) // <-- scroll to the ProgressView
}
}
}
}
func endOfListReached() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
items.append("Item \(items.count + 1)")
if items.count < 15 {
endOfListReached() // <--- testing up to 15
}
}
}
}
I would rather keep the List for its convenient layout, but it appears that the List is causing the issue here and that changing it to a ScrollView and LazyVStack and styling it with Dividers works as expected.

Why is my SwiftUI #State variable null the fist time my sheet appears?

In the view shown below there is a simple list of 3 items. When the user selects an item in the list a sheet is presented showing the name of the item selected. However, when I run it, the first time I select an item it shows "item was null". If I select another item in the list it works perfectly. If I go back and select the first item again, it now works.
Can someone tell me why the selectedItem variable is null the first time a button is clicked?
struct TestView: View {
#State private var showSheet = false
#State private var selectedItem: String?
var body: some View {
List {
Button(action: {
selectedItem = "Item 1"
showSheet = true
}, label: { Text("Item 1") })
Button(action: {
selectedItem = "Item 2"
showSheet = true
}, label: { Text("Item 2") })
Button(action: {
selectedItem = "Item 3"
showSheet = true
}, label: { Text("Item 3") })
}
.sheet(isPresented: $showSheet) {
Text(selectedItem ?? "item was null")
}
}
}
As discussed in the comments, you're going to need to use .sheet(item:). The reason for this is that the common .sheet(isPresented:) is created before the button is tapped.
When using .sheet(item:) the string needs to be identifiable, but you can achieve that with a simple extension.
And now you also don't need the showSheet bool, because the sheet is now bound to selectedItem. Keep in mind that this means that selectedItem is set to nil when the sheet is dismissed.
An example of what this solution could look like for your code is as follows:
struct TestView: View {
#State private var selectedItem: String?
var body: some View {
List {
Button(action: {
selectedItem = "Item 1"
}, label: { Text("Item 1") })
Button(action: {
selectedItem = "Item 2"
}, label: { Text("Item 2") })
Button(action: {
selectedItem = "Item 3"
}, label: { Text("Item 3") })
}
.sheet(item: $selectedItem, content: { item in
Text(item)
})
}
}
extension String: Identifiable {
public var id: String { return self }
}

How to display default detail in NavigationSplitView after deleting an item — on iPad

I have a NavigationSplitView (SwiftUI 4, iOS16), list of items in the left part, detail on right. When I run the application, no item selected, it displays "Select item" on the right (detail part). I select an item and it displays item detail on right. So far so good.
Now I delete the item on the left by swiping to the left... but the right part still displays the deleted item detail.
Is there any way to get right part go back to the not-selected detail?
Please notice this behaviour can be observed on iPad only, not on iPhone, as iPhone does not display both parts of NavigationSplitView together.
import SwiftUI
struct ContentView: View {
#State var items = ["item 1", "item 2", "item 3", "item 4"]
var body: some View {
NavigationSplitView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: Text(item)) {
Text(item)
}
}
.onDelete(perform: { offsets in
items.remove(atOffsets: offsets)
})
}
} detail: {
Text("Select item")
}
}
}
Bind your selection with List using #State property and also you don't require to add NavigationLink, NavigationSplitView automatically show detail for the selected list item.
Now if you remove the selected item from the list then after deleting it from the array simply set nil to the selection binding of List.
struct ContentView: View {
#State var items = ["item 1", "item 2", "item 3", "item 4"]
#State private var selection: String?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: { offsets in
items.remove(atOffsets: offsets)
guard selection != nil, !items.contains(selection!) else { return }
selection = nil
})
}
} detail: {
if let selectedItem = selection {
Text(selectedItem)
}
else {
Text("Select item")
}
}
}
}
Suggest you to check the Apple documentation of NavigationSplitView for more details.

Cycle through a Picker selection from Array

I'm quite new to Swift and coding in general so apologies if this is a super simple question.
I'm trying to add a button either side of a Picker to allow the user to move up/down the selections within the Picker (my Picker is populated from an Array) - I'm using this selection in another part of my App.
The code below works but only updates the example Text, but it does not update the selection: within the pickerto update correctly.
Any ideas what I'm doing wrong?
import SwiftUI
struct ContentView: View {
let myArray = ["Period 1", "Period 2", "Period 3", "Period 4", "Period 5", "Period 6", "Period 7", "Period 8", "Period 9", "Period 10", "Period 11", "Period 12", "Period 13"]
#State var currentIndex = 0
var body: some View {
VStack(spacing: 20) {
HStack {
Button(action: {
if currentIndex == 0 {
} else {
currentIndex -= 1
}
}) {
Image(systemName: "chevron.left.circle")
.imageScale(.large)
}
.padding(2)
.frame(maxWidth: .infinity, alignment: .leading)
Picker(selection: $currentIndex, label: Text("Picker")) {
ForEach(myArray, id: \.self) {
Text($0)
}
}
Button(action: {
if currentIndex == 12 {
} else {
currentIndex += 1
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
}
.padding(2)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
Text("\(myArray[currentIndex])")
}
}
}
'''
The problem here is that you are programmatically updating your State variable currentIndex but you are not modifying the array selection. To make this work you need to iterate the array over the indices and not the elements so change the Picker code to this
Picker(selection: $currentIndex, label: Text("Picker")) {
ForEach(myArray.indices) { index in
Text(myArray[index])
}
}
Here each item in the picker automatically gets the id of the raw value of index which matches with currentIndex which makes this work.
To work directly with the elements of the array here is an alternative solution where a new State variable is added to hold the selected string.
#State var currentSelection = ""
#State var currentIndex = 0 {
didSet {
currentSelection = myArray[currentIndex]
}
}
The picker code gets changed to
Picker(selection: $currentSelection, label: Text("Picker")) {
ForEach(myArray, id: \.self) { period in
Text(period)
}
}
but the rest of the code is the same since the buttons are still using currentIndex

Editable TextField in SwiftUI List

UPDATE: 14 months later, there is this intriguing note in the AppKit release notes:
A TextField that you are editing inside a selected List row now has correct text foreground colors. (68545878)
Now when placing a TextField in a List, the TextField becomes focused on click the very first time it is selected, but subsequent editing attempts fail: the List row is selected but the TextField does not gain focus.
O/P:
In a beta6 SwiftUI macOS (not iOS) app, I need to have a List with editable text fields, but the TextFields in the list are not editable. It works fine if I swap out List for Form (or just VStack), but I need it working with a List. Is there some trick to tell the list to make the fields editable?
import SwiftUI
struct ContentView: View {
#State var stringField: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $stringField).environment(\.isEnabled, true)
TextField("Line 2", text: $stringField)
TextField("Line 3", text: $stringField)
TextField("Line 4", text: $stringField)
TextField("Line 5", text: $stringField)
}
}
}
}
The following code is only an experiment to understand the character of List in SwiftUI and show an alternative. What I understand from observing the output from various combinations is that, List View's style is structured in a way to override the default behaviors of underlying View to become Selectable. This means that TextField does absolutely different. TextField is an focusable element where we can type. This focusing variable is not wired in to List View to work together. Hence, List override default focusable. Hence it is not possible to create List with TextView. But if you need, next best option is ScrollView instead of List and do the styling explicitly. Check the following code and both ways.
import SwiftUI
struct ContentView: View {
#State private var arr = ["1","2","3"]
var body: some View {
HStack {
VStack {
List {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
}
}
}
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
VStack {
ScrollView {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
.textFieldStyle(PlainTextFieldStyle())
.padding(2)
}
}
.padding(.leading, 5)
.padding(3)
}
.background(Color(NSColor.alternatingContentBackgroundColors[0]))
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
}
}
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
Bug Report
I updated the project to target a MacOS app and found the bug you are reporting. I've updated Apple with this feedback because it indeed does seem to be a bug (Welcome to Beta).
FB7174245 - SwiftUI Textfields embedded in a List are not editable
when target is macOS
Update
So what's the point of all the focus on state and binding below? One variable should be bound to a single control. Here is a working example. I'll leave up the older answer as it carries the example forward with a full app saving and retrieving data to/from CoreData.
import SwiftUI
struct ContentView: View {
#State var field1: String = ""
#State var field2: String = ""
#State var field3: String = ""
#State var field4: String = ""
#State var field5: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $field1).environment(\.isEnabled, true)
TextField("Line 2", text: $field2)
TextField("Line 3", text: $field3)
TextField("Line 4", text: $field4)
TextField("Line 5", text: $field5)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
BTW - This works on Xcode Beta (11M392r) with MacOS Catalina - 10.15
Beta (19A546d).
Sample Project
Check out this sample that includes an editable Textfield that writes to CoreData, which I am building on Github.
Take a look at the difference between #State and #Binding so that you
can manipulate data from outside of the content view. (60-sec
video)
struct ContentView: View {
// 1.
#State var name: String = ""
var body: some View {
VStack {
// 2.
TextField(" Enter some text", text: $name)
.border(Color.black)
Text("Text entered:")
// 3.
Text("\(name)")
}
.padding()
.font(.title)
}
}
source
Check out the following answer for the appropriate use-case of #State
(SO Answer)
Does get focus—if you tap just right
Using Xcode 12.4 and SwiftUI 2 for a macOS app, I seemed to have the same problem: Could not make TextEdit work inside a List. After reading here, and experimenting some more, I realized that in my case the TextField does reliably get the focus, but only if you tap in just the right way. I think and hope this is not how it should be working, so I posted the question TextField inside a List in SwiftUI on macOS: Editing not working well, explaining the details of my observations.
In summary: Single-tap exactly on existing text does give focus (after a small, annoying, delay). Double-tap anywhere does not give focus. Single-tap anywhere in an empty field does give focus.
SwiftUI 2.0
You can use TextEditor instead of TextField in lists in order to get editable text fields.
TextEditor(text: $strings)
.font(.body)
.cornerRadius(5)
.shadow(color: Color.black.opacity(0.18), radius: 0.8, y: 1)
.frame(height: 20)
This way you will get a similar aspect between the TextEditor and TextField.