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
Related
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.
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 }
}
The following code shows different Pickers.
I want to achieve: MenuPickerStyle with Sections and custom Label like in second example https://imgur.com/a/Q0uxEmh but it is not selectable because it is grayed out
When doing it with the Menu container as recommended in the internet the rows are grayed out. How can I get the wanted behavior?
In the code you can see 4 different examples. The second is the grayed out picker rows.
Tested in Xcode 13.2.1, iOS 15
struct ContentView: View{
#State var number_1 = 0
#State var number_2 = 0
#State var number_3 = 0
#State var number_4 = 0
let numbers = [0,1,2,3,4]
var body: some View{
VStack{
//Picker works, no Section --------------------------
Menu(content: {
Picker(selection: $number_1, content: {
ForEach(numbers,id: \.self){i in
Text("Number: \(i)").tag(i)
}
}, label: {EmptyView()})
}, label: {
Text("selected Number = \(number_1)")
})
//Picker is grayed out ---------------------------
Menu(content: {
Picker(selection: $number_2, content: {
ForEach(numbers,id: \.self){i in
Section{
Text("Number: \(i)").tag(i)
}
}
}, label: {EmptyView()})
}, label: {
Text("selected Number = \(number_2)")
})
//Picker works, collapsed View not desired behavior --------
Menu(content: {
Picker(selection: $number_3, content: {
ForEach(numbers,id: \.self){i in
Section{
Text("Number: \(i)").tag(i)
}
}
}, label: {EmptyView()})
.pickerStyle(MenuPickerStyle())
}, label: {
Text("selected Number = \(number_3)")
})
//Picker Works, label not ----------------------------
Picker(selection: $number_4, content: {
ForEach(numbers,id: \.self){i in
Section{
Text("Number: \(i)").tag(i)
}
}
}, label: {Text("selected Number = \(number_4)")})
.pickerStyle(MenuPickerStyle())
}
}
}
You can put multiple Pickers in a Menu, each with a subset of the available values, and they’ll all work as expected:
Menu {
// First section, with values 0–2
Picker(selection: $num) {
ForEach(0..<3, id: \.self) { i in
Text("Number: \(i)").tag(i)
}
} label: {
EmptyView()
}
// Second section, with values 3–5
Picker(selection: $num) {
ForEach(3..<6, id: \.self) { i in
Text("Number: \(i)").tag(i)
}
} label: {
EmptyView()
}
} label: {
Text("selected Number = \(num)")
}
SwiftUI will even stick a separator between the Pickers automatically.
The simplest fix is for case 4 - just rebuild menu (force-refresh) on selection changed:
//Picker Works, label not ----------------------------
Picker(selection: $number_4, content: {
ForEach(numbers,id: \.self){i in
Section{
Text("Number: \(i)").tag(i)
}
}
}, label: {Text("selected Number = \(number_4)")})
.pickerStyle(MenuPickerStyle())
.id(number_4) // << here !!
Tested with Xcode 13.4 / iOS 15.5
I think I've got a workaround.
I tried to use a menu and use Buttons for manipulating the selection. The problem is the mark of the selection if there is already a Image in the label of the button I think.
struct ContentView: View{
#State var number = 0
let numbers = [1,2,3,4]
var body: some View{
Menu(content: {
ForEach(numbers,id:\.self){i in
Section{
Button(action: {
number = i
}, label: {
Text("Number: \(i)")
})
}
}
}, label: {
Text("The selection is: \(number)")
.frame(width: UIScreen.main.bounds.width, height: 20)
.border(Color.green)
})
}
}
I'm not 100% certain I understand, but if your goal is to build a picker that lets the user click on the title and then select an option from a menu, returning the selection to the screen I would suggest doing it as follows:
struct SwiftUIView: View {
#State var option:Int = 0
let numbers = [1,2,3,4]
var body: some View {
VStack {
//Picker to select the option
Picker("Option Picker", selection: $option) {
ForEach(numbers, id: \.self) {
number in
Text("Selection Number \(number)").tag(number)
}
}.pickerStyle(MenuPickerStyle())
//Text confirming the selection won't show before a pick is made
Text(option != 0 ? "You selected option number \(option)" : "")
}
}
}
Let me know if that works for you / is what you were looking for.
I have a horizontal scrollview with a list of items, I want to change background color color of items when the user taps on it. This is my code but when I run it and click on items nothing happens.
struct HorizontalList: View {
var list = ["item 1", "item 2", "item 3", "item 4", "item 5", "item 6", "item 7", "item 8", "item 9", "item 10"]
#State var selectedIndex = 0
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(0..<list.count) { index in
ListItem(isSelected: selectedIndex == index, label: list[index])
.listRowBackground(Color.blue)
.onTapGesture {
selectedIndex = index
}
}
}
}
}
}
struct ListItem: View {
#State var isSelected: Bool
#State var label: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(isSelected ? Color.blue : Color.clear)
.frame(minHeight: 16, idealHeight: 16, maxHeight: 16)
Text(label)
}
}
}
You need to use a .simultaneousGesture as the list is using a DragGesture behind the scenes.
.simultaneousGesture(
TapGesture()
.onEnded({ _ in
selectedIndex = index
})
)
Edit:
See the comments in the code.
struct HorizontalList: View {
var listItems = Array(1...10).map( { ListItem(text: "Item \($0)") })
#State var selectedIndex = 0
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
// It is good practice to always supply a ForEach with an identifiable. If you were
// to try to delete an element in the array, you can cause a crash without this.
// What Array(zip()) does is takes listItems and listItems.indices and put them together
// into an array of pairs, requiring two arguments in the ForEach, one for the item and
// one for the index. You can then use both in the loop. However, the ForEach tracks them by
// the identifiable element, not the index which is what the id: \.0 does.
ForEach(Array(zip(listItems, listItems.indices)), id: \.0) { item, index in
ListItemView(isSelected: selectedIndex == index, label: item.text)
.listRowBackground(Color.blue)
.simultaneousGesture(
TapGesture()
.onEnded({ _ in
selectedIndex = index
})
)
}
}
}
}
}
struct ListItemView: View {
// There is no reason for these to be an #State var unless this view changes them. If it does,
// it really should be a Binding to pass the data back.
let isSelected: Bool
let label: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(isSelected ? Color.blue : Color.clear)
.frame(minHeight: 16, idealHeight: 16, maxHeight: 16)
Text(label)
}
}
}
// This becomes the item you itereate on
struct ListItem:Hashable, Identifiable {
let id = UUID()
var text: String
}
I have a SwiftUI app that has two columns and a toolbar. I'm trying to emulate the latest macOS applications and use the toolbar in the same way that Mail does, i.e. select a sidebar item and show that as the title of the window.
Here is my code:
import SwiftUI
var listItems = ["Item 1", "Item 2", "Item 3", "Item 4"]
var secondItems = ["Second 1", "Second 2", "Second 3", "Second 4"]
struct ContentView: View
{
#State var select: String? = "Item 1"
var body: some View
{
VStack
{
NavigationView
{
List
{
ForEach((0..<listItems.count), id: \.self)
{index in
NavigationLink(destination: SecondView(), tag: listItems[index], selection: $select)
{
Text(listItems[index])
.padding(.vertical, 2.0)
}
}
Spacer()
}.frame(width:160)
}
.toolbar
{
Text("this is not the title")
Button(action: {})
{
Label("Upload", systemImage: "square.and.arrow.up")
}
}
}
}
}
struct SecondView: View
{
var body: some View
{
NavigationView
{
List
{
ForEach((0..<secondItems.count), id: \.self)
{index in
NavigationLink(destination: Text(secondItems[index]))
{
Text(secondItems[index])
.frame(height: 20)
}
}
}.frame(width:150)
}
}
}
So I get a toolbar like this:
but how do I set the title of the window in the toolbar when I select an item in the first list?
Here is how I solved it:
import SwiftUI
var listItems = ["Item 1", "Item 2", "Item 3", "Item 4"]
var secondItems = ["Second 1", "Second 2", "Second 3", "Second 4"]
struct ContentView: View
{
#State var select: String? = "Item 1"
var body: some View
{
VStack
{
NavigationView
{
List
{
ForEach((0..<listItems.count), id: \.self)
{index in
NavigationLink(destination: SecondView(), tag: listItems[index], selection: $select)
{
Text(listItems[index])
.frame(height: 20)
}
}
Spacer()
}
.listStyle(SidebarListStyle())
}
.toolbar
{
Text("this is not the title")
Button(action: {})
{
Label("Upload", systemImage: "square.and.arrow.up")
}
}
.navigationTitle(select!)
.navigationSubtitle("\(listItems.count) records")
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
}
struct SecondView: View {
var body: some View {
NavigationView {
List
{
ForEach((0..<secondItems.count), id: \.self)
{index in
NavigationLink(destination: Text(secondItems[index]))
{
Text(secondItems[index])
.frame(height: 20)
}
}
}
}
}
}
HStack(spacing: 0) {
ToolView()
Divider()
PanelView()
}
.frame(width: 290)
.toolbar(id: "id") {
ToolbarItem(id: "sidbar", placement: .primaryAction) {
Button {
} label: {
Label {
Text("Stack")
} icon: {
Image("stack")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.offset(y: 10)
}
}
}
}
Use id to create ToolbarItem and you can show the title with right click on the toolbar.
Use Button to show the content, and the label and image will fit the toolbar.
NOTE: if you have multiple toolbars, make sure to provide each one with an id, or you still won't be able to show the title.
It is best to use SF Symbols, but if you use a custom font, make sure to resize it, and offset it.
If you are attempting to change the text that is currently set to "toolbar", you want to set the navigation title. This is done via .navigationTitle(String) and .navigationSubtitle(String).