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

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

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

State is nil when showing sheet

For some reason, my selectedTask State is Empty when presenting the Sheet,
even if I set it on the onTapGesture.
What I'm I missing?
struct TasksTabView: View {
#State private var showComputedTaskSheet: Bool = false
#State var selectedTask: OrderTaskCheck?
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(Array(tasks.enumerated()), id:\.1.title) { (index, task) in
VStack(alignment: .leading, spacing: 40) {
HStack(spacing: 20) {
PillForRow(index: index, task: task)
}.padding(.bottom, 30)
}.onTapGesture {
// Where I'm setting selectedTask
self.selectedTask = task
self.showComputedTaskSheet.toggle()
}
}
}
}.listStyle(SidebarListStyle())
}
.sheet(isPresented: $showComputedTaskSheet) {
// self.selectedTask is returns nil
showScreen(task: self.selectedTask!)
}
.onAppear {
UITableView.appearance().backgroundColor = .white
}
}
Since I have no access to your full project this example can help you to get the idea, you can use .sheet() with item initializer like aheze said.
The advantage is here you pass optional to input item and you receive unwrapped safe value to work!
struct ContentView: View {
#State private var customValue: CustomValue?
var body: some View {
Button("Show the Sheet View") { customValue = CustomValue(description: "Hello, World!") }
.sheet(item: $customValue){ item in sheetView(item: item) }
}
func sheetView(item: CustomValue) -> some View {
return VStack {
Text(item.description)
Button("Close the Sheet View") { customValue = nil }.padding()
}
}
}
struct CustomValue: Identifiable {
let id: UUID = UUID()
var description: String
}

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

A View with an ObservedObject and nothing will allow me to edit it

https://github.com/ryanpeach/RoutinesAppiOS
A Picker will not move, the Edit Button won't click, and a Text Field won't enter more than 1 character. Return on keyboard doesn't work. Most of these are tied to bindings to the ObservedObject and should be able to directly edit it. I believe there is a common cause with CoreData hanging on object updates. The done button on the final player view hangs, but the skip button does not! That means it only happens when TaskData is updated. If you delete an Alarm in certain situations the app crashes too.
Here's a video of the app behavior so far. In the view right before the end nothing will click. At the final view it hangs when you click the checkmark.
https://www.icloud.com/photos/#0HMiYqZ08ZYoFu5BEQXET4gRA
I am seeking some tips on how to debug this error. I can't put a breakpoint on the picker "when something changes" and I similarly cant do it on the Text Field. Why would a text field only take one character and then stop? Why would the edit button not work? This is the only view in the app where these sub-views don't work, the rest of the app works fine.
Some relevant background information:
I'm using coredata. There are 3 classes: AlarmData for the Routines Page, which has a one2many relationship to TaskData for the TaskList Page, which has a one2many relationship to SubTaskData for the TaskPlayerView and TaskEditor pages, the ones I'm having trouble with.
No further relationships.
I'm doing a fetchrequest at the root view and then using #ObservedObject the rest of the way down the view hierarchy. I'm using mostly isActive and tag:selection NavigationLinks.
The relevant file:
struct TaskEditorView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var taskData: TaskData
#State var newSubTask: String = ""
var subTaskDataList: [SubTaskData] {
var out: [SubTaskData] = []
for sub_td in self.taskData.subTaskDataList {
out.append(sub_td)
}
return out
}
var body: some View {
VStack {
TitleTextField(text: self.$taskData.name)
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
TimePickerRelativeView(time: self.$taskData.duration)
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
HStack {
Spacer().frame(width: DEFAULT_LEFT_ALIGN_SPACE, height: DEFAULT_HEIGHT_SPACING)
ReturnTextField(
label: "New Subtask",
text: self.$newSubTask,
onCommit: self.addSubTask
)
Button(action: {
self.addSubTask()
}) {
Image(systemName: "plus")
.frame(width: DEFAULT_LEFT_ALIGN_SPACE, height: 30)
}
Spacer().frame(width: DEFAULT_HEIGHT_SPACING)
}
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
Text("Subtasks:")
Spacer().frame(height: DEFAULT_HEIGHT_SPACING)
List {
ForEach(self.subTaskDataList, id: \.id) { sub_td in
Text(sub_td.name)
}
.onDelete(perform: self.delete)
.onMove(perform: self.move)
}
}
.navigationBarItems(trailing: EditButton())
}
...
}
It also doesn't like to be edited here (see FLAG comment):
struct TaskPlayerView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var taskDataList: [TaskData] {
return self.alarmData.taskDataList
}
var taskData: TaskData {
return self.taskDataList[self.taskIdx]
}
var subTaskDataList: [SubTaskData] {
var out: [SubTaskData] = []
for sub_td in self.taskData.subTaskDataList {
out.append(sub_td)
}
return out
}
#ObservedObject var alarmData: AlarmData
#State var taskIdx: Int = 0
// For the timer
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var isPlay: Bool = true
#State var done: Bool = false
#State var startTime: Date?
#State var lastTime: Date = Date()
#State var durationBeforePause: TimeInterval = 0
#State var durationSoFar: TimeInterval = 0
var body: some View {
VStack {
if !self.done {
...
HStack {
Spacer()
Button(action: {
withAnimation {
if self.subTaskDataList.count == 0 || self.subTaskDataList.allSatisfy({$0.done}) {
// FLAG: It fails here where setting task data
self.taskData.lastDuration_ = self.durationSoFar
self.taskData.done = true
self.taskData.lastEdited = Date()
self.next()
}
}
}) {
if self.subTaskDataList.count == 0 || self.subTaskDataList.allSatisfy({$0.done}) {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 100.0, height: 100.0)
} else {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 100.0, height: 100.0)
.foregroundColor(Color.gray)
}
}
Spacer()
}
...
}
...
}
...
}

