Custom picker with access to option value instead of just selected index - ios

I'm trying to implement a custom Picker that makes a custom value available to the parent (as opposed to just the selected index). In the example below, I've got a #State variable bound to selection on the picker as well as a computed variable selectedOption which updates correctly.
import SwiftUI
struct CustomPicker: View {
var options = ["option1", "option2", "option3"]
#State var selectedIndex = 0
var selectedOption {
options[selectedIndex]
}
var body: some View {
Picker(selection: $selectedIndex, label: Text("")) {
ForEach(0..<self.options.count) { index in
Text("\(self.options[index])").tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
However, I want to be able to interact with the CustomerPicker in another view like this:
#State var selectedOption: string;
...
CustomPicker(selectedOption: $selectedOption)
So that the parent view is dealing with the option directly, as opposed to the index. Does anyone have any tips as to how I would go about this using SwiftUI?

Your custom picker needs #Binding to the selected index. I think it would also be a good idea to offer a "no selection" option to the picker itself:
struct CustomPicker: View {
var options = ["option1", "option2", "option3"]
#Binding var selectedIndex: Int?
var body: some View {
Picker(selection: $selectedIndex,
label: Text("")) {
Text("no selection")
.tag(Optional<Int>.none)
ForEach(0..<self.options.count) { index in
Text("\(self.options[index])")
.tag(index)
}
} .pickerStyle(SegmentedPickerStyle())
}
}

Related

SwiftUI #State variable does not change view

Using HalfASheet (https://github.com/franklynw/HalfASheet).
I have a View called ProjectsView, and in the ZStack in ProjectsView I have ProjectSorting and SortingView(both injected with the EnvironmentObject). I want the Text(🟩) in ProjectSorting to be changed, and the HStack(🟦) in SortingView to have a checkmark, both depending on the value of the sorting variable in SortingValues. Users can change the value of the sorting by pressing the Button in SortingView.
For whatever reason, the Text(🟩) in ProjectSorting does not change at all. And the HStack(🟦) in SortingView only gets the checkmark when its ancestor stack has another Text(🟨) which includes the #State variable from the environment, which I find very weird.
What should I change? Is there any way I can make this work using #EnvironmentObject? I'm a newbie and couldn't really understand other wrappers so I'd like to make this work within #State, #Binding, #EnvirionmentObject.
Thanks in advance.
SortingValues.swift
import Combine
class SortingValues: ObservableObject {
#Published var sorting = "Top Rated"
}
ProjectsView.swift
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
}
}
}
ProjectSorting.swift
import SwiftUI
struct ProjectSorting: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HStack {
Text("Projects")
Spacer()
Button {
showSortingSheet.toggle()
} label: {
HStack(spacing: 3) {
Image("sortingArrows")
Text(sortingValue) // < 🟩 this is the Text I want to be changed
}
}
}
// Another HStack goes here
}
.onReceive(sortingValues.$sorting) { sorting in
print("This is ProjectSorting. sorting:", sorting) // < this does not print when I close the half sheet
sortingValue = sorting
}
}
}
SortingView.swift
import SwiftUI
import HalfASheet
struct SortingView: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HalfASheet(isPresented: $showSortingSheet) {
let sorting = ["Most Recent", "Most Reviewed", "Top Rated", "Lowest Price", "Highest Price"]
VStack(alignment: .leading) {
ForEach(sorting, id: \.self) { sorting in
VStack(alignment: .leading, spacing: 14) {
Button (action: {
sortingValues.sorting = sorting
}, label: {
HStack { // 🟦
Text(sorting)
Spacer()
if sorting == sortingValue { // < this is where I add the checkmark
Image(systemName: "checkmark")
}
}
.foregroundColor(.primary)
})
if sorting != "Highest Price" {
Divider()
}
}
}
}
}
.height(.fixed(325))
// Text("Inside VStack, outside HalfASheet") // adding this Text DOES NOT make the HStack have a checkmark
Text("Inside VStack, outside HalfASheet: \(sortingValue)") // 🟨 adding this Text DOES make the HStack have a checkmark
}
.onReceive(sortingValues.$sorting) { sorting in
// the two printing lines below print correctly every time I tap the Button
print("This is SortingView. sorting:", sorting)
print("sortingValues.sorting: \(sortingValues.sorting)")
sortingValue = sorting
}
}
}
Your SortingView and ProjectSorting both access an environment object of type SortingValues, but you're passing new, separate instances to each. So the change you make in one place isn't being reflected in the other, because each view is communicating with one of two completely different objects of the same type.
If you want them to interact with the same object instance, you need to declare it at a point that's above both in the object hierarchy and make sure that that single instance is passed into both. For example:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValue)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValues)
}
}
}
But you can go one step further. Because environment objects and values propagate down the view hierarchy automatically, you can replace two separate .environmentObject calls with one:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
}
.environmentObject(sortingValues)
}
}
There are probably better ways of dealing with reacting to changes in your observed model rather than duplicating variable values in a local state variable -- but ensuring that all your views are using the same shared environment object should get you on your way.

