Kotlin, when to delegate by map? - delegates

I checked the documentation on delegate, and I found there was a provided delegate type map:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
But I couldn't figure out what's the difference between the version without delegate, like this:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String
var age: Int
}
And what's the common usage for the delegate by map?
Thanks!

Difference is that in the first example with delegate, all you have to do is to put map to the constructor and done.
val user = MutableUser(mutableMapOf(
"name" to "John Doe",
"age" to 25
))
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
But for this to work the same with your second example, you have to implement initialization of properties from map yourself.
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String
var age: Int
init {
name = map["name"].toString()
age = map["age"].toString().toInt()
}
}
One common use case would be implementing a JSON parser.
Storing Properties in a Map

Related

How to change the sort comparator or display Int? values on a Table with SwiftUI on macOS

I have the following table that mostly works when all the column values are strings:
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
}
#State var people = []
#State var sort = [KeyPathComparitor(\Person.rank)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: ranking.rank
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
This works well enough with one exception when the rank is unknown coming into the sink, its sent as an empty string and the empty strings sort before those with valid ranking. Ideally, I'd like to have those without a ranking sort after until their rank is received.
I've tried a couple of things, changing Person.rank to an Int but the compiler gives the typical 'too complex' error when:
TableColumn("Rank", value: \Person.rank) { Text(\($0.rank)) } // rank is an int here
Alternatively I've tried to create a custom sort comparator when creating the KeyPathComparator(\Person.rank) but the documentation around that is limited and haven't been able to suss out a workable solution.
Any ideas how to get the Table to simply display Ints (which I imagine would have to be optionals in this instance) or add a string comparator that moves empty strings below populated strings?
TIA
This is the best answer I could find, tho admit it does seem a bit like a kludge. Basically adding Int property for sorting and setting the sort to initially sort on this Person property
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
//Add rankForSort as an int which is a non displayed column
let rankForSort: Int
}
#State var people = []
//Change to initial sort to non-displayed rank field
#State var sort = [KeyPathComparitor(\Person.rankForSort)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
// Ranking now coming through in the subject as Int?
let rankString = ranking.rank != nil ? "\(ranking.rank!)" : ""
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: rankString, rankForSort: ranking.rank ?? Int.max)
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
Here is a way to do an initial sort and uses didSet which has the added benefit it doesn't use onChange which results in body called twice when resorting.
struct ProductsData {
var sortedProducts: [Product]
var sortOrder = [KeyPathComparator(\Product.name)] {
didSet {
sortedProducts.sort(using: sortOrder)
}
}
init() {
sortedProducts = myProducts.sorted(using: sortOrder)
}
}
struct ContentView: View {
#State private var data = ProductsData()
var body: some View {
ProductsTable(data: $data)
}
}
struct ProductsTable: View {
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#Binding var data: ProductsData
var body: some View {
Table(data.sortedProducts, sortOrder: $data.sortOrder) {
See this answer to a different question for the full sample.

How to make SwiftUI List/OutlineGroup lazy for use with large trees like a file system?

Here's a simple demo of the hierarchical List in SwiftUI. I'm testing it on macOS Big Sur, but unlike similar tree components in other UI toolkits, it asks for all its children immediately. So I can't use it for something like a file system browser.
Is there a way to make it lazy, so that it only asks for children when the UI element is expanded?
class Thing: Identifiable {
let id: UUID
let depth: Int
let name: String
init(_ name: String, depth: Int = 0) {
self.id = UUID()
self.name = name
self.depth = depth
}
/// Lazy computed property
var children: [Thing]? {
if depth >= 5 { return nil }
if _children == nil {
print("Computing children property, name=\(name), depth=\(depth)")
_children = (1...5).map { n in
Thing("\(name).\(n)", depth:depth+1)
}
}
return _children
}
private var _children: [Thing]? = nil
}
struct ContentView: View {
var things: [Thing] = [Thing("1"), Thing("2"), Thing("3")]
var body: some View {
List(things, children: \.children) { thing in
Text(thing.name)
}
}
}
Even though the initial UI only displays the top nodes:
You can see in the console that it asks for everything - all the way down the tree. This is a performance problem for large trees.
...
Computing children property, name=3.4.4.1.4, depth=4
Computing children property, name=3.4.4.1.5, depth=4
Computing children property, name=3.4.4.2, depth=3
Computing children property, name=3.4.4.2.1, depth=4
Computing children property, name=3.4.4.2.2, depth=4
...
I believe this could be a bug in SwiftUI and I hope Apple will fix this. In the meantime, you can use the following workaround:
struct Node {
var id: String
var value: String
var children: [Node]?
}
struct LazyDisclosureGroup: View {
let node: Node
#State var isExpanded: Bool = false
var body: some View {
if node.children != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node.children!, id: \.self.id) { childNode in
LazyDisclosureGroup(node: childNode)
}
}
},
label: { Text(node.value) })
} else {
Text(node.value)
}
}
}
struct ContentView: View {
let node = Node(
id: "a",
value: "a",
children: [Node(id: "b", value: "b", children: nil),
Node(id: "c", value: "c", children: nil)])
var body: some View {
List {
LazyDisclosureGroup(node: node)
}
}
}
I don't know if that's a best practice but the workaround uses the observation that DisclosureGroup "notices" when it is inside a List. The combination of the two produces the same visual structure and behaviour.

