Why does both Picker's .onReceive fire? - ios

In the swiftUI View below, there are two boolean #State variables (boolA and boolB) connected to two different pickers. Each picker has an .onReceive, with the following kind of publisher
[self.boolA].publisher.first()
(To be honest, I don't understand that line of code but it appears in several answers on S.O.)
In any case, whichever picker I change both .onReceive fire!
Questions: 1) Why does both onReceive fire? 2) How to avoid this?
struct ContentView: View {
#State private var boolA = false
#State private var boolB = false
var body: some View {
VStack{
Picker(selection: $boolA, label: Text("a? ")) {
Text("a is true").tag(true)
Text("a is false").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.boolA].publisher.first()) { _ in
print("on receive on boolA")
}
Picker(selection: $boolB, label: Text("b? ")) {
Text("b is true").tag(true)
Text("b is false").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.boolB].publisher.first()) { _ in
print("on receive on boolB")
}
Spacer()
}
}
}

You only need a single onRecieved in your View. Here's the code:
struct ContentView: View {
#State private var boolA = false
#State private var boolB = false
var body: some View {
VStack{
Picker(selection: $boolA, label: Text("a? ")) {
Text("a is true").tag(true)
Text("a is false").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
Picker(selection: $boolB, label: Text("b? ")) {
Text("b is true").tag(true)
Text("b is false").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
Spacer()
.onReceive([self.boolA, self.boolB].publisher.first()) { _ in
print("boolA:", self.boolA, "boolB:", self.boolB)
}
}
}
}

Related

Why fullScreenCover always take first index from array?

Why fullScreenCover always take just first index of an array?
This is some example of code:
struct TestView: View {
#State private var isFullScreen: Bool = false
var body: some View {
VStack{
ForEach(0..<5, id:\.self) { number in
VStack{
Text("\(number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen.toggle()
}
}
.fullScreenCover(isPresented: $isFullScreen) {
test2View(title: number)
}
}
}
}
}
This is the code of test2View:
struct test2View: View {
var title:Int
var body: some View {
Text("\(title)")
}
}
Whenever I click on any number it always show just 0, but when I make navigationLink instead of fullScreenCover, it works as expected, but navigationLink isn't a solution for my problem, I want that to be fullScreenCover.
It's because fullScreenCover is using a single isFullScreen for each number so only the first one works. Fix by adding a third intermediary View to hold an isFullScreen bool for each number, e.g.
struct TestView: View {
var body: some View {
VStack{
ForEach(0..<5) { number in
TestView2(number: number)
}
}
}
}
struct TestView2: View {
let number: Int
#State private var isFullScreen: Bool = false
var body: some View {
Text("\(number, format: .number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen.toggle()
}
.fullScreenCover(isPresented: $isFullScreen) {
TestView3(number: number)
}
}
}
struct TestView3: View {
let number: Int
var body: some View {
Text("\(number, format: .number)")
}
}
I found a solution using .fullScreenCover item parameter like this:
struct TestView: View {
#State private var isFullScreen: Int? = nil
var body: some View {
VStack{
ForEach(0..<5, id:\.self) { number in
VStack{
Text("\(number)")
.background(.red)
.foregroundColor(.white)
.padding(20)
.onTapGesture {
isFullScreen = number
}
}
.fullScreenCover(item: $isFullScreen) { item in
test2View(title: item)
}
}
}
}
}

Why don't #State parameter changes cause a view update?

I am trying to follow the guidance in a WWDC video to use a #State struct to configure and present a child view. I would expect the struct to be able to present the view, however the config.show boolean value does not get updated when set by the button.
The code below has two buttons, each toggling a different boolean to show an overlay. Toggling showWelcome shows the overlay but toggling config.show does nothing. This seems to be working as intended, I just don't understand why SwiftUI behaves this way. Can someone explain why it's not functioning like I expect, and/or suggest a workaround?
https://developer.apple.com/videos/play/wwdc2020/10040/ # 5:14
struct InformationOverlayConfig {
#State var show = false
#State var title: String?
}
struct InformationOverlay: View {
#Binding var config: InformationOverlayConfig
var body: some View {
if config.title != nil {
Text(config.title!)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlayConfig(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter", action: {
configWelcome.show.toggle()
})
Button("Toggle boolean state", action: {
showWelcome.toggle()
})
}
.overlay(
VStack {
InformationOverlay(config: $configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: $configWelcome).opacity(showWelcome ? 1 : 0)
})
}
You "Config" is not a View. State variables only go in Views.
Also, do not use force unwrapping for config.title. Optional binding or map are the non-redundant solutions.
Lastly, there is no need for config to be a Binding if it actually functions as a constant for a particular view.
struct InformationOverlay: View {
struct Config {
var show = false
var title: String?
}
let config: Config
var body: some View {
VStack {
if let title = config.title {
Text(title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
// or
config.title.map {
Text($0)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlay.Config(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter") {
configWelcome.show.toggle()
}
Button("Toggle boolean state") {
showWelcome.toggle()
}
}
.overlay(
VStack {
InformationOverlay(config: configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: configWelcome).opacity(showWelcome ? 1 : 0)
}
)
}
}

Pass binding that is dependent on another binding

I'm rather new to SwiftUI, so please bear with in case I mix up the nomenclature.
I have an Int #State property that is used in a TabView to hold the current index. Then I have a function that takes a Binding<Bool> to display another view.
What I basically want to do is pass a condition into the function whenever my Int state reaches a given value.
Sample code:
import SwiftUI
struct FooTabView: View {
#State private var selected = 0
#State private var shouldShow: Bool = false
var body: some View {
TabView(selection: $selected) {
Text("Hello")
.tabItem {
Text("Foo")
}.tag(0)
Text("World")
.tabItem {
Text("Bar")
}.tag(1)
}.anotherFunctionReturningAView(show: shouldShow)
}
}
Lets say shouldShow should only be true, when selected is equal to 1. How would I do that?
I'm not sure if I understood your goal but you can try:
.anotherFunctionReturningAView(show: .constant(selected == 1))
You can use Proxy Binding, making it depend on your #State variable like that
struct ContentView: View {
#State private var selected = 0
#State private var shouldShow: Bool = false
var body: some View {
TabView(selection: $selected) {
Text("Hello")
.tabItem {
Text("Foo")
}.tag(0)
Text("World")
.tabItem {
Text("Bar")
}.tag(1)
}.alert(isPresented: Binding<Bool>(
get: {
selected == 1 // << Here comes your condition
}, set: {
shouldShow = $0
})
, content: {
Alert(title: Text("Test"))
})
}
}

Secondary Picker's 'ForEach' gives me Fatal Error: Index Out of Range

I get this error every time I launch a secondary picker.
The first picker works okay.
However when I switch pickers & scroll, I get the following:
Here is my entire code (written as a test of this problem):
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 0
#State private var isMainPickerHidden = false
#State private var isSecondaryPickerHidden = true
var colors = ["Red", "Green", "Blue", "Tartan"]
var sizes = ["Tiny", "Small", "Medium", "Large", "Super Size"]
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
ZStack {
VStack {
Picker(selection: $selectedItem, label: Text("Please choose a color")) {
ForEach(colors.indices, id: \.self) {
Text(self.colors[$0])
}
}.hiddenConditionally(isHidden: isMainPickerHidden)
Text("You selected: \(colors[selectedItem])")
.hiddenConditionally(isHidden: isMainPickerHidden)
}
VStack {
Picker(selection: $selectedItem, label: Text("Please choose a size")) {
ForEach(sizes.indices, id: \.self) {
Text(self.sizes[$0])
}
}.hiddenConditionally(isHidden: isSecondaryPickerHidden)
Text("You selected: \(sizes[selectedItem])")
.hiddenConditionally(isHidden: isSecondaryPickerHidden)
Spacer()
Button(action: {
isSecondaryPickerHidden = !isSecondaryPickerHidden
isMainPickerHidden = !isMainPickerHidden
}) {
Text("Switch Pickers")
}.padding()
}
}
}
}
}
// =========================================================================================================
extension View {
func hiddenConditionally(isHidden: Bool) -> some View {
isHidden ? AnyView(hidden()) : AnyView(self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What is the correct syntax for the ForEach{} to avoid this problem?
This is because you use the same selectedItem for both pickers.
If in the second picker you select the last item (index 4) and then you switch to the first picker (max index = 3), then in this line:
Text("You selected: \(colors[selectedItem])")
you'll try accessing the index which is out of range.
To fix this you can use a separate #State variable for each picker:
struct ContentView: View {
#State private var selectedColorIndex = 0
#State private var selectedSizeIndex = 0
Picker(selection: $selectedColorIndex, label: Text("Please choose a color")) {
Picker(selection: $selectedSizeIndex, label: Text("Please choose a size")) {

Creating controls at runtime in SwiftUI

The following code creates new controls every time a button is pressed at runtime, the problem is that the picker selection is set to the same state.
How can I create new controls with different state variables so they can operate separately ?
struct ContentView: View {
#State private var numberOfControlls = 0
#State var selection: String="1"
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
Picker(selection: self.$selection, label:
Text("Picker") {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
}
}
How can I create new controls with different state variables so they can operate separately ?
Separate control into standalone view with own state (or view model if/when needed).
Here is a demo:
struct ContentView: View {
#State private var numberOfControlls = 0
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
ControlView()
}
}
}
}
struct ControlView: View {
#State var selection: String="1"
var body: some View {
Picker(selection: self.$selection, label:
Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
}

Resources