How can I reproduce Picker's selection binding?

I'm building a custom view and I'm trying to manipulate the active state of its children.
I have this:
struct Menu<Content>: View where Content: View {
#ViewBuilder var content: () -> Content
var body: some View {
content()
}
}
struct ScreenView: View {
var body: some View {
Menu {
Text("Home")
Text("Settings")
Text("Profile")
}
}
}
I would like to be able to pass a binding to the Menu view and based on that, to change the text color if the state is matching the actual text or id of the view. There is a Picker view example that does what I want to achieve. The Picker is managing the look and feel of the selected element. Am I wrong?
struct PickerViewExample: View {
#State var selection: Int
var body: some View {
Picker(selection: $selection, label: Text("Picker"), content: {
Text("1").tag(1)
Text("2").tag(2)
})
}
}
I'd like to know if there is a way to ForEach somehow the content ViewBuilder property in order to manipulate subviews. I like the way Apple solved this using tags. Any alternative is welcome.
I think, this will help
struct ContentView: View {
private var menuItem = ["Home", "Settings", "Profile"]
#State private var selectedMenu = "Home"
var body: some View {
VStack {
Picker("Menu", selection: $selectedMenuIndex, content: {
ForEach(menuItem, id: \.self, content: { title in
Text(title)
})
})
Text("Selected menu: \(selectedMenu)")
}
}
}

SwiftUI: Is it possible to layer these views without each one being scrollable?

