With some items of my picker, the text of the selected item takes 2 lines, how can I avoid this ?
There is my code :
struct PickerTestView: View {
#AppStorage("firstNotificationSelection") var firstNotificationSelection: String = (UserDefaults.standard.string(forKey: "firstNotificationSelection")) ?? "None"
#AppStorage("secondNotificationSelection") var secondNotificationSelection: String = (UserDefaults.standard.string(forKey: "secondNotificationSelection")) ?? "None"
let notificationChoices: [String] = ["None", "At time of match", "5 minutes before...", "10 minutes before...", "30 minutes before...", "1 hour before...", "2 hours before..."]
var body: some View {
NavigationStack {
Form {
Section {
Picker(selection: $firstNotificationSelection) {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
} label: {
Text("Alert")
}
if firstNotificationSelection != "None" {
Picker(selection: $secondNotificationSelection) {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
} label: {
Text("Second alert")
}
}
}
}
.navigationTitle("Notifications")
.navigationBarTitleDisplayMode(.inline)
}
}
}
I tried with the modifier .lineLimit(1) and to fix the frame of the Picker but it has no effect.
you have more control if you wrap your picker inside a Menu
Menu {
Picker(selection: $firstNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(firstNotificationSelection)
}
or, in the full context of your example, it would look like this
struct PickerTestView: View {
#AppStorage("firstNotificationSelection") var firstNotificationSelection: String = (UserDefaults.standard.string(forKey: "firstNotificationSelection")) ?? "None"
#AppStorage("secondNotificationSelection") var secondNotificationSelection: String = (UserDefaults.standard.string(forKey: "secondNotificationSelection")) ?? "None"
let notificationChoices: [String] = ["None", "At time of match", "5 minutes before...", "10 minutes before...", "30 minutes before...", "1 hour before...", "2 hours before..."]
var body: some View {
NavigationStack {
Form {
Section {
HStack {
Text("Alert")
Spacer()
Menu {
Picker(selection: $firstNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(firstNotificationSelection)
}
}
if firstNotificationSelection != "None" {
HStack {
Text("Second Alert")
Spacer()
Menu {
Picker(selection: $secondNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(secondNotificationSelection)
}
}
}
}
}
.navigationTitle("Notifications")
.navigationBarTitleDisplayMode(.inline)
}
}
}
As of today (feb 2023) the default SwiftUI picker does not have an option to change the number of lines for the selected value text
Related
Hi all thanks in advance.
I am having an issue when running app (not in preview), a textfield is not updating the state. I've not continued to expand the MVVM yet as I am getting caught up in this UI/Binding issue.
Not sure what have I missed here? I am passing a StateObject (view model instance) into the EnvironmentObject list, which is then accessed from an EnvironmentObject and the models array of elements in a view is iterated over, then further passing the iterated elements of the array to a Binding in another view which is then bound to a textfield to be edited by the user?
Specifically, the issue is:
When swipe action > edit on an expense in the ContentView to navigate to EditExpenseView, the textfields don't allow editing.
Note:
If I move the textfield up to the ExpenseList View, the binding to edit works. I thought that maybe the List(items) was the issue because it's iterating over an immutable collection.
I am using the index and passing the array binding via $expenses[index] which is avoiding accessing the immutable collection as its only being used to get the index of the list item the user will edit.
If your still reading, thanks for being awesome!
Let me know if I can add any further information or provide clarity.
Expense Model:
struct Expense: Equatable, Identifiable, Codable {
init(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus, budgetId: UUID?) {
self.date = date
self.description = description
self.amount = amount
self.type = type
self.status = status
self.budgetId = budgetId
}
static func == (lhs: Expense, rhs: Expense) -> Bool {
lhs.id == rhs.id
}
var id: UUID = UUID()
var date: Date
var description: String
var amount: Decimal
var type: ExpenseType
var status: ExpenseStatus
var budgetId: UUID?
}
ExpenseViewModel:
class ExpenseViewModel: ObservableObject, Identifiable {
#Published var expenses: [Expense] = []
func insertExpense(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus) -> Void {
expenses.insert(Expense(date: date, description: description, amount: amount, type: type, status: status, budgetId: nil), at:0)
}
func remove(_ expense: Expense) {
expenses.removeAll(where: {$0.id == expense.id})
}
}
App Entry:
import SwiftUI
#main
struct iBudgeteerApp: App {
#StateObject private var expenses = ExpenseViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(expenses)
}
}
}
Initial View:
struct ContentView: View {
#EnvironmentObject private var model: ExpenseViewModel
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
NavigationStack {
VStack {
Button("Add Row") {
model.insertExpense(date: Date(), description: "Groceries", amount: 29.94, type: .Expense, status: .Cleared)
}
ExpenseList(expenses: $model.expenses)
}
}
}
}
Expense List View:
struct ExpenseList: View {
#Binding var expenses: [Expense]
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
index in
HStack {
Text("\(index + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expenses[index].date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expenses[index].description)
}
HStack {
Text(expenses[index].description)
Spacer()
Text("\(expenses[index].amount as NSNumber, formatter: formatter)")
.foregroundColor( expenses[index].type == .Expense ? .red : .green)
Image(systemName: expenses[index].type == .Expense ? "arrow.down" : "arrow.up").foregroundColor( expenses[index].type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { expenses.remove(at: index) } ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: self.$expenses[index])
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
}
}
}
Edit Expense View:
struct EditExpenseView: View {
#Binding var expense: Expense
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
Form {
Section(header: Text("Editing: \(expense.description)")) {
VStack {
DatePicker(
"Date",
selection: $expense.date,
displayedComponents: [.date]
)
HStack {
Text("Name")
Spacer()
TextField("description",text: $expense.description)
.fixedSize().multilineTextAlignment(.trailing)
}
HStack {
Text("Amount")
Spacer()
TextField("0.00", value: $expense.amount, formatter: formatter).fixedSize()
}
Picker("Status", selection: $expense.status) {
ForEach(ExpenseStatus.allCases, id: \.self) {
status in
Text("\(status.rawValue)")
}
}
Picker("Type", selection: $expense.type) {
ForEach(ExpenseType.allCases, id: \.self) {
type in
Text("\(type.rawValue)")
}
}
}
}
}
}
}
UPDATE
It works in:
List ($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
ForEach($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
But not in:
List($expenses) {
$expense in
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer() }
HStack {
Text(expense.description)
Spacer()
Text("\(expense.amount as NSNumber, formatter: formatter)")
.foregroundColor( expense.type == .Expense ? .red : .green)
Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up").foregroundColor(expense.type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { //expenses.remove(expense)
} ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: $expense)
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
Disclaimer:
I couldnĀ“t test this answer properly as your example is missing information and is not reproducible. Please consider posting a minimal reproducible example.
The issue is in these lines:
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
and then doing:
EditExpenseView(expense: self.$expenses[index])
You are not passing a binding reference of Expense on to your EditExpenseView but a binding to a copy of it. You are breaking the binding chain.
The following aproach should yield the desired result:
List ($expenses) { $expense in
HStack {
Text("\(expenses.firstIndex(of: expense) + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expense.description)
}
.....
and passing your Expense on to your subview:
EditExpenseView(expense: $expense)
I have a view with two TextFields. When the first one is focused, I'd like to display Next button in the toolbar and when the second text field is focused, I'd like to present Previous and Done buttons in the toolbar.
I have an if statement inside the toolbar, but it looks like it doesn't pick up the change of #FocusState until I type something.
Any ideas how to make it work properly or why doesn't the toolbar pick up the changes?
This is more or less the code (I simplified the actual code):
import SwiftUI
import Combine
enum Field {
case inLangName
case outLangName
}
struct MyView: View {
#FocusState private var focusedTextField: Field?
#State var inLangName: String = ""
#State var outLangName: String = ""
var body: some View {
NavigationView {
VStack(spacing: 15) {
TextField("In lang name", text: $inLangName)
.focused($focusedTextField, equals: .inLangName)
.simultaneousGesture(TapGesture().onEnded { _ in
focusedTextField = .inLangName
})
TextField("Out lang name", text: $outLangName)
.focused($focusedTextField, equals: .outLangName)
.simultaneousGesture(TapGesture().onEnded { _ in
focusedTextField = .outLangName
})
}
.navigationBarHidden(true)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
if inLangName.isEmpty {
focusedTextField = .inLangName
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focusedTextField == .inLangName || focusedTextField == nil {
Spacer()
Button(action: {
focusedTextField = .outLangName
}) {
Text("next")
}
} else if focusedTextField == .outLangName {
Button(action: {
focusedTextField = .inLangName
}) {
Text("previous")
}
Spacer()
Button(action: {
//onDoneButtonClicked()
}) {
Text("done")
}
}
}
}
}
}
}
After removing all the items from a list and then adding the items back to the list, each list item is indented like it is in edit mode and swipe actions are unavailable. I only see the issue when I have the conditional checking if the array is empty.
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
}
}
}
}
Before removing items from array:
After removing items and adding new items to array:
here is an example that shows that deletion is not the problem
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
} else {
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
// .swipeActions(allowsFullSwipe: false) {
// Button(role: .destructive) {
// categories.remove(at: i)
// } label: {
// Label("Delete", systemImage: "trash.fill")
// }
// }
.onTapGesture {
categories.remove(at: i)
}
}
}
}
}
}}
the problem is that after deleting an element from the list with swipeActions the list is supposed to reposition itself, doing so just after deleting the last element from the list with swipeActions you decide to disappear the list so it will not have the time to finish his action.
I suggest the following code which works fine
struct TestView: View {
#State var categories = ["dog", "cat"]
var body: some View {
VStack {
if(categories.isEmpty){
Button ("Add category"){
categories = ["dog", "cat"]
}
}
List {
ForEach(categories.indices, id: \.self) { i in
Text(categories[i])
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
categories.remove(at: i)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
// don't display if categories.isEmpty
.frame(height: categories.isEmpty ? 0 : nil)
}
}}
Here's a possible solution. You could try using onDelete for this, documentation is here. I also included onMove if needed and added a button which is only active when the array is empty.
struct ContentView: View {
#State private var animals = [
"Dog",
"Cat",
]
var body: some View {
NavigationView {
List {
ForEach(animals, id: \.self) { animal in
Text(animal)
}
.onDelete { self.delete(at :$0) }
.onMove { self.move(from: $0, to: $1) }
}
.navigationTitle("Animals")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.animals = [
"Dog",
"Cat",
]
}, label: {
Label("Add", systemImage: "plus")
.labelStyle(.iconOnly)
}).disabled(!animals.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
func delete(at: IndexSet) {
for i in at {
animals.remove(at: i)
}
}
func move(from: IndexSet, to: Int) {
animals.move(fromOffsets: from, toOffset: to)
}
}
I have a details screen and i want to display outside the list a Text() view and then another List, but when i adding the Text() below and outside the List i can't see the view
Part of my code:
var body: some View {
NavigationView {
List {
Button(action: {}, label: {
Text("Name: \(name)")
})
Button(action: {}, label: {
Text("Description: \(description)")
})
Button(action: {}, label: {
Text("Numerical Target: \(numTarget)")
})
Picker(selection: $numUnitIndex, label: Text("Numerical Unit: \(numUnit)")) {
ForEach(0 ..< units.count) {
Text(self.units[$0]).tag($0).foregroundColor(.blue)//.listRowInsets(EdgeInsets())
}
}.pickerStyle(MenuPickerStyle())
Button(action: {}, label: {
Text("Start date: \(startDate)")
})
Button(action: {}, label: {
Text("End date: \(endDate)")
})
Button(action: {}, label: {
Text("Category: \(category)")
})
Spacer()
}.onAppear() {
setPickerValue()
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Goal Details")
.toolbar {
Button("Add Step") {
print("Help tapped!")
}
}
Text("Steps for this goal").background(Color.black).font(.system(size: 20))
}
}
}
struct GoalDetailsView_Previews: PreviewProvider {
static var previews: some View {
GoalDetailsView(id: "", authorId: "", name: "", description: "", startDate: "", endDate: "", numTarget: "", numUnit: "", category: "")
}
}
When you're placing more that one item inside NavigationView you need to specify container(VStack/HStack/ZStack) explicitly, like this:
NavigationView {
VStack {
List {
// content
}
Text("Steps for this goal").font(.system(size: 20))
}
}
Generally speaking, screen will be split into sections for each NavigationView sibling up to a system specific limit. For iOS this limit is 1, which makes all other views disappear, on iPadOS and macOS the limit is higher
I've created a view for setting a time (minutes and seconds). It uses two wheel pickers bound to two state variables.
Now I'd like to use that view at different places in the app, but I don't like the interface with two seperate variables for the time. Instead, I'd like to have just one bound variable holding the time in seconds (so time = 185 would translate to 3 minutes and 5 seconds).
Is it possible to have some sort of "adapter" between bindings?
Here's the view:
import SwiftUI
struct TimePicker: View {
var minutes: Binding<Int>
var seconds: Binding<Int>
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Sec.")
Spacer()
}
}
}
Here is the approach based on the Binding(get:set:)
struct TimePicker: View {
#Binding var total: Int
var minutes: Binding<Int> {
Binding<Int>(get: { self._total.wrappedValue / 60 },
set: { self._total.wrappedValue = self._total.wrappedValue % 60 + $0 * 60 })
}
var seconds: Binding<Int> {
Binding<Int>(get: { self._total.wrappedValue % 60 },
set: { self._total.wrappedValue = (self._total.wrappedValue / 60) * 60 + $0 })
}
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Sec.")
Spacer()
}.frame(height: 200)
}
}
struct TestTimePicker: View {
#State var seconds = 185
var body: some View {
VStack {
Text("Current: \(seconds)")
TimePicker(total: $seconds)
}
}
}