Is there a way to solve the no-data when I click on the view for the first time? In Swift - swift

I want to display the content of a Card with a Modal View that appears when I click on the Custom Card, but when I click only one card (not different card) , it displays a Model View with no data in it. After that, it works good for every card.
This is the code where the error is:
import SwiftUI
struct GoalsView: View {
#Binding var rootActive: Bool
#State private var showSheet = false
#Binding var addGoalIsClicked: Bool
#State private var selectedGoal : Goal = Goal(title: "", description: "")
var body: some View {
ScrollView {
VStack(){
HStack{
Text("Add goals")
.fontWeight(.bold)
.font(.system(size: 28))
Spacer()
}
.padding()
ForEach(goalDB) {goal in
Button() {
selectedGoal = goal
showSheet = true
}label: {
Label(goal.title, systemImage: "")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.padding()
.fullScreenCover(isPresented: $showSheet) {
ModalView(rootActive: $rootActive, goal: selectedGoal)
}
.background {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
}
}
.padding(4)
}
}.background(Color.gray.opacity(0.15))
}
}
I tried to find a solution on the Internet (someone has my same problem), reading the documentation of the FullScreenCover but I can not find out how to do it

Instead of using a Boolean to control whether or not to show a sheet and/or full screen cover (I'm going to use "sheet" throughout, the principle is identical for both), you can instead use an optional object.
In this scenario, you start with an optional value that is nil. When you want your sheet to display, you set the object to an actual value. Dismissing the sheet will return your state object to nil, just as dismissing a sheet managed by a Boolean resets the state to false.
In your code, you can remove your showSheet state value, and convert selectedGoal to an optional:
#State private var selectedGoal: Goal?
Your button action now only needs to set this value:
Button {
selectedGoal = goal
} label: {
// ...
}
And then in your modifier, you use the item: form instead. The closure receives the unwrapped version of the state object – i.e., it'll always get a Goal, rather than a Goal?. This is the value you should pass into the view being presented:
.fullScreenCover(item: $selectedGoal) { goal in
ModalView(rootActive: $rootActive, goal: goal)
}

Related

When pressing button and toggling a Bool var all of my Bool vars toggle?

I have a form with several Boolean variables that change images on toggle, but when I click any of the buttons, all my variables change images? I have pulled all of them into separate subviews with the same result. Doesn't make any sense to me. Any help would be appreciated. Probably something simple I'm overlooking, but I'll be damned if I can see it.
Here is the code.
#State var total: String = ""
#State var date: Date = Date()
#State var bathroom: Bool = false
#State var steps: Bool = false
#State var furniture: Bool = false
#State var travel: Bool = false
#State var woodFloor: Bool = false
#State var concreteFloor: Bool = false
#State var takeUp: Bool = false
var body: some View {
VStack {
Form {
Section(header: Text("Job Info")
.font(.title2)
.foregroundColor(.blue)
.fontWeight(.bold)
.padding(.bottom, 10)
) {
VStack(alignment: .leading) {
DatePicker("Date", selection: $date)
.font(.title3)
.foregroundColor(.black)
.padding(.bottom, 10)
HStack(alignment: .center) {
Text("Total: $")
.font(.title3)
.foregroundColor(.black)
.padding(.trailing, 0)
TextField("", text: $total)
.font(.title3)
.frame(width: 100, height: 20, alignment: .leading)
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(5)
Spacer()
}
.padding(.bottom, 20)
}
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) {
Spacer()
HStack {
Text("Bathroom")
Spacer()
Button {
self.bathroom.toggle()
} label: {
Image(systemName: bathroom ? "checkmark.square" : "square")
}
}
HStack {
Text("Steps")
Spacer()
Button {
self.steps.toggle()
} label: {
Image(systemName: steps ? "checkmark.square" : "square")
}
}
HStack {
Text("Furniture")
Spacer()
Button {
self.furniture.toggle()
} label: {
Image(systemName: furniture ? "checkmark.square" : "square")
}
}
Spacer()
}
}
}
}
You put all buttons in one row (VStack with HStacks creates one view, so one row), and Form (being a List) sends all actions whenever any button is clicked in a row (it is designed to have one active element in a row).
So the solution would be either to remove VStack
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) { // << this !!
Spacer()
and let every HStack with button live in own row...
... or instead of buttons use Image with tap gesture, like
HStack {
Text("Steps")
Spacer()
Image(systemName: steps ? "checkmark.square" : "square")
.padding()
.onTapGesture {
self.steps.toggle()
}
}
If you have a button inside a Section of Form the whole section will act as a button. So when you tap on it, all 3 button actions get executed.
instead, You can use Image with .onTapGesture{} modifier. In that way, you'll get what you want.
Sample code,
Image(systemName: bathroom ? "checkmark.square" : "square")
.foregroundColor(.blue)
.onTapGesture {
self.bathroom.toggle()
}
I think you are expecting the wrong thing from Form. Forms are not to make flexible lists with fancy stuff. If you want to be able to do anything you want and be able to customize your list at will, you should probably use Grids and other normal components like VStack...
How to use Form and what to expect:
The easiest way to see what you can do or not do with Forms without working around the limitations, is to go to the settings on your iPhone and see how different part of the settings are made. You can achieve almost all those stuff very easily without much work. However, if you want something even a little bit different than what you see in the settings, then you probably need a workaround to implement that in your app because Forms are not flexible.
Example of what you can do with Forms, taken from the settings:
For clarification, i am not saying you can't achieve what you want using forms. I'm saying thats most likely not a good idea to try to force forms to be something that they are not supposed to be.
The Answer:
So what should you do now? You should probably replace your button with Toggles so you get something similar to what you see in the settings.
[Other people have already said how to fix your current problem, so i'll just say the better way]
Consider using something like this:
struct ContentView: View {
#State var date = Date()
#State var total = ""
#State var bathroom = false
#State var steps = false
#State var furniture = false
var body: some View {
NavigationView { // DELETE this if the view before this view
// already has a navigation view
Form {
Section(header: Text("Job Info")) {
DatePicker("Date", selection: $date)
NavigationLink.init(
destination: FormTextFieldView(name: "Total $", value: $total),
label: {
Text("Total")
Spacer()
Text("$ " + total).foregroundColor(.secondary)
})
}
Section(header: Text("Addons")) {
Toggle("Bathroom", isOn: $bathroom)
Toggle("Steps", isOn: $steps)
Toggle("Furniture", isOn: $furniture)
}
}
.navigationBarTitle("Form", displayMode: .inline)
}
}
}
struct FormTextFieldView: View {
let name: String
#Binding var value: String
var body: some View {
Form {
TextField(name, text: $value)
}
.navigationBarTitle(name)
}
}
Why to use this?
First, because it is built with No Effort. You barely need to use any modifiers. So simple!
Second, this will work well on Any apple device. When you use modifiers, specially setting a frame for the view, you'll need to consider what will happen if you use e.g. an iPad. You don't need to worry about that when you are using this approach.

SwiftUI - How do I change a synchronous value using an asynchronous operation inside a List Cell

I have this previous question properly answer. That case is of an image somewhere in the interface.
I have another variation of the same problem but now the image is inside a List Cell.
That image shows a padlock that must only show if a particular inapp purchase was not purchased, on SwiftUI.
Something like
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(wasPurchased(item: item))
But as far as I see wasPurchased must be a synchronous function, right?
Something like
func wasPurchased(item: item) -> Bool {
return check(item:item) ? true : false
}
But, such checks normally happen asynchronously, over the network, and the function, as I see, must have a signature like
func wasPurchased(item: item, runOnFinishChecking:(Bool)->()) {
That list is populated from Core Data, like this
#FetchRequest(fetchRequest: Expressao.getAllItemsRequest())
private var allItems: FetchedResults<Expressao>
var body: some View {
List {
ForEach(allItems, id: \.self) { item in
HStack {
Text(item.term)
.font(fontItems)
.foregroundColor(.white)
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(wasPurchased(item: item))
}
}
}
}
I don't see how I can use something asynchronous to control the opacity of such element when the whole thing is an array.
How do I do that?
Just separate your row content into standalone view and apply approach from previous post.
var body: some View {
List {
ForEach(allItems, id: \.self) {
ExpressaoRowView(item: $0)
}
}
}
...
struct ExpressaoRowView: View {
#ObservedObject var item: Expressao
#State private var locked = true
var body: some View {
HStack {
Text(item.term)
.font(fontItems)
.foregroundColor(.white)
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(self.locked ? 1 : 0)
}
.onAppear {
self.wasPurchased(self.item) { purchased in
DispatchQueue.main.async {
self.locked = !purchased
}
}
}
}
}
Note: you can even keep wasPurchased checker somewhere outside (in parent or in some helper) and inject into ExpressaoRowView as a property

SwiftUI TextField takes max width

I have a navigation list with each list item being in this format:
HStack {
TextField("Insert something here.",text: self.$userData.pages[i].title)
.border(Color.blue)
Spacer()
}
This results in the following view:
The touchable area is highlighted by the blue border and it takes the whole width of the row
The problem with this is that despite the list item being a navigation link, the user clicking anywhere along the item will result in them editing the text content. What I would prefer is a TextField that has the same width as a Text:
The blue border wraps the text instead of taking the max width
So if the user clicks outside the TextField, the navigation works, but if they click on the text, it will let them edit the text. (The above view is with Text field).
Apologies if I've asked an unclear or bad question. I'm new to Stack Overflow and SwiftUI.
Edit:
I've tried using the fixedSize modifier, and the TextField correctly wraps my Text, but now the Navigation Link doesn't work (i.e. clicking on it just doesn't navigate). This is my full code:
NavigationLink(destination: PageView(page: self.userData.pages[i])) {
HStack {
Button(action: {}){
TextField(" ", text: self.$userData.pages[i].title)
.fixedSize()
}
.buttonStyle(MyButtonStyle())
.border(Color.blue)
Spacer()
}
}
No need to apologize, your question is clear.
You can do this by using fixedSize()
so your code should be like this
HStack {
TextField("Insert something here.",text: self.$userData.pages[i].title)
.border(Color.blue)
.fixedSize()
Spacer()
}
You can further specify how would you like the stretch to be, either vertical or horizontal or even both by passing parameters like so
.fixedSize(horizontal: true, vertical: false)
UPDATED ANSWER TO MATCH YOUR NEW REQUIREMENTS
import SwiftUI
struct StackOverflow5: View {
#State var text: String = ""
#State var selection: Int? = nil
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: Page2(), tag: 1, selection:self.$selection) {
Color.clear
.onTapGesture {
self.selection = 1
}
}
TextField("Text", text: self.$text)
.fixedSize()
}
}
}
}
struct StackOverflow5_Previews: PreviewProvider {
static var previews: some View {
StackOverflow5()
}
}
struct Page2: View {
var body: some View {
Text("Page2")
}
}
We used a ZStack here to separate between our TextField and our NavigationLink so they can be interacted with separately.
Note the use of Color.clear before our TextField and this is on purpose so that our TextField has interaction priority. Also we used Color.clear because it will stretch as a background and it's clear so it's not visible.
Obviously I hard coded 1 here but this can be from List or a ForEach
Additionally, if you don't want to use selection and tag you can do something like this
...
#State var isActive: Bool = false
...
NavigationLink(destination: Page2(), isActive: self.$isActive) {
Color.clear
.onTapGesture {
self.isActive.toggle()
}
}
....

Toggling #State variables using .OnTapGesture in SwiftUI

Can somebody tell me why this logic does not work? I am trying to create an instance of a view and store it in a variable. Then I use this variable to return a view in var body. My goal is to toggle the isActive variable of the view object on a tap so that the checkmark image is shown.
I can make this work when I put the onTapGesture inside the custom view object, but I can not get a change in state when I toggle the variable from parent view. I hope this makes sense.
struct SensorFamilyView: View {
#State var analogView = FamilyItemView(title: "Analog", isActive: false)
var body: some View {
VStack(alignment: .leading, spacing: 0) {
analogView // Show view instance
.onTapGesture { // I want this tap gesture to work
self.analogView.isActive.toggle()
}
}
}
}
struct FamilyItemView: View { // Custom View
#State var title: String
#State var isActive = false
var body: some View {
HStack {
if ( isActive ) // isActive toggles a checkmark image
{
Image(systemName: "checkmark.circle")
}
else
{
Image(systemName: "circle")
}
Text("\(title)")
}
.padding()
.onTapGesture { // This Tap works, but not what I want
//self.isActive.toggle()
}
}
}
Why won't it work?
You cannot hold an instance of FamilyItemView. Why? Because it is a struct, not a class. When you toggled the isActive property, the view is recreated (because it is using #State).
How can this be fixed?
Use #Binding. Creating a binding means that FamilyItemView will be updated when SensorFamilyView's isActive property changes. It can be used like the following:
struct SensorFamilyView: View {
#State private var isActive = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
FamilyItemView(title: "Analog", isActive: $isActive)
.onTapGesture {
self.isActive.toggle()
}
}
}
}
struct FamilyItemView: View {
#State var title: String
#Binding var isActive: Bool
var body: some View {
HStack {
if isActive {
Image(systemName: "checkmark.circle")
} else {
Image(systemName: "circle")
}
Text("\(title)")
}.padding()
}
}
Side note: As for the code right now, title does not need to be #State.
Additional clearing up of code
struct FamilyItemView: View {
let title: String
#Binding var isActive: Bool
var body: some View {
HStack {
Image(systemName: isActive ? "checkmark.circle" : "circle")
Text(title)
}.padding()
}
}
#State
To understand this, we need to first touch upon #State. What is it?
SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body.
...
A State instance isn’t the value itself; it’s a means of reading and writing the value. To access a state’s underlying value, use its variable name, which returns the wrappedValue property value.
Ref: https://developer.apple.com/documentation/swiftui/state
But why did we need #State? Well... Structs are value type and it's variables are non-mutating by default so to get around this, #State propertyWrapper was provided that basically wraps a value and stores and maintains it for us in some persitent* storage within the SwiftUI framework.
*See the WWDC on this for more details: https://developer.apple.com/videos/play/wwdc2019/226/
When a #State property is changed within the body of the View's struct in which it was declared, SwiftUI's engine automatically re-renders the body. But if it's modified from outside the view, SwiftUI does not pick up on this.
So then, now what?
That's where #Binding can be used to create a 2-way binding.
#Binding
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data. A binding connects a property to a source of truth stored elsewhere, instead of storing data directly. For example, a button that toggles between play and pause can create a binding to a property of its parent view using the #Binding property wrapper.
Ref: https://developer.apple.com/documentation/swiftui/binding
Solution:
struct SensorFamilyView: View {
#State var isActive: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
FamilyItemView(title: "Title", isActive: $isActive)
.onTapGesture {
self.isActive.toggle()
}
}
}
}
struct FamilyItemView: View {
#State var title: String
#Binding var isActive: Bool
var body: some View {
HStack {
if (isActive) {
Image(systemName: "checkmark.circle")
} else {
Image(systemName: "circle")
}
Text("\(title)")
}
}
}
SensorFamilyView has a state property isActive
FamilyItemView has a binding property isActive
There's a 2-way binding between them so when one changes, the other also changes. Furthermore, this is all within the Combine framework (which SwiftUI is heavily based on) and so the right sequence of event are fired that cause the body to render.

