SwifUI iOS 14 Picker Binding - swiftui

SwiftUI Pickers appear not to work in iOS 14 as they used to.
The lower one works perfectly, while the first one produces this error:
"displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract."
Is there anything I am missing? I don't know how to get the upper version to work again?
It does work if I change the picker style to the Wheel, however I prefer to use the "DefaultPickerStyle", where it works like a Navigationlink revealing the list of options in a second page.
struct WeekdayPickerViewNEW1 : View{
#Binding var selection: Int
var text: String
var body: some View {
Picker(selection: self.$selection, label: Text(self.text)){
Text("1").tag(1)
Text("2").tag(2)
Text("3").tag(3)
}
}
}
struct WeekdayPickerViewNEW2 : View{
#State var test: Int = 0
var text: String
var body: some View {
Picker(selection: self.$test, label: Text(self.text)){
Text("A").tag(1)
Text("B").tag(2)
Text("C").tag(3)
}
}
}

Related

Best way to present a long Segmented Picker in Swift UI

I need some suggestions on presenting a segmented picker in Swift UI.
It is to display distinct time ranges (<15min, <30min, <45min) all the way to 120min.
It ends up being 8 segments. I am really not a fan of the scrolling picker as it not in theme what what I am looking for in presentation.
The problem with how it stands now is that the time unit is cut off with each segment showing "15.." and doesn't look clean.
I have put the segmented picker in a horizontal scroll view which looks okay but the user may not know to scroll.
One option I used but can't get to work out is splitting the one long segment into 2 separate views.
The problem is the user can select a segment from either pickers which is not what I want.
What I want is if the user selects one picker, the other one is not selectable or vice versa.
I have been messing with some formatting options, so please ignore that.
Is this possible?
Thanks is advance!
struct ContentView: View {
var body: some View {
VStack{
To60min()
To120min()
.foregroundColor(Color.red)
}
}}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}}
struct To60min: View {
#State private var selectedTimeRangeto60 = ""
#State private var timeRangesTo60 = ["15min", "30min", "45min", "60min"]
var body: some View {
Picker("", selection: $selectedTimeRangeto60) {
ForEach(timeRangesTo60, id: \.self) {
Text($0)
}
}
.frame(width: .infinity, height: 75)
.background(.gray)
.padding()
.pickerStyle(.segmented)
.contrast(22.0)
}
}
struct To120min: View {
#State private var selectedTimeRangeto120 = ""
#State private var timeRangesTo120 = ["75min", "90min", "105min", "120min"]
var body: some View {
Picker("", selection: $selectedTimeRangeto120) {
ForEach(timeRangesTo120, id: \.self) {
Text($0)
}
}
.padding()
.pickerStyle(.segmented)
.contrast(22)
}
}
For anything more than 3-4 items (depending on label length), I would switch from a .segmented to .menu picker style. https://developer.apple.com/documentation/swiftui/pickerstyle

SwiftUI: Picker content does not display when selected in a list

I am trying to make a list of selectionable rows in Swift UI, the rows including a Picker. Everything works fine, except that the content of the Picker disappears when selected, see attached screenshot (but is actually visible when the window of the app is not active (i.e. when I click on another window))
I tried everything I could think of, I could not solve this issue. Below a minimal code to reproduce the problem. Anyone has any idea, how to get around this problem
SelectionList.swift
struct SelectionList: View {
#State var dudes: [String] = ["Tim", "Craig", "Phil"]
#State var selectedRow = Set<String>()
var body: some View {
return List(selection: $selectedRow) {
ForEach(self.dudes, id: \.self) { item in
SelectionRow()
}
}
}
}
SelectionRow.swift
struct SelectionRow: View {
#State var selectedFruit = 0
let fruits = ["Apples", "Oranges", "Bananas", "Pears"]
var body: some View {
Picker(selection: self.$selectedFruit, label: EmptyView()) {
ForEach(0 ..< fruits.count, id: \.self) {
Text(self.fruits[$0])
}
}
}
}

String Interpolation with Text View and Textfield in Swiftui

Hi I'm wondering if there's any way to have string interpolation with a textfield and Text in Swiftui. Like
Text("hi \(TextField("Enter your name", $name)")
You can try below code
struct ContentView: View {
#State private var name = ""
var body: some View {
VStack{
TextField("Enter your name", text: $name)
Text("Hi \(name)")
}
}
}
Hope this is what you want
If you really want to do that you can put your statements in an Hstack like so:
import SwiftUI
struct SwiftUIView: View {
#State var name = ""
var body: some View {
HStack {
Text("Hi")
TextField("Name", text: $name)
.frame(width: (name.isEmpty ? 45 : 0) + CGFloat(name.count) * 9)
Text("blabla")
}
}
}
Note: This gives you a dynamic change, but not a perfect one Because the size of each character is different you will get a bigger whitespace at the end. I just choose 9 here, for char width. I still think having a extra Textfield and using the variable then later, is the better option.

How make work a Picker with an ObservedObject in SwiftUI?

