SwiftUI Picker to change second state var in the same View - binding

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

Related

How to properly set DataStore for storing boolean value

I want to set a simple switch that'll save a boolean value and if then block in my function.
Currently I have this in my DataStore:
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("userToken")
private val AutoRestartSystemUI = booleanPreferencesKey("AutoRestartSystemUIValue")
}
var getAutoRestartSystemUIValue: Flow<Boolean> = context.dataStore.data.map { preferences ->
(preferences[AutoRestartSystemUI] ?: "") as Boolean
}
suspend fun setAutoRestartSystemUI(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[AutoRestartSystemUI] = value
}
}
}
and
Button(onClick = {
// if [pro.themed.manager.UserStore(context).getAutoRestartSystemUIValue = true] ()
CoroutineScope(Dispatchers.IO).launch {
UserStore(context).setAutoRestartSystemUI(false)
}
}) {
Text(text = UserStore(context).getAutoRestartSystemUIValue.collectAsState(
initial = ""
).toString())
}
in my activity. I have generally no idea of what I should do next and for some weird reason instead of showing value in a text (temp solution for testing) i have
How do i simplify datastore? How do I properly implement switch that'll make it = !it? How to set default value?

I have a composable not setting button text as expected; wondering why. Have a reproducible example

this started as a new compose project
with the following code the intent is to change the text to the picked time. The code is commented where the behavior occurs
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TimeCardButton(id = 1, symbol ="In", enabled=true,modifier = Modifier) { entry ->
Log.d("click", "$entry result")
}
}
}
}
}
}
data class TimeCardEntry(val id: Int = -1, var entry: String = "")
#Composable
fun TimeCardButton(
id: Int,
symbol: String,
enabled: Boolean = false,
modifier: Modifier,
onValueChange: (TimeCardEntry) -> Unit = {},
) {
// Value for storing time as a string
val timeState = remember {
mutableStateOf(TimeCardEntry(id, symbol))
}
val validState = remember {
timeState.value.entry.trim().isNotEmpty()
}
val mTime = remember { mutableStateOf(symbol) }
if (enabled) {
// Fetching local context
val mContext = LocalContext.current
// Declaring and initializing a calendar
val mCalendar = Calendar.getInstance()
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
// Creating a TimePicker dialog
val mTimePickerDialog = TimePickerDialog(
mContext,
{ _, mHour: Int, mMinute: Int ->
timeState.value.entry = "$mHour:$mMinute"
mTime.value = "$mHour:$mMinute"
onValueChange(timeState.value)
}, mHour, mMinute, false
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
TextButton(onClick = { mTimePickerDialog.show() }.also {
Log.d("click", "id $id clicked!") }) {
Column() {
// if I use just this it works [in changes to the time picked]
//Text(text = mTime.value)
// if i use both of these BOTH are set when the date picker is invoked
// if I just use the second one alone, the text never changes
Text(text = timeState.value.entry)
}
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
Text(text = symbol, color =
MaterialTheme.colors.onBackground)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApplicationTestTheme {
}
}
First of all how to fix it:
Your problem basically is this. The easiest way to fix it would be to reassign the whole value of TimeState, not just entry by calling
timeState.value = timeState.value.copy(entry = "$mHour:$mMinute")
The reason it doesn't work with only the second one is that the change of a property doesn't trigger recomposition, even if the variable containing it is a mutableState. To fix (as outlined in the answers to the question linked above) this you either have to reassign the whole variable or make the parameter you want to observe observable (for example changing the String to State<String>)
PS: if you use by with mutableStateOf (i.e. val timeState = remember { mutableStateOf(TimeCardEntry(id, symbol)) }) you don't have to use .value every time. I find that a lot cleaner and more readable

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

Handling a list of buttons in SwiftUI

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 ?

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

Resources