SwiftUI: NavigationLink is always activated when in a List

I can't prevent SwiftUI's NavigationLink from being activated when in a List, I have this simple piece of code in which I need to do some kind of business check before deciding to show the details page or not (in a real world app, there might be some business logic happens inside the button's action):
struct ContentView: View {
#State var showDetail = false
var body: some View {
NavigationView {
List {
Text("Text 1")
Text("Text 2")
Text("Text 3")
NavigationLink(destination: DetailView(), isActive: $showDetail) {
LinkView(showDetails: $showDetail)
}
}
}
}
}
struct LinkView: View {
#Binding var showDetails: Bool
var body: some View {
Button(action: {
self.showDetails = false
}) {
Text("Open Details")
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
how can I prevent navigation link from opening the details page in this case ? and is this a bug in the SDK ?
p.s. XCode version: 13.3.1 and iOS version (real device): 13.3.1
Edit
I can't replace List with ScrollView because I have a ForEach list of items in my real app, so don't post an answer considering using ScrollView.
in a real world app, there might be some business logic happens inside
the button's action
seems to be a little bit alogical.You can simply conditionally disable the link (and inform the user, that the link is unavailable by visual appearance)
NavigationLink(...).disabled(onCondition)
where
func disabled(_ disabled: Bool) -> some View
Parameters
disabled
A Boolean value that determines whether users can interact with this view.
Return Value
A view that controls whether users can interact with this view.
Discussion
The higher views in a view hierarchy can override the value you set on this view. In the following example, the button isn’t interactive because the outer disabled(_:) modifier overrides the inner one:
HStack {
Button(Text("Press")) {}
.disabled(false)
}
.disabled(true)
If I correctly understood your goal, it can be as follows
List {
Text("Text 1")
Text("Text 2")
Text("Text 3")
LinkView(showDetails: $showDetail)
.background(
NavigationLink(destination: DetailView(), isActive: $showDetail) { EmptyView() })
}
and
struct LinkView: View {
#Binding var showDetails: Bool
var body: some View {
Button(action: {
self.showDetails = true // < activate by some logic
}) {
Text("Open Details")
}
}
}
If you use .disable(true) it will reduce your list item opacity like a disabled button, to prevent this. use below code style. Use Navigation Link in backGround and check your navigation condition on Tap Gesture of your view.
VStack{
List(0..<yourListArray.count, id: \.self) { index in
{
Text("\(yourListArr[index].firstName)")
}().onTapGesture{
let jobType = getFlags(jobsArr: yourListArray, index:index)
if jobType.isCancelledFlag == true{
self.shouldNavigate = false
}else{
self.shouldNavigate = true
}
}//Tap Gesture End
.background(NavigationLink(destination: YourDestinationView(),isActive: self.$shouldNavigate) {
}.hidden())}}//vStack