Pass binding that is dependent on another binding - ios

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"))
})
}
}

Related

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)
}
)
}
}

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)
}
}
}

Why does both Picker's .onReceive fire?

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)
}
}
}
}

Why the first item of the list is displayed all the on the opened sheet

I am passing binding variable into other view:
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var isSheetIsVisible = false
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.isSheetIsVisible.toggle()
}
.sheet(isPresented: self.$isSheetIsVisible){
PocketDetailsView(pocketItem: self.$pocket.pockets[index])
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
}
}
}
the other view is:
struct PocketDetailsView: View {
#Binding var pocketItem: PocketItem
var body: some View {
Text("\(pocketItem.name)")
}
}
Why I see the first item when i open sheet for second or third row?
When I use NavigationLink instead of the .sheet it works perfect
You activate all sheets at once, try the following approach (I cannot test your code, but the idea should be clear)
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var selectedItem: PocketItem? = nil
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.selectedItem = pocketItem
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
.sheet(item: self.$selectedPocket) { item in
PocketDetailsView(pocketItem:
self.$pocket.pockets[self.pocket.pockets.firstIndex(of: item)!])
}
}
}
}

SwiftUI: prevent View from refreshing when presenting a sheet

I have noticed that SwiftUI completely refresh view when adding sheetmodifier.
Let's say I have View that displays random number. I expect that this value would be independent and not connected to the sheet logic (not changing every time I open/close sheet), but every time sheet presented/dismissed Text is changing.
Is it supposed to work so?
Am I wrong that main point of #Sateis to update only connected Views but not all stack?
How can I prevent my View from refreshing itself when presenting a modal?
struct ContentView: View {
#State var active = false
var body: some View {
VStack {
Text("Random text: \(Int.random(in: 0...100))")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
P.S. ContentView calls onAppear()/onDisappear() and init() only ones.
It needs to make separated condition-independent view to achieve behavior as you wish, like below
struct RandomView: View {
var body: some View {
Text("Random text: \(Int.random(in: 0...100))")
}
}
struct ContentView: View {
#State var active = false
var body: some View {
VStack {
RandomView()
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
In this case RandomView is not rebuilt because is not dependent on active state.
Asperi sad :
View is struct, value type, if any part of it changed then entire
value changed
He is absolutely right! But for that we have state properties. When the view is recreated, the value of state doesn't change.
This should work, as you expected
struct ContentView: View {
#State var active = false
#State var number = Int.random(in: 0 ... 100)
var body: some View {
VStack {
Text("Random text: \(number)")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
What is the advantage? For simple things, the state / binding is the best solution, without any doubt.
import SwiftUI
struct SheetView: View {
#Binding var randomnumber: Int
var body: some View {
Button(action: {
self.randomnumber = Int.random(in: 0 ... 100)
}) {
Text("Generate new random number")
}
}
}
struct ContentView: View {
#State var active = false
#State var number = Int.random(in: 0 ... 100)
var body: some View {
VStack {
Text("Random text: \(number)")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
SheetView(randomnumber: self.$number)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Now you can dismiss the sheet with or without generating new random number. No external model is required ...

Resources