SwiftUI: Creating own data types and work with them. How?

currently I am programming my first own app in SwiftUI. I want to create a custom datatype called "Exercise". For this I have created a struct:
struct Exercise: Identifiable {
var id = UUID()
var name: String = ""
var description: String?
}
Also I have a class with different data where I have created a instance of that struct in form of an array:
var exercise: [Exercise] = []
Now I want to append to these array with a new element in a different view where I have a textfield for the name of the Exercise and a textfield for the description. But I don't know how I can do this. Can anybody help me?
Thanks in advance and sorry for my English :)
EDIT:
Code with the View where I want to append a new element to the class with the Array "exercise":
var body: some View {
NavigationView {
VStack(spacing: 20){
Text("Name")
.alignmentNewExercise()
.font(.title)
TextField(placeholder, text: $newExercise)
.alignmentNewExercise()
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Description", text: $description)
.alignmentNewExercise()
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
exercise.name = newExercise
exercise.description = description
//data.exercise.append(...) **here I want to add the new element to the array in the class "data", but I don't know how**
newExercise = ""
}) {
Text("Hinzufügen")
}
.disabled(validExercise)
Spacer()
}
.navigationBarTitle("Neue Übung")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Zurück")
})
}
}
The class with the array:
class Daten: ObservableObject{
#Published var exercise: [Exercise] = []
}
To append new data you can create a new Exercise and add it to your collection:
let exercise = Exercise(name: self.newExercise, description: self.description)
data.exercise.append(exercise)
Which can be shortened to:
data.exercise.append(Exercise(name: self.newExercise, description: self.description))
Also try using plural names for your collections:
#Published var exercises: [Exercise] = []
It indicates it's a collection and not a single object.
You should pass an instance of Daten around. Here I used a singleton object for it as a shared instance:
class Daten: ObservableObject{
static let shared = Daten()
#Published var exercise: [Exercise] = []
}
then you can access it and change the value like any other array in Swift:
Button("Hinzufügen") {
let exercise = Exercise(
name: self.newExercise,
description: self.description)
Daten.shared.exercise.append(exercise)
}

fragment for flowpane in TornadoFX