Do something when Toggle state changes

I am using SwiftUI and want to do something when toggle state changes but I cannot find a solution for this.
How can I call a function or do whatever when toggle state changes ?
#State var condition = false
Toggle(isOn: $condition) {
Text("Toggle text here")
}
If you want to do something whenever the toggle state change, you may use didSet.
struct ContentView: View {
#State var condition = false{
didSet{
print("condition changed to \(condition)")
}
}
var body: some View {
let bind = Binding<Bool>(
get:{self.condition},
set:{self.condition = $0}
)
return Toggle(isOn: bind){
Text("Toggle text here")
}
}
}
When we say #State var condition = true, it's type is State<Bool> and not Bool.
when we use binding in toggle ($condition in this case), toggle directly changes the stored value. Meaning it never changes the #State, so didSet will never be triggered.
So here we create and assign to let bind an instance of Binding<Value> struct, and use that Binding struct directly in toggle. Than when we set self.condition = $0, it will trigger didSet.
You can use the .onChange modifier like in the first example below.
However, keep in mind that in most cases we don't need it.
In case of a view setting, you bind to a #State bool value, then you use this bool to modify the view hierarchy, as in the first example.
In case of a change in your model, you bind the toggle to a #Published value in your model or model controller. In this case, the model or model controller receive the change from the switch, do whatever is requested, and the view is rerendered. It must have an #ObservedObject reference to your model.
I show how to use the .onChange modifier in the first example. It simply logs the state.
Remember there should be as few logic as possible in SwiftUI views.
By the way that was already true far before the invention of SwiftUI ;)
Example 1 - #State value. ( Local UI settings )
import SwiftUI
struct ToggleDetailView: View {
#State var isDetailVisible: Bool = true
var body: some View {
VStack {
Toggle("Display Detail", isOn: $isDetailVisible)
.onChange(of: self.isDetailVisible, perform: { value in
print("Value has changed : \(value)")
})
if isDetailVisible {
Text("Detail is visible!")
}
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleDetailView_Previews: PreviewProvider {
static var previews: some View {
ToggleDetailView()
}
}
Example 2 - #ObservedObject value. ( Action on model )
import SwiftUI
class MyModel: ObservableObject {
#Published var isSolid: Bool = false
var string: String {
isSolid ? "This is solid" : "This is fragile"
}
}
struct ToggleModelView: View {
#ObservedObject var model: MyModel
var body: some View {
VStack {
Toggle("Solid Model", isOn: $model.isSolid)
Text(model.string)
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleModelView_Previews: PreviewProvider {
static var previews: some View {
ToggleModelView(model: MyModel())
}
}
Try
struct ContentView: View {
#State var condition = false
var body: some View {
if condition {
callMe()
}
else {
print("off")
}
return Toggle(isOn: $condition) {
Text("Toggle text here")
}
}
func callMe() {
}
}
Karen please check
#State var isChecked: Bool = true
#State var index: Int = 0
Toggle(isOn: self.$isChecked) {
Text("This is a Switch")
if (self.isChecked) {
Text("\(self.toggleAction(state: "Checked", index: index))")
} else {
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
}
}
Add function like this
func toggleAction(state: String, index: Int) -> String {
print("The switch no. \(index) is \(state)")
return ""
}
I tend to use:
Toggle(isOn: $condition, label: {
Text("Power: \(condition ? "On" : "Off")")
})
.padding(.horizontal, 10.0)
.frame(width: 225, height: 50, alignment: .center)

Resources