I'm trying to get a list of Datacenters from a Rest API and show them in a Picker, so the user can choose one. When I do it with a static list it works fine. However, retrieving the Datacenters dinamically seems not to work fine.
I'm using Xcode 11 (GM)
This is the Datacenter Object
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
This is the ObservedObject (it has the property datacenters that is an array of Datacenter objects)
#ObservedObject var datacenters_controller : DatacentersController
#State private var selectedDatacenter = 0
This was my first attempt:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(0 ..< datacenters_controller.datacenters.count) {
Text(self.datacenters_controller.datacenters[$0].location)
}
}
Swift complained with the following error:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Then I switched to:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
It "works" (no error), but the result is not the expected because although I can select a datacenter, it is not "stored", not shown in the Picker as selected.
Actual result
Expected result
Any idea? What I'm doing wrong?
Here's a working example. The key is that selectedDatacenter needs to be the same type as Datacenter.id (in this case, String).
struct ContentView: View {
#ObservedObject var datacenters_controller = DatacentersController()
#State private var selectedDatacenter = ""
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
// Just here for demonstration
Text("selectedDatacenter (id): \(selectedDatacenter.isEmpty ? "Nothing yet" : selectedDatacenter)")
}
}
}
}
Here's the supporting code
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
class DatacentersController: ObservableObject {
#Published var datacenters: [Datacenter] = []
init() {
datacenters = [
Datacenter(id: "ABQ", location: "Albuquerque"),
Datacenter(id: "BOS", location: "Boston"),
Datacenter(id: "COS", location: "Colorado Springs")
]
}
}
I think you are missing tag on your picker:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
Apple docs on tag:
Sets the tag of the view, used for selecting from a list of View
options.
In your second attempt, you need to use the tag modifier (as described by LuLuGaGa). You also need to change the type of selectedDatacenter to match. For example:
struct ContentView: View {
init(_ controller: DatacentersController) {
self.datacenters_controller = controller
self._selectedDatacenter = State(initialValue: controller.datacenters[0].id)
}
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
}
}
}
#ObservedObject private var datacenters_controller: DatacentersController
#State private var selectedDatacenter: String
}

Why does binding to the Picker not work anymore in swiftui?

When I run a Picker Code in the Simulator or the Canvas, the Picker goes always back to the first option with an animation or just freezes. This happens since last Thursday/Friday. So I checked some old simple code, where it worked before that and it doesn't work for me there, too.
This is the simple old Code. It doesn't work anymore in beta 3, 4 and 5.
struct PickerView : View {
#State var selectedOptionIndex = 0
var body: some View {
VStack {
Text("Option: \(selectedOptionIndex)")
Picker(selection: $selectedOptionIndex, label: Text("")) {
Text("Option 1")
Text("Option 2")
Text("Option 3")
}
}
}
}
In my newer code, I used #ObservedObject, but also here it doesn't work.
Also I don't get any errors and it builds and runs.
Thank you for any pointers.
----EDIT----- Please look at the answer first
After the help, that I could use the .tag() behind all Text()like Text("Option 1").tag(), it now takes the initial value and updates it inside the view. If I use #ObservedObject like here:
struct PickerView: View {
#ObservedObject var data: Model
let width: CGFloat
let height: CGFloat
var body: some View {
VStack(alignment: .leading) {
Picker(selection: $data.exercise, label: Text("select exercise")) {
ForEach(data.exercises, id: \.self) { exercise in
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
}
}
.frame(width: width, height: (height/2), alignment: .center)
}
}
}
}
Unfortunately it doesn't reflect changes on the value, if I make these changes in another view, one navigationlink further. And also it doesn't seem to work with the my code above, where I use firstIndex(of: exercise)
---EDIT---
Now the code above works if I change
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
into
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise)!)
because it couldn't work with an optional.
The answer summarized:
With the .tag() behind the Options it works. It would look like following:
Picker(selection: $selectedOptionIndex, label: Text("")) {
ForEach(1...3) { index in
Text("Option \(index)").tag(index)
}
}
If you use a range of Objects it could look like this:
Picker(selection: $data.exercises, label: Text("")) {
ForEach(0..<data.exercises.count) { index in
Text("\(data.exercises[index])").tag(index)
}
}
I am not sure if it is intended, that .tag() is needed to be used here, but it's at least a workaround.
I found a way to simplify the code a bit without the need of operating on indicies and tags.
At first, make sure to conform your model to Identifiable protocol like this (this is actually a key part, as it enables SwiftUI to differentiate elements):
public enum EditScheduleMode: String, CaseIterable, Identifiable {
case closeSchedule
case openSchedule
public var id: EditScheduleMode { self }
var localizedTitle: String { ... }
}
Then you can declare viewModel like this:
public class EditScheduleViewModel: ObservableObject {
#Published public var editScheduleMode = EditScheduleMode.closeSchedule
public let modes = EditScheduleMode.allCases
}
and UI:
struct ModeSelectionView: View {
private let elements: [EditScheduleMode]
#Binding private var selectedElement: EditScheduleMode
internal init?(elements: [EditScheduleMode],
selectedElement: Binding<EditScheduleMode>) {
self.elements = elements
_selectedElement = selectedElement
}
internal var body: some View {
VStack {
Picker("", selection: $selectedElement) {
ForEach(elements) { element in
Text(element.localizedTitle)
}
}
.pickerStyle(.segmented)
}
}
}
With all of those you can create a view like this:
ModeSelectionView(elements: viewModel.modes, selectedElement: $viewModel.editScheduleMode)