Passing Variables between Views SwiftUI - variables

Basic questions again:
I want to make the variable "anytext" visible and accessible for all future views I am going to add. In my case the variable is going to be a String.
Does the procedure change if it's a Float?
How can I save it as a Global variable?
Does the variable delete itself if I restart the app? And how do I save variables that will remain even after restarting the app?
import SwiftUI
struct Entry: View {
#State var anytext: String = ""
var body: some View {
VStack {
TextField("Enter text here", text: $anytext)
.padding()
.border(Color.black, width: 1)
.padding()
}
}
}
struct Entry_Previews: PreviewProvider {
static var previews: some View {
Entry()
}
}

Create & pass in an environment object at the root view of your app, or where any 'children' views may need access to anytext. Store anytext as a #Published property in that ObservedObject.
That's a pointer, but there will be lots of similar questions and stuff. Here is a HWS article which may help.
Here is an example:
class MyModel: ObservableObject {
#Published var anytext = ""
}
struct ContentView: View {
#StateObject private var model = MyModel()
var body: some View {
Entry().environmentObject(model)
}
}
struct Entry: View {
#EnvironmentObject private var model: MyModel
var body: some View {
VStack {
TextField("Enter text here", text: $model.anytext)
.padding()
.border(Color.black, width: 1)
.padding()
TextDisplayer()
}
}
}
struct TextDisplayer: View {
#EnvironmentObject private var model: MyModel
var body: some View {
Text(model.anytext)
}
}
Result:
All three views have model which they can access, to get the anytext property.
To answer your questions:
You use #Published because String, Float, Double etc are all value types.
Using environment objects, as shown here.
They are not persisted, see #AppStorage for saving that.

Related

TornadoFx pass data from mysql to table view

I have a class to pass notes from mysql to tableview in kotlin but i cant seem to make it work
Im a little new in kotlin for desktop, only used in android with firebase
This is my class to get the notes
class Notes(id_notes: Int = 0, title: String = "none", description: String = "none"){
private var id_notes: SimpleIntegerProperty = SimpleIntegerProperty(id_notes)
private var title: SimpleStringProperty = SimpleStringProperty(title)
private var description: SimpleStringProperty = SimpleStringProperty(description)
fun getId(): Int {
return id_notes.get()
}
fun setId(id: Int) {
id_notes.set(id)
}
fun getTitle(): String {
return title.get()
}
fun setTitle(Title: String) {
title.set(Title)
}
fun getDescription(): String {
return description.get()
}
fun setDescription(Description: String) {
description.set(Description)
}
then i have the actual code
tableview(data){
prefWidth = 400.0
column("ID", Notes::getId)
column("Title", Notes::getTitle)
rowExpander {
label {
this.text = Notes::getDescription.toString()
}
}
}
private fun getNotes(){
try {
val notes = Notes()
val sql = ("SELECT id_notes, title, description, date FROM notes")
val con: Connection? = Conn.connection()
stmt = con?.createStatement()
rs = stmt?.executeQuery(sql)
while (rs!!.next()) {
notes.setId(rs!!.getInt("id_notes"))
notes.setDescription(rs!!.getString("description"))
notes.setTitle(rs!!.getString("title"))
data.add(notes.toString())
}
} catch (ex: SQLException) {
alert(Alert.AlertType.ERROR, "Error", "Could not perform this action")
}
}
At the end I will try to solve your problem, but please, read this part first, because this is far more import for you than the actual answer. I believe your programing skills (for now) are not the required for the kind of things you are trying to accomplish, especially because you are converting your class to string before adding it to your data (which seem to be a collection of string not a collection of Notes), so I don’t know how you expect the tableview will get your Id, Title and Description.
Also, you have a constructor for Notes, but you are overcomplicating things by not using it and assign values later. In other hand, you getNotes() function is never call in your code, probably is called in some other part you are not showing.
Because of that, I think you should slow down a little bit, try to level up your basic skills (specially working with classes and collections), them read the tornadofx manual, and them try with this kind of stuff.
Now this is my solution. First try this without the database. I did it this way because I don’t know if there is any problem with your database. Them change the getNotes() function to the way is in your code, without converting the notes.toString(), just de data.add(notes). Remember to click the button to load the data.
class Prueba: View("MainView") {
//data should be an FXCollections.observableArrayList<Notes>
//You didn't show your data variable type, but apparently is some collection of string
val data = FXCollections.observableArrayList<Notes>()
override val root = vbox {
tableview(data){
prefWidth = 400.0
column("ID", Notes::getId)
column("Title", Notes::getTitle)
rowExpander() {
label() {
//Note the difference here, Notes::getDescription.toString() won't do what you want
this.text = it.getDescription()
}
}
}
//This button is calling the function getNotes(), so data can be loaded
button("Load Data") {
action {
getNotes()
}
}
}
//Note this function is out side root now
private fun getNotes() {
data.clear()
data.add(Notes(1,"Title 1", "Description 1"))
data.add(Notes(2,"Title 2", "Description 2"))
data.add(Notes(3,"Title 3", "Description 3"))
}
}

Cannot invoke initializer for type 'TextField<_>' with propertyWrapper of UseDefaults

I am working on a SwiftUI screen that updates multiple values in the UserDefaults, to allow the app to persist basic settings. I am trying to use Combine and SwiftUI, as this is a native WatchOS app.
The basic View is giving me an error that I believe has to do with the propertyWrapper for UserDefaults, but as I have never worked with propertyWrappers (or Combine for that matter) I am un able to figure out how to fix this.
here's the property wrapper:
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
UserDefaults(suiteName: "group.com.my.app")!.value(forKey: key) as? T ?? defaultValue
} set {
UserDefaults(suiteName: "group.com.my.app")!.set(newValue, forKey: key)
}
}
}
As you can see it wraps all the Key Pairs for the UserDefaults.
My class is similarly very simple, consisting of two bools and a double
import SwiftUI
import Foundation
import Combine
class Setup: ObservableObject {
private var notificationSubscription: AnyCancellable?
let objectWillChange = PassthroughSubject<Setup,Never>()
#UserDefault("keyOpt1Enabled", defaultValue: false)
var opt1Enabled: Bool {
willSet{
objectWillChange.send(self)
}
}
#UserDefault("keyOpt2Enabled", defaultValue: false)
var opt2Enabled: Bool {
willSet{
objectWillChange.send(self)
}
}
#UserDefault("keyValueDouble", defaultValue: Double(0.00))
var someValueDouble: Double {
willSet{
objectWillChange.send(self)
}
}
init() {
notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
self.objectWillChange.send(self)
}
}
}
The problem is that in SwiftUI I am using a TextField to allow for entering and updating the double value
#ObservedObject var setup: Setup = Setup()
private var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
var body: some View {
ScrollView{
HStack{
TextField(self.$setup.someValueDouble,
formatter: currencyFormatter,
placeholder: "0.00",
onEditingChanged: {_ in
print("editing changed")
},
onCommit: {
print("updated")
}
)
}
HStack{
Button(action: {
self.setup.opt1Enabled = false
self.setup.opt2Enabled = true
} ) {
Text(verbatim: "Opt 1")
.font(Font.system(size: 16, design: Font.Design.rounded))
}
.background(setup.opt1Enabled ? Color.blue : Color.gray)
.disabled(self.setup.opt1Enabled)
.cornerRadius(5)
Button(action: {
self.setup.opt1Enabled = true
self.setup.opt2Enabled = false
}) {
Text(verbatim: "Opt 2")
}
.background(setup.opt2Enabled ? Color.blue : Color.gray)
.disabled(self.setup.opt2Enabled)
}
}
}
}
The TextField then gives the message that the Generic parameter 'Label' could not be inferred. Xcode offers to "Fix" this but the end results is TextField which is obviously incomplete, but the only view in this whole program is "ContentView" which is invalid.
The problem is that TextField's initialiser takes first a String as it's placeholder and next the value it is bound to, so it should look roughly like this:
TextField("Placeholder",
value: self.$setup.someValueDouble,
formatter: currencyFormatter,
onEditingChanged: { _ in },
onCommit:{})
.keyboardType(.numberPad)
You might want to read more about TextFields and Formatter, because it seems to be a tricky subject.

TableView and Fragment to edit Details with tornadofx

I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}

RxJava how to group items of one list to Map<Key, List<Value>>

I have a class "Cabinet" with such structure:
class Cabinet {
var title: String
var floor: Int
var cabinetNumber: Int
}
And another class "Tabs"
class Tabs {
var floor: Int
var cabinetList: List<Cabinet>
}
So with help of RxJava I'm trying to create Observable<List<Tabs>>.
Firstly I get a List of Cabinet, so I have to get all "cabinets" to "Tabs" structure.
fun getCabinets(): Observable<List<Tabs>> {
return getCabs() //This metod returns List<Cabinet>
.observeOn(Schedulers.io))
.filter { it.title != null }
... ??? //There I don't get what to do next
I thought to use "toMap" to create collection Map<floor: Int, cabinetList: List<Cabinet>> but don't know how to make it reqursively.
Please help
To put your cabinets into a Map you could utilize collectInto operator:
getCabs()
.filter { it.title != null }
.collectInto(mutableMapOf<Int, MutableList<Cabinet>>()) { map, cabinet ->
if(map.contains(cabinet.floor)) map[cabinet.floor]?.add(cabinet)
else map.put(cabinet.floor, mutableListOf(cabinet))
}
// here is your Map<Int, List<Cabinet>>
Going further, you could map your Map to List<Tab> using flatMapIterable and Set of Map entries:
.toObservable() // because collectInto returns Single
.flatMapIterable { it.entries }
.map { Tab(floor = it.key, cabinetList = it.value) }
// here is your Observable<List<Tabs>>
P.S. I assume that you use RxJava2.

TornadoFX how to add validation while editing TableView

Consider folowing example:
class Item(name: String, number: Int) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val numberProperty by lazy { SimpleIntegerProperty(number) }
var number by numberProperty
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).makeEditable(NumberStringConverter())
enableCellEditing()
}
}
}
How can I add a validator while editing cells? Is the only way to do that is to add rowExpander with some textfield and try to validate a model there?
You can either implement your own cellfactory and return a cell that shows a textfield bound to a ViewModel when in edit mode and an label if not. Alternatively, if you're fine with always displaying a textfield, you can use cellFormat and bind the current item to an ItemModel so you can attach validation:
class ItemModel(item: Item) : ItemViewModel<Item>(item) {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFormat {
val model = ItemModel(rowItem)
graphic = textfield(model.number, NumberStringConverter()) {
validator {
if (model.number.value == 123) error("Invalid number") else null
}
}
}
}
}
}
It will look like this:
While it works, it's sort of wasteful since the nodes are recreated frequently. I would recommend approach number one if performance is a concern, until we get cellFragment support for TableView like we have for ListView.
EDIT: I implemented cellFragment support, so it's possible to create a more robust solution which will show a label when not in edit mode and a validating textfield when you enter edit mode.
class ItemModel : ItemViewModel<Item>() {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFragment(NumberEditor::class)
}
}
}
class NumberEditor : TableCellFragment<Item, Number>() {
// Bind our ItemModel to the rowItemProperty, which points to the current Item
val model = ItemModel().bindToRowItem(this)
override val root = stackpane {
textfield(model.number, NumberStringConverter()) {
removeWhen(editingProperty.not())
validator {
if (model.number.value == 123L) error("Invalid number") else null
}
// Call cell.commitEdit() only if validation passes
action {
if (model.commit()) {
cell?.commitEdit(model.number.value)
}
}
}
// Label is visible when not in edit mode, and always shows committed value (itemProperty)
label(itemProperty) {
removeWhen(editingProperty)
}
}
// Make sure we rollback our model to avoid showing the last failed edit
override fun startEdit() {
model.rollback()
}
}
This will be possible starting from TornadoFX 1.7.9.