SwiftUI make LazyGrid Item twice the size of other Items - swiftui

I want to display a grid where one item takes twice the amount of space of the others.
Similar to this HTML Grid
Basically I want the last item Text("div4") to take up the space of two items. I do not want to use .fixed since it should take exactly twice the space of the other grids no matter the screen size.
If there is a way to archive this without LazyGrid, that is also fine with me ;)
Here is my current code:
let cols = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
var body : some View {
LazyVGrid(columns: cols){
Text("div1")
Text("div2")
Text("div3")
Text("div4")
}
}

Since Swift does not support this. A solution would be to use a library like: ExyteGrid!
var body : some View {
Grid(tracks: [.fr(1),.fr(1),.fr(1),.fr(2)]){
Text("div1")
Text("div2")
Text("div3")
Text("div4")
}
}
https://cocoapods.org/pods/ExyteGrid

Related

format TextField as you type numbers and grouping

I'm having trouble doing something that should be straightforward simple on a good and clean framework, but with SwiftUI is all a big problem.
I simply need a user to be able to type in a TextField, showing only the numPad, and have the numbers automatically grouped as in 3,000,000 as the user types. Also, if the user taps anywhere in the field to correct a mistaken number, the focus should go to the correct tapped place and the user should be able to erase, keep the focus on the same position, and type the new corrected number. Something as simple as that.
Well, there are multiple problems I'm experiencing:
If using a formatter, the formatted number will only display after the field loses focus (i.e. it will show as 3000000 and then after losing focus, 3,000,000). That shouldn't be a great deal of a problem, but if I focus the field again to add more numbers, it ends up typing something like 3,000,000123 and after losing focus it will not recognize anything after the last 0, so it erases 123. Also, the numPad doesn't have a comma to type it manually.
That's the following case:
#State var number: Int = 0
let numberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.generatesDecimalNumbers = true
numberFormatter.maximumFractionDigits = 0
numberFormatter.groupingSize = 3
return numberFormatter
}()
TextField("1000", value: $number, formatter: numberFormatter)
.keyboardType(.numberPad)
If using Combine, the moment a user tries to correct a digit in the middle of the number, the focus goes to the end of the field, screwing the user experience and making people angry.
For this, I'm using this code:
#State var text: String = ""
TextField("1000", text: $text)
.keyboardType: .numberPad)
.onReceive(Just(text)) { _ in formatNumber() }
func formatNumber() {
if (text.count == 0) {
return
}
let filtered = text.filter { "0123456789".contains($0) }
let groups: Int = filtered.count / 3
let remaining: Int = filtered.count % 3
var res: String = ""
if (remaining > 0) {
res += filtered.substring(with: 0 ..< remaining)
res += ","
}
for i in 0 ..< groups {
res += filtered.substring(with: (remaining + i*3) ..< (remaining + (i+1)*3))
res += ","
}
res.removeLast(1)
text = res
}
Is there any clean solution to something as basic as this?
I use normally the .onChange Modifier directly attached to the TextField, like
TextField("Value:", text: $depotActionWertString)
.onChange(of: depotActionWertString) { _ in
let filtered = depotActionWertString.filter {"-0123456789.".contains($0)}
depotActionWertString = filtered
} // .onChange
It is more compact for the checks of the user input and you may combine it with the .onRecive statement for formatting.
For the first case in simple Versions for displaying text I use often the combination Text with String(format:...., like
Text("\(String(format: "%.2f", self._model.wrappedValue.progress * 100)) %")
I think its more a style question as to be right ore wrong....
I don't know if this is the most optimal solution, but one possible solution using Combine without losing the cursor position is to check the length of the published value.
Since the cursor moves to the end of the text because you updated your textfield with the subscriber, you can solve this problem by preventing your subscriber from updating the published value when you edit your text in the middle. Since when you edit, you always start by deleting, you can then just check in your subscriber whether the length of new value is shorter than the length of your old value. If it is, then just return.
I tried the following implementation and it works:
.sink { value in
let filtered = value.filter { "0123456789".contains($0) }
//check is just an Int saving the length of the old value
if filtered.count > 3 && filtered.count >= self.check {
//withCommas is just a formatter
self.text = String(Int(filtered)!.withCommas())
}
self.check = filtered.count
}
Note there are also some problems with this implementation:
since it will not update when you delete, you may end up with something like this: 123,456,789 and the user deleted "456" to become 123,,789. It won't autocorrect itself. I haven't figured out a way to make the commas autocorrect while keeping the cursor in the same place either.
the above implementation force unwrapped optional, which might cause a crash.

ChartJS v3.X - Limit the string size of label on canvas, without changing tooltip hover string

So, I had some old charts code using a very old Chart.js version 2.x but now due to the need of some functions only found on versions 3.x I'm updating a lot of code from the old charts on the website because a lot of the syntax changed between versions.
I just finished updating all charts and they are working nicely using plugins and etc, except for one.
On this type: 'bar' chart I have some very long strings on the 'X' axis, so I need to trim them to a max of 12 characters, but the tooltips need to show the whole string. The old code used to be this one and worked like a charm, right before rendering the chart I would set the updateScaleDefaults:
// Limit the size of label for 12 characters, tooltip on hover not changed
Chart.scaleService.updateScaleDefaults('category', {
ticks: {
callback: function(tick) {
var characterLimit = 12;
if ( tick.length >= characterLimit) {
return tick.slice(0, tick.length).substring(0, characterLimit -1).trim() + '...';;
}
return tick;
}
}
});
So right after that I would call the new Chart instance and would render it on a canvas. But now this code doesn't work anymore on v3.x, and I can't make an alternative version of this work.
As the migration guide for V3.x states on the documentation ,
Chart.scaleService was replaced with Chart.registry. Scale defaults are now in Chart.defaults.scales[type]
So I tried changing the first line of the code above to this and many other variations, trying to reuse the old code just changing the call to the object:
Chart.defaults.scales['category']
Nothing I tried worked.
I then tried creating a plugin with an beforeDraw, acessing the chart.scales.x.ticks and trying to make an arrow function on a map call, but I got a $context is a object error like this:
const pluginLimitTitle = {
beforeDraw: (chart) => {
console.log(chart.scales.x.ticks);
chart.scales.x.ticks = chart.scales.x.ticks.map(function (tick) {
var characterLimit = 12;
if (tick['label'].length >= characterLimit) {
return tick['label'].slice(0, tick['label'].length).substring(0, characterLimit - 1).trim() + '...';
;
}
return tick;
});
}
I also tried putting the ticks callback inside the options on the chart creation on options: scales: x: ticks but it did not work either way.
Can someone help me make this on v3.x?
Spent the whole day trying many things and don't look I'm getting closer to make it work.
After wasting many hours I found a "Tip" highlight on the documentation that should be in the examples, and not just badly highlighted on the "Labeling Axes" page.
When you do a callback for ticks on the chart options settings, you get 3 params to call:
function(value, index, ticks)
I tried in many ways to change the last one because it is the array of all ticks with their labels, and it is the only one where the label string appears so I was trying to modify it.
You'd think at first that the "value" parameter is the one to be changed, but it returns the exactly same integer value as the "index" parameter on each callback iteration, so I thought the only one the I could manipulate to change the string was the "ticks" array, but I was completely wrong.
You actually need to call a special function called getLabelForValue().
The working code ended up like this:
const configTotal = {
type: 'bar',
data: dataTotal,
options: {
scales: {
y: {
beginAtZero: true
},
x: {
ticks: {
callback: function(value, index, ticks_array) {
let characterLimit = 12;
let label = this.getLabelForValue(value);
if ( label.length >= characterLimit) {
return label.slice(0, label.length).substring(0, characterLimit -1).trim() + '...';
}
return label;
}
}
}
}
},
};
I hope this helps anyone having the same problem as me.
Maybe all the time I wasted is all on me for not reading every single line with more patience, but in my opinion, this documentation lacks a lot more example codes for accessing properties and callback parameters and it's object values, the way it is just showing the type of object and returns of each class method call, makes it very confusing for non-experienced on chart.js users.

Can I put an array into another array in swiftUI?

I have an array of cards and want to put them into an array of decks. Users can make their own decks and creates cards within. I've finished making the flashcard part of the app and you can make one deck only.
NavigationView {
Section {
List(decks) { deck in
NavigationButton(destination: ContentView (deck: testDeck))
DecksRow(deck: testDeck)
}
}
} .navigationBarTitle(Text: "Rows")
}
I am new to swiftUI.

SwiftUI Make a list that shows only a certain amount of rows

I have recently started messing around with SwiftUI and so far it is great. I have however ecnountered a problem that I can't seem to find an answer to.
I am trying to create this design:
As you can see here is a tableview (a list) which rows have a smaller tableview (a list) inside them. The task was pretty simple while using IB, but with swiftUI I can't seem to find a way to limit the number of rows to only the amount I need or to manually set the lists height based on the number of rows (keep in mind those rows can also be variable in height).
My code so far only consist of creating the List and adding 2 test rows:
List {
OrderItemRow()
OrderItemRow()
}
But as you can see SwiftUI automatically stretches the List to fill all the available space. The effect:
If you know the height of your rows, this can be achieved like this:
List(self.arrayOfAllItemsInOrder(currentOrder)) { item in
OrderItemRow(currentOrder: order, item:item)
}
.environment(\.defaultMinListRowHeight, rowHeight)
.frame(height: self.arrayOfAllItemsInOrder.reduce(0) { i, _ in i + rowHeight })
This will calculate the full size of the list based on rowHeight and limit the frame's height.
Note that if the size of your row goes beyond rowHeight, the entire calculation will be messed up and you will probably be seeing half (or missing entire) cells.
I would suggest a List using a ForEach to show your orders and a nested ForEach to show items in an order. The key is to manage the Data being supplied to each ForEach to the number of items desired and also to construct appropriate Row Views for OrderHeaderRow, OrderItemRow, etc.
struct OrdersView: View {
var body: some View {
List {
Section(header: Text("Current Order")) {
OrderHeaderRow(order: currentOrder)
ForEach(self.arrayOfAllItemsInOrder(currentOrder)) { item in
OrderItemRow(currentOrder: order, item:item)
}
OrderTotalRow(order: order)
ActionButtonsRow(order:order)
}
Section(header: Text("Previous Orders")) {
ForEach(self.arrayOfAllPreviousOrders) { order in
OrderHeaderRow(order: order)
ForEach(self.arrayOfAllItemsInOrder(order)) { item in
OrderItemRow(order: order, item:item)
}
OrderTotalRow(order: order)
ActionButtonsRow(order:order)
}
}
}
}
}

Regex replacement within a partial text element in google apps script

Below is Google's example for emboldening a partial text element. This is used for selected text that is not an entire element (e.g. just a sentence fragment is selected). I need to replace the action of emboldening with that of performing a regex replacement. The replaceText() function does not accept integers to tell it where to start and end (unlike the setBold() function).
This is a very similar (unanswered) question, but I believe Google Scripts has changed some of the commands, so I thought it worth posting again.
The Google example is:
// Bold all selected text.
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var elements = selection.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
// Only modify elements that can be edited as text; skip images and other non-text elements.
if (element.getElement().editAsText) {
var text = element.getElement().editAsText();
// Bold the selected part of the element, or the full element if it's completely selected.
if (element.isPartial()) {
text.setBold(element.getStartOffset(), element.getEndOffsetInclusive(), true);
} else {
text.setBold(true);
}
}
}
}
The regex implementation of replaceText method does not support lookarounds or capture groups, which makes it impossible to perform such partial replacement with it.
A workaround is to use a JavaScript replacement to prepare new substring, and then put it in place with replaceText. This preserves the formatting of text, even if, say, italicized part overlaps the selection where replacement happens. A potential drawback is that if the element contains a piece of text identical to the selection, replacement will happen there as well.
var text = element.getElement().editAsText();
if (element.isPartial()) {
var start = element.getStartOffset();
var finish = element.getEndOffsetInclusive() + 1;
var oldText = text.getText().slice(start, finish);
var newText = oldText.replace(/a/g, "b");
text.replaceText(oldText, newText);
}