Handling a list of buttons in SwiftUI - ios

I am working on a SwiftUI app (Xcode Version 12.4 and iOS 14.4.2) and am having problems to properly handle buttons in a list. I hope someone can point out the way to go.
Here is the relevant code:
struct CustomListView: View {
var localList:[SomeManagedObject], moc:NSManagedObjectContext
#State var showingOtherView = false
#State var selectNbr:Int!
func handleCustomItem(_ argument: SomeManagedObject) {
print(#function+" (1):\(showingOtherView):")
selectNbr = localList.firstIndex(of: argument)
self.showingOtherView.toggle()
print(#function+" (2):\(showingOtherView):\(selectNbr):")
..... Do useful things .....
}
var body: some View {
List {
ForEach(self.localList) {
item in
HStack {
Spacer()
Button(action: {
self.handleCustomItem(item)
})
{
Text(item.expression!)
.foregroundColor(Color.red))
.font(.headline)
.padding(.horizontal, 11).padding(.vertical, 15)
}
Spacer()
}
}
}.sheet(isPresented: $showingOtherView) {
OtherView(parameter: localList[selectNbr])
}
}
}
When I run the app this is what I get as ouput, which is what I expect:
handleCustomItem(_:) (1):false:
handleCustomItem(_:) (2):true:Optional(4):
Then I have a break point before running this line of code (to avoid a crash):
OtherView(parameter: localList[selectNbr])
This is where things get weird (unexpected for me), in the debugger I can see:
(lldb) p showingOtherView
(Bool) $R0 = false
(lldb) p selectNbr
(Int?) $R2 = nil
(lldb)
But I would expect showingOtherView to be true, and (more important) selectNbr to hold the value 4. What is going on here that I am missing ?

Related

SwiftUI Text Field Integer/Decimal Problem

I'm trying to suss out a problem I'm having. Basically, I have a text field for medical lab values, and I want it to display a symbol when out of range (out of normal medical limits) and another symbol when within normal range. These values are then used in formulae in another view in the app.
This is now my second post on this platform, so please forgive any posting faux pas, I'm trying to adhere to the rules as best as possible re: minimum reproducible and making sure my code is formatted properly in the posting. Here's what I have so far:
import SwiftUI
struct EntryMRE: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var showingResults: Int? = 1
#FocusState private var isTextFieldFocused: Bool
#State var isDone = false
#State var isSaving = false //used to periodically save data
#State var saveInterval: Int = 5 //after how many seconds the data is automatically saved
//DataPoints Chemistry
#State var potassium = ""
var body: some View {
List {
Section(header: Text("🧪 Chemistry")) {
Group {
HStack {
Text("K")
+ Text("+")
.font(.system(size: 15.0))
.baselineOffset(4.0)
Spacer()
TextField("mEq/L", text: $potassium)
.focused($isTextFieldFocused)
.foregroundColor(Color(UIColor.systemBlue))
.modifier(TextFieldClearButton(text: $potassium))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
if potassium != "" && Int(potassium) != nil {
if Int(potassium)! >= Int(Double(Int(3.5))) && Int(potassium)! <= Int(Double(4.5)) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
}
else {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
}
}
}
}
}
}
}
I've tried making it >= Double(3.5) which then pops the same error and says it should be Int(Double(3.5)) which does allow the code to build, but doesn't actually display the symbol when in range with a decimal (ExhibitA), only with a whole integer. (ExhibitB)
I've added some pictures that'll hopefully help show what I mean.
Thanks in advance!
This fails because you are converting a Double to an Int and vice versa and casting your String to an Int instead of a Double. You are loosing your digits when you do this.
Try:
if let numberValue = Double(potassium) { // Cast String to Double
if (3.5...4.5) ~= numberValue { //Check if casted value is in customRange
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
}
else {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
}
}

SwiftUI: Combining Text on the same line