I am presenting a sheet that will present a slider within a section which is within a form. I was trying to add my subview AddDoujin into this view as well. But the problem is, is that both views are scrollable and aren't layering on top of each other and seems like it is being added to the bottom (if that makes sense). My goal is to make the AddDoujin view right under the slider rather than both being each their own view and both of them being scrollable. Sorry if this is unclear and you need more information.
import SwiftUI
struct TestingAddDoujin: View {
//Varaibles
#State private var InputDoujin:String = ""
var DoujinApi:DoujinAPI
#Binding var isPresented:Bool
#State private var RedoEntry:Bool = false
var PickerOptions = ["Doujin", "Hentai"]
#State var PickerSelected = ""
#State var CurrentSelectionForPicker = 0
var body: some View {
GeometryReader { geo in
NavigationView{
VStack {
ZStack{
Form{
Section(header: Text("What you you looking for?")) {
Picker(selection: $CurrentSelectionForPicker, label: Text("Please select one")) {
ForEach(0..<PickerOptions.count) {
Text("\(self.PickerOptions[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented)
}
}
}
}
}
struct TestingAddDoujin_Previews: PreviewProvider {
static var previews: some View {
TestingAddDoujin(DoujinApi: DoujinAPI(), isPresented: .constant(false))
}
}
It seems like if you just move AddDoujin(DoujinApi: DoujinApi, isPresented: $isPresented) right below .pickerStyle(SegmentedPickerStyle()) You will get the desired result.
I'm guessing you've tried this. So maybe show and tell us more about what you are trying to accomplish.
btw. In Swift it's not convention to capitalize the first letter of a variable.

How to create a view that works with #Binding. It should refresh itself while working with #Binding data

This question is identical to SwiftUI #Binding update doesn't refresh view, but the accepted answer is not applicable for my case.
The accepted answer says
A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
In my case, the view hierarchy doesn't have the view which is having the #State. The view having the binding is presented modally to the user.
To summarize the issue again
I want to create a view, similar to Toggle which initializes from a Binding. This view will show the contents from the wrapped value and as it performs the updates, the original storage of the value will get updated automatically.
As I have learnt, updating the #Binding in a view, doesn't invalidate it. Then how to implement such a view.
Also I can't depend on the parent view to eventually update this view, because the view is shown on a modally presented screen.
I don't want to use workarounds like using a #State to explicitly trigger a refresh. So what is the correct way to implement such a view.
Code example
The view TextModifier takes a Binding. The view does some modifications to the view. For now it just appends "_Updated" to the value passed.
I initialize the view as TextModifier(text: <some_binding_var>)
struct TextModifier: View {
#Binding var text: String
var body: some View {
Text(text)
.onTapGesture {
text += "_Updated"
}
}
}
This view shows the text and on tapping it updates it in the original source, but as expected the view doesn't update itself on tapping.
So, how to implement this view so that it also updates itself when it updates the binding value.
The accepted answer to the linked question also says
Else you could bind to a publisher
I don't know how to do this. Does anybody know how to implement this and also provide a code example. Thanks.
Updated with full code and gif
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List {
ForEach(viewModel.itemsList, id: \.self) { item in
ItemView(text: $viewModel.itemsList[getItemIndex(item)])
}
}
}
private func getItemIndex(_ item: String) -> Int {
viewModel.itemsList.firstIndex { $0 == item }!
}
}
class TestViewModel: ObservableObject {
#Published var itemsList = ["Item 1", "Item 2", "Item 3"]
}
struct ItemView: View {
#Binding var text: String
#State private var showEditorView = false
var body: some View {
Text(text)
.onTapGesture {
showEditorView = true
}
.sheet(isPresented: $showEditorView) {
TextModifier(text: $text, showView: $showEditorView)
}
}
}
struct TextModifier: View {
#Binding var text: String
#Binding var showView: Bool
var body: some View {
VStack(spacing: 20) {
Text("Tap on the text to update it")
.foregroundColor(.blue)
Text(text)
.onTapGesture {
text += "_Updated"
}
Button {
showView = false
} label: {
Text("Dismiss")
.foregroundColor(.blue)
}
}
}
}

SwiftUI #Binding update doesn't refresh view

I feel like I'm missing something very basic, but this example SwiftUI code will not modify the view (despite the Binding updating) when the button is clicked
Tutorials I have read suggest this is the correct way to use a binding and the view should refresh automatically
import SwiftUI
struct ContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView_Previews: PreviewProvider {
#State static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: $selected)
}
}
You have not misunderstood anything. A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
Below, I have changed the name of your ContentView to OriginalContentView and then I have defined the #State in the new ContentView that contains your original content view.
import SwiftUI
struct OriginalContentView: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#State private var selected = false
var body: some View {
OriginalContentView(isSelected: $selected)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SwiftUI View affects #Binding. #State affects SwiftUI View.
#State var affects the view, but to affect another #State it must be used as binding by adding leading $ to value name and it works only inside SwiftUI.
To trigger SwiftUI change from outside, i.e. to deliver/update Image, use Publisher that looks like this:
// Declare publisher in Swift (outside SwiftUI).
public let imagePublisher = PassthroughSubject<Image, Never>()
// It must be handled within SwiftUI.
struct ContentView: View {
// Declare a #State that updates View.
#State var image: Image = Image(systemName: "photo")
var body: some View {
// Use #State image declaration
// and subscribe this value to publisher "imagePublisher".
image.onReceive(imagePublisher, perform: { (output: Image) in
self.image = output // Whenever publisher sends new value, old one to be replaced
})
}
}
// And this is how to send value to update SwiftUI from Swift:
imagePublisher.send(Image(systemName: "photo"))
In the top Level of SwiftUI, #Binding cannot refresh View hierarchy unless manually adding a #state or other refreshing triggers.
struct ContentView: View {
#Binding var isSelected : Bool
#State var hiddenTrigger = false
var body: some View {
VStack {
Text("\(hiddenTrigger ? "" : "")")
Button(action: {
self.isSelected.toggle()
self.hiddenTrigger = self.isSelected
}) {
Text(self.isSelected? "Selected" : "not Selected")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: Binding<Bool>(get: {selected}, set: { newValue in
selected = newValue}))
}
}
Looking into this some more I think I understand what's happening.
In this instance I want to use #Binding as I'm building a custom control (like SwiftUI's native Toggle, which also binds to a Bool)
The issue is that the static state in ContentView_Previews (i.e., the line #State static var selected: Bool = false) does not trigger a re-render of the preview when the state changes, so even though the selected state has changed due to interaction with the control, the control (a child of ContentView_Previews) does not re-render itself
This makes it tough to test controls in isolation in the SwiftUI preview, however moving the state into a dummy ObservableObject instance functions correctly. Here's the code:
import SwiftUI
import Combine
class SomeData: ObservableObject {
#Published var isOn: Bool = false
}
struct MyButton: View {
#Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
#EnvironmentObject var data: SomeData
var body: some View {
MyButton(isSelected: $data.isOn)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SomeData())
}
}
It seems that a change in #State static var doesn't trigger a preview re-render. In the above code my #Binding example is moved into MyButton and the content view's dummy environment instance is bounds to its isSelected property. Tapping the button updates the view as expected in the SwiftUI preview.
You need to use #State instead of #Binding.
If the UI should update when its value changes, you designate a variable as a
#State variable. It is the source of truth.
You use #Binding instead of #State, when the view doesn't own this data and its not the source of truth.
Here is your variable:
#State var isSelected: Bool
In my case, having the #Binding or #State control a top level if statement caused issues.
I put the if check inside a top level VStack and it started working fine.
struct ContentView: View {
#Binding var value: Bool // #State breaks too
var body: some View {
// Add a VStack here to fix the bug
if value { // Top level `if` based on #State or #Binding won't work
Text("view 1")
} else {
Text("view 2")
.onAppear {
value = true // Won't trigger update
}
}
}
}
This was only sometimes though.. Depending on what the rest of the view hierarchy looked like. My view hierarchy was nested inside a NavigationView, a TabView, a ZStack, etc. I'm not sure what the minimum requirements are to trigger this. Really weird behavior.

Resources