Best way to present a long Segmented Picker in Swift UI - swiftui

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

Related

Specific times in a DatePicker()

I am trying to use a DatePicker in swiftUI, but can't seem to find a way to give it specific times such as 8:00, 11:00, and 2:00. Is there a way to do it, and if so, how?
Currently I'm using this to do Time Intervals, but I don't think this helps with the current problem.
You can't specify selected times in DatePicker.
But you could do something like this instead and and combine the values on commit:
struct ContentView: View {
#State private var date = Date()
#State private var time = "8:00"
var body: some View {
HStack {
DatePicker("Select", selection: $date, displayedComponents: .date)
.datePickerStyle(.compact)
.background(.background)
Picker("Time", selection: $time) {
Text("2:00").tag("2:00")
Text("8:00").tag("8:00")
Text("11:00").tag("11:00")
}
.pickerStyle(.wheel)
.frame(width: 100)
.zIndex(-1)
}
.padding()
}
}

Radiobuttons in List swiftUI

Im displaying data from server and need to display the options in radiobuttons. But default none of the button should be selected. I was able to display radiobuttons. But when a particular button is clicked, only its image should be changed. Though with my code all the other button images too change. I have gone through some references SwiftUI - How to change the button's image on click?, but couldn't get it work. As am a newbie to SwiftUI, strucked here.
struct ListView: View {
#State var imageName: String = "radio-off"
var body: some View {
List(vwModel.OpnChoice.ItemList) { opn in
VStack(alignment:.leading) {
ForEach(opn.Choices.indices, id: \.self) { row in
Button(action: {
print("\(opn.Choices[row].ChoiceId)")
self.imageName = "radio-On"
}) {
Image(imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
Text(opn.Choices[row].ChoiceName)
}
}
}
}
}
As you are working with indices anyway, add a #Published property to your view model which contains the selected index.
Then set the index in the button action. The redraw of the view sets the button at the selected index to the on-state and the others to the off-state.
As I don't know your environment this is a simplified stand-alone view model and view with SF Symbols images
class VMModel : ObservableObject {
#Published var selectedOption = -1
var numberOfOptions = 5
}
struct ListView: View {
#StateObject private var vwModel = VMModel()
var body: some View {
VStack(alignment:.leading) {
ForEach(0..<vwModel.numberOfOptions, id: \.self) { opn in
Button(action: {
vwModel.selectedOption = opn
print("index \(opn) selected")
}) {
Image(systemName: opn == vwModel.selectedOption ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
}
}
}
}
Update:
Meanwhile you can use a Picker with the .radioGroup modifier
enum Choice {
case one, two, three
}
struct ListView: View {
#State private var choice : Choice = .one
var body: some View {
Picker(selection: $choice, label: Text("Select an option:")) {
Text("One").tag(Choice.one)
Text("Two").tag(Choice.two)
Text("Three").tag(Choice.three)
}.pickerStyle(.radioGroup)
}
}
At the moment you are storing a single #State property "imageName" for the entire list. And then when any of the buttons in the list are tapped you are changing that single property. Which will affect all the buttons.
I'd suggest removing this property and putting something into the viewModel.
You appear to have a view model with an array called vwModel.opnChoice.itemList.
There are multiple ways of making this work. You could stop a boolean in the itemList to say whether each item is selected or not. But, as you want it to work like radio buttons what might be better is to have a property on the vwModel like selectedItem.
The button could then do...
vwModel.selectedItemId = opn.choices[row].choiceId
Then you button Label would be...
Image(vsModel.selectedItemId == opn.choices[row].choiceId ? "radio-on" : "radio-off")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
This would allow you to toggle the button when it is tapped and should turn the other buttons off.

Disable or ignore taps on TabView in swiftui

I have a pretty usual app with a TabView. However, when a particular process is happening in one of the content views, I would like to prevent the user from switching tabs until that process is complete.
If I use the disabled property on the TabView itself (using a #State binding to drive it), then the entire content view seems disabled - taps don't appear to be getting through to buttons on the main view.
Example:
struct FooView: View {
var body: some View {
TabView {
View1().tabItem(...)
View2().tabItem(...)
}
.disabled(someStateVal)
}
}
Obviously, I want the View1 to still allow the user to, you know, do things. When someStateVal is true, the entire View1 doesn't respond.
Is there a way to prevent changing tabs based on someStateVal?
Thanks!
I could not find a way to individually disable a tabItem, so here is
an example idea until someone comes up with more principled solution.
The trick is to cover the tab bar with a clear rectangle to capture the taps.
struct ContentView: View {
#State var isBusy = false
var body: some View {
ZStack {
TabView {
TestView(isBusy: $isBusy)
.tabItem {Image(systemName: "globe")}
Text("textview 2")
.tabItem {Image(systemName: "info.circle")}
Text("textview 3")
.tabItem {Image(systemName: "gearshape")}
}
VStack {
Spacer()
if isBusy {
Rectangle()
.fill(Color.white.opacity(0.001))
.frame(width: .infinity, height: 50)
}
}
}
}
}
struct TestView: View {
#Binding var isBusy: Bool
var body: some View {
VStack {
Text("TestView")
Button(action: {
isBusy.toggle()
}) {
Text("Busy \(String(isBusy))").frame(width: 170, height: 70)
}
}
}
}
I use another trick. Just hide the tab image.
struct FooView: View {
var body: some View {
TabView {
View1().tabItem{Image(systemName: someStateVal ? "": "globe")}
View2().tabItem{Image(systemName: someStateVal ? "": "gearshape")}
}
}
}

How to make a reusable TextPopUp in SwiftUI?

I am having trouble building a reusable SwiftUI text pop up view. I think the problem is the binding but I not sure. It is supposed to be a ContextMenu, but the reason I am not using ContextMenu is it does not show enough lines of text.
So far I have this...
struct TextPopUpView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#Binding var showPopover: Bool
var displayedText: String
var popUpText: String
var body: some View {
Text("\(displayedText)")
.font(.title)
.fontWeight(.bold)
.onLongPressGesture {
self.showPopover = true
}
.popover(isPresented: $showPopover) {
Text("\(self.popUpText)")
.frame(width: 250.0)
.onTapGesture {
self.showPopover = false
}
}
}
}
And than I implement it for a particular view like so...
TextPopUpView(showPopover: $showPopover, displayedText: oracleViewModel.***someElement***, popUpText: oracleViewModel.getDescriptionFor***SomeElement***()).environmentObject(oracleViewModel)
where 'someElement' is the particular element text and popup text I want to show.
The problem is when I use TextPopUpView more than once in a view, the Popup view only displays the text for the last implementation on the said page.
I am guessing I am doing something wrong in the implementations of a reusable view, but I am not sure what. Any suggestions?
EDIT: (Example Used in code)
struct TopView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#State private var showPopover: Bool = false
***Blarg Blarg Blarg***
VStack{
VStack {
Text("RUNE")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.rune,
popUpText: oracleViewModel.getDescriptionForRune()).environmentObject(oracleViewModel)
}
.padding(.bottom)
VStack {
Text("ELEMENT")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.element,
popUpText: oracleViewModel.getDescriptionForElement())
.environmentObject(oracleViewModel)
}
}
***Blarg Blarg Blarg***
}
(For some reason I only get the description for the Element, when I long press either view the TextPopUp is attached to)

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)