the SwiftUI App I'm writing has large blocks of text, so I'm trying to create a "markup language" I can use in my JSON files that hold the text, to define when a word should be bolded. The issue is, I can't seem to stop new text blocks going to a different line.
Here is the relevant code as I currently have it.
struct formattedText: View {
var text: String
var body: some View {
let split = text.components(separatedBy: "**")
Group {
ForEach(split, id: \.self) { line in
if line.hasPrefix("$$") {
Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
Text(line)
}
}
}
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
Using this code, I can put a **$$ before a word and ** after, to define it as bold.
Only issue is every time I bold a word it goes to a new line. I know the traditional way to fix this is:
Text("Simple ") + Text("Swift ") + Text("Guide")
This does not work with my ForEach loop though. Any suggestions?
ForEach creates separate Views. You really just want one Text, so you mean a for...in loop:
var body: some View {
let split = text.components(separatedBy: "**")
var result = Text("")
for line in split {
if line.hasPrefix("$$") {
result = result + Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
result = result + Text(line)
}
}
return result
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
Since you may want a lot more things than just bold, you might find it useful to extract that part into its own function or collection of functions:
private func applyAttributes(line: String) -> Text {
if line.hasPrefix("$$") {
return Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
return Text(line)
}
}
With that, constructing this is simpler:
var body: some View {
text.components(separatedBy: "**")
.map(applyAttributes)
.reduce(Text(""), +)
.lineLimit(nil)
.multilineTextAlignment(.leading)
}

SwiftUI Picker to change second state var in the same View

I am missing something in my understanding of SwiftUI State variables.
I have a Picker and I want to apply the value from the Picker to another State var.
Specifically I want to use a Picker to spect a MassUnit and apply the new MassUnit to a Measurement. When the MassUnit is selected, the measurement uses .convert or .converted to change the Measurement to the new unit.
If you look at the code below, the value of converted is correctly calculated but I can’t set max to the new value and I can't use max.convert to change it.
What am I doing wrong? (I've tried applying a Binding var but that wasn't working. My understanding is that this view should be able to mutate this state property.)
import SwiftUI
struct ConverterView: View
{
#State var massUnit: UnitMass = .pounds
#State var min: Measurement<UnitMass> = Measurement(value: 1, unit: .pounds)
#State var max: Measurement<UnitMass> = Measurement(value: 1.5, unit: .pounds)
private let massFormatter = MewsMassFormatter()
private let unitFormatter = MewsMassFormatter(style: .long)
var body: some View {
Form {
Picker(selection: $massUnit,
label: Text("Weight Units"),
content: {
ForEach(massUnits, id: \.symbol) {
Text(unitFormatter.string(from: $0)).tag($0)
}
})
.onChange(of: $massUnit.wrappedValue, perform: { newMassUnit in
let converted = $max.wrappedValue.converted(to: newMassUnit)
$max.wrappedValue = converted //doesn't work
$max.wrappedValue.value = converted.value //doesn't work
// $max.wrappedValue.unit = converted.unit //doesn't compile
$max.wrappedValue.convert(to: newMassUnit) //doesn't work
})
TextField("Min", value: $min, formatter: massFormatter).keyboardType(.decimalPad)
TextField("Max", value: $max, formatter: massFormatter).keyboardType(.decimalPad)
Text(unitFormatter.string(from: $massUnit.wrappedValue))
}
}
}
struct ConverterView_Previews: PreviewProvider {
static var previews: some View {
ConverterView()
}
}
No need $ sign and wrapped value. you can directly assign.
.onChange(of: massUnit, perform: { newMassUnit in
let converted = max.converted(to: newMassUnit)
max = converted
max.convert(to: newMassUnit)
})
And,
Text(unitFormatter.string(from: massUnit))

Foreach if all of a field is "0", return one view

I'm trying to run through a list of items that all have a field called "yes" which is an integer. Essentially this loop runs through and if the yes field for an item is more than 0, it will show up, and if it is 0 it won't. Works as I want it to so far but what I would like to do is show a different message if ALL items have a 0 value, so that the section of the screen is not simply empty. How would I go about doing this? I tried putting the message into the "else" but (obviously) it just repeated the message the amount of times there are items in the db.
ForEach(items.indices, id: \.self) { i in
if (items[i].yes != 0) {
HStack {
Text(items[i].name)
Spacer()
Text("\(items[i].yes)")
}
Divider()
} else {}
}
You need something like below (typed in place, so might be typos):
if items.filter({ $0.yes != 0}).isEmpty {
Text("Message for ALL are 0")
} else {
ForEach(items.indices, id: \.self) { i in
if (items[i].yes != 0) {
HStack {
Text(items[i].name)
Spacer()
Text("\(items[i].yes)")
}
Divider()
} else {}
}
}

Core data always deletes first object

I am using swiftUI in my app that uses coredata to store FoodItems. In my app I have a list of all the foodItems stored on the users device. Supposedly they are supposed to be able to delete any one of the foodItems by long holding and pressing either eat or throw away, but each time I try to delete any of the foodItems, it always deletes the first one in the list instead of the one I long holded. Can anyone help me? Here is a snippet of my code:
ForEach(self.storageIndex.foodItemArray, id:\.self) { item in
RefrigeratorItemCell(icon: item.wrappedSymbol, title: item.wrappedName, lastsUntil: self.addDays(days: Int(item.wrappedStaysFreshFor), dateCreated: item.wrappedInStorageSince))
.gesture(LongPressGesture()
.onEnded({ i in
self.showEatActionSheet.toggle()
})
)
//TODO: Make a diffrence between eat all and throw away
.actionSheet(isPresented: self.$showEatActionSheet, content: {
ActionSheet(title: Text("More Options"), message: Text("Chose what to do with this food item"), buttons: [
.default(Text("Eat All"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Throw Away"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Cancel"))
])
})
}
You should use instead .actionSheet(item: and move it out of ForEach cycle, as on below scratchy example
#State private var foodItem: FoodItem? // << tapped item (use your type)
// .. other code
VStack { // << some your parent container
ForEach(self.storageIndex.foodItemArray, id:\.self) { item in
RefrigeratorItemCell(icon: item.wrappedSymbol, title: item.wrappedName, lastsUntil: self.addDays(days: Int(item.wrappedStaysFreshFor), dateCreated: item.wrappedInStorageSince))
.gesture(LongPressGesture()
.onEnded({ i in
self.foodItem = item // << tapped item
})
)
//TODO: Make a diffrence between eat all and throw away
}
} // actionSheet attach to parent
.actionSheet(item: self.$foodItem, content: { item in // << activated on item
ActionSheet(title: Text("More Options"), message: Text("Chose what to do with this food item"), buttons: [
.default(Text("Eat All"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Throw Away"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Cancel"))
])
})

Resources