The TornadoFX docs describe using the ListCellFragment to bind each cell in a list control to each item model in a list. I am wondering how to do something similar in a flowpane. I'd like to use that kind of class to render a bunch of controls and an SVG drawing in each cell. (So it would replace the button component in the example code below and somehow bind the shapeItem model to it).
class LibraryView : View("Current Library") {
val shapeLibraryViewModel : LibraryViewModel by inject()
override val root = anchorpane{
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty){
shapeItem -> button(shapeItem.nameProperty)
}
}
}
}
Since I don't see a pre-made class like the one for list view, perhaps I would need to create something similar to it...or maybe there's a more lightweight approach?
Using an ItemFragment is a bit overkill, since the itemProperty of the fragment will never change (a new fragment would be created for every item whenever the libraryItemsProperty change. However, if your view logic for each item is substantial, this approach will provide a clean way to separate and contain that logic, so it might be worth it. Here is a complete example you can use as a starting point.
class ShapeItemFragment : ItemFragment<ShapeItem>() {
val shapeModel = ShapeItemModel().bindTo(this)
override val root = stackpane {
label(shapeModel.name)
}
}
class ShapeItem(name: String) {
val nameProperty = SimpleStringProperty(name)
}
class ShapeItemModel : ItemViewModel<ShapeItem>() {
val name = bind(ShapeItem::nameProperty)
}
class LibraryViewModel : ViewModel() {
val libraryItemsProperty = SimpleListProperty<ShapeItem>(
listOf(
ShapeItem("Shape 1"),
ShapeItem("Shape 2")
).observable()
)
}
class LibraryView : View("Current Library") {
val shapeLibraryViewModel: LibraryViewModel by inject()
override val root = anchorpane {
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
val itemFragment = find<ShapeItemFragment>()
itemFragment.itemProperty.value = shapeItem
itemFragment.root
}
}
}
}
A slightly lighter version would be to pass the parameter into your fragment manually and just extend Fragment:
class ShapeItemFragment : Fragment() {
val item: ShapeItem by param()
override val root = stackpane {
label(item.nameProperty)
}
}
You can still bind to changes to properties inside the ShapeItem, since the underlying item won't change (as seen from the ItemFragment) anyway.
Your bindChildren statement would then look like this:
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
find<ShapeItemFragment>(ShapeItemFragment::item to shapeItem).root
}

swift programming superclass variable update from subclasses

I'm just starting to take up programming and am trying to understand the way a subclass could update a variable in a superclass. I have a family superclass that keeps track of family members and every time a member of the family is created the family members variable adds 1 to it. here is what i have.
I have tried a couple ways of doing this, including creating a update family members method in the superclass and calling it from the child classes in their init statements but it still didn't work. If anyone could help me or at least explain why its not updating I would be grateful. Thanks.
Oh, the playground states it runs the familymembers++ 4 times, but the variable is not keeping the updates. I have also tried creating two variable and having one update the other but still nothing.
EDIT:
Thank you. I will try and understand the concept better. I appreciate your help. Thanks :D
class Family {
var familyMembers = 0
init() {
familyMembers++
}
func talk() {
println("The family is talking")
}
func argue() {
println("The family is arguing")
}
}
class Son : Family {
var hisName = ""
init(name: String) {
super.init()
self.hisName = name
}
override func argue() {
println("\(hisName) is arguing")
}
}
class Mother : Family {
var herName = ""
init(name: String) {
super.init()
self.herName = name
}
}
let barthelemy = Family()
let ayden = Son(name: "ayden")
let tasha = Mother(name: "tasha")
let jim = Son(name: "jim")
barthelemy.familyMembers
This is because each time you create a Family object or a subclass (so a Son or Mother), you're referencing a different Family: familyMembers++ gets called, but it's for the variable for each specific family member rather than a shared variable that is accessed by each object.
It seems like you're a little confused about what a subclass (and, by extension, inheritance) symbolizes: instead of thinking that a subclass belongs to its parent class, pretend that a subclass is a more specific version of its parent class. For example, a Jaguar would be a subclass of an Animal; a Sedan would be a subclass of a Car; to use your example, a Son would not be a subclass of a Family, but a subclass of a FamilyMember.
Consider the following example, in which we pass a common Family object to the FamilyMember constructor to keep track of the number of members:
import Cocoa
class Family {
var familyMembers = 0
func talk() {
println("The family is talking")
}
func argue() {
println("The family is arguing")
}
}
class FamilyMember {
init(family: Family) {
family.familyMembers++
}
}
class Son : FamilyMember {
var hisName = ""
init(family: Family, name: String) {
super.init(family: family)
self.hisName = name
}
}
class Mother : FamilyMember {
var herName = ""
init(family: Family, name: String) {
super.init(family: family)
self.herName = name
}
}
let barthelemy = Family()
let ayden = Son(family: barthelemy, name: "ayden")
let tasha = Mother(family: barthelemy, name: "tasha")
let jim = Son(family: barthelemy, name: "jim")
barthelemy.familyMembers // returns '3'

Resources