SwiftUI - LazyVStack inside ScrollView gives unexpected behaviour - swiftui

I'm building a chat app using SwiftUI and I'm having difficulties getting a LazyVStack to work inside a ScrollView. Here are my questions:
Question 1
Where I have .id(message.id), why is this id required for scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id, anchor: .bottom) to work, when the ForEach is assigning the same id using id: \.1.id? If I comment out the .id(message.id) line, scrollView.scrollTo doesn't work.
Question 2:
a) If I comment out the code .id(message.id),on an iPhone 6S, I get 11 messages loaded in view, however, the print statement print(message.messageContent) prints out 22 messages. Why does this happen?
b) Why are the print(message.messageContent) print statements not printed in order? I thought a LazyVStack would render in vertical order?
c) As I scroll down to reveal the 12th message, I get "Message 23" printed to the console instead of "Message 12". Why is this?
import SwiftUI
struct ChatMessageModel: Identifiable {
var id: String
var messageContent: String
}
class ChatMessageViewModel: ObservableObject {
#Published var arrayOfMessages: [ChatMessageModel] = [ChatMessageModel(id: "1", messageContent: "Message 1"),
ChatMessageModel(id: "2", messageContent: "Message 2"),
ChatMessageModel(id: "3", messageContent: "Message 3"),
ChatMessageModel(id: "4", messageContent: "Message 4"),
ChatMessageModel(id: "5", messageContent: "Message 5"),
ChatMessageModel(id: "6", messageContent: "Message 6"),
ChatMessageModel(id: "7", messageContent: "Message 7"),
ChatMessageModel(id: "8", messageContent: "Message 8"),
ChatMessageModel(id: "9", messageContent: "Message 9"),
ChatMessageModel(id: "10", messageContent: "Message 10"),
ChatMessageModel(id: "11", messageContent: "Message 11"),
ChatMessageModel(id: "12", messageContent: "Message 12"),
ChatMessageModel(id: "13", messageContent: "Message 13"),
ChatMessageModel(id: "14", messageContent: "Message 14"),
ChatMessageModel(id: "15", messageContent: "Message 15"),
ChatMessageModel(id: "16", messageContent: "Message 16"),
ChatMessageModel(id: "17", messageContent: "Message 17"),
ChatMessageModel(id: "18", messageContent: "Message 18"),
ChatMessageModel(id: "19", messageContent: "Message 19"),
ChatMessageModel(id: "20", messageContent: "Message 20"),
ChatMessageModel(id: "21", messageContent: "Message 21"),
ChatMessageModel(id: "22", messageContent: "Message 22"),
ChatMessageModel(id: "23", messageContent: "Message 23"),
ChatMessageModel(id: "24", messageContent: "Message 24"),
ChatMessageModel(id: "25", messageContent: "Message 25")]
}
struct ChatMessagesView: View {
#StateObject var chatMessageViewModel = ChatMessageViewModel()
var body: some View {
ScrollViewReader { scrollView in
ScrollView (.vertical, showsIndicators: true) {
LazyVStack (spacing: 0) {
ForEach(Array(zip(chatMessageViewModel.arrayOfMessages.indices, chatMessageViewModel.arrayOfMessages)), id: \.1.id) { (index, message) in
Text("Index is \(index) with message: \(message.messageContent)")
.padding(.vertical, 20)
.id(message.id)
.onAppear {
print(message.messageContent)
}
}
}
}
.onAppear {
scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id, anchor: .bottom)
}
}
.environmentObject(chatMessageViewModel)
}
}

What you are seeing is the intented way the LazyVStack works, it will create and destroy views as needed LazyVStack, that's why the onAppear is called when you are scrolling, about the ordering as you can see the order is fine swiftui handles the views creation in its own cycle.
As I scroll down to reveal the 12th message, I get "Message 23" printed to the console instead of "Message 12". Why is this?
That's because the bounce animation when you reach the bottom, the upper message will be recreated.
struct ChatMessagesView: View {
#StateObject var chatMessageViewModel = ChatMessageViewModel()
var body: some View {
ScrollViewReader { scrollView in
ScrollView (.vertical, showsIndicators: true) {
LazyVStack (spacing: 0) {
ForEach(chatMessageViewModel.arrayOfMessages.indices, id: \.self) { index in
let message = chatMessageViewModel.arrayOfMessages[index]
Text("Index is \(index) with message: \(message.messageContent)")
.padding(.vertical, 20)
.id(message.id)
.onAppear {
print(message.messageContent)
}
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation{
scrollView.scrollTo(chatMessageViewModel.arrayOfMessages.last?.id)
}
}
}
}
.environmentObject(chatMessageViewModel)
}
}
I would add some animation and the timeout so the scroll looks smoother.

Related

Combining of views using coordinates and zstack

i have an issue with ordering, combining and positioning of my solid shapes and text
the task is simple enough, but while im completely newbie in swfitui i cannot do it for my self
task: place solid shapes with zstack relative by their parrent using coordinates (including text of each shape) and apply isometric/perpective modifier
i will be glad for explanations and hints
this is what i have done for now
works without modifiers
and no with isometric modifier
struct ContentView: View {
#ObservedObject var datas = ReadData()
//MagnificationState
//DragState
//#GestureState
//#State viewMagnificationState
//#GestureState
//#State viewDragState
//magnificationScale
//translationOffset
//#State popoverState
//#State selectedShape
var body: some View {
//magnificationGesture
//dragGesture
//magnificationGesture.simultaneously
GeometryReader { geometry in
ZStack(alignment: .topLeading) {
MainShape(showingPopover: $popoverState,
setCurrentShape: $selectedShape,
alldatas: datas.pokedex)
}
.scaleEffect(magnificationScale)
.offset(translationOffset)
//.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
//.gesture(both)
//popover
}
.edgesIgnoringSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainShape: View {
let alldatas: [PokedexElement]
var body: some View {
ForEach(alldatas, id: \.id) { shape in
ZStack(alignment: .topLeading) {
Text(shape.metaProperties.name)
.font(.system(size: 20))
.zIndex(2)
.offset(x: shape.metaProperties.offset.x, y: shape.metaProperties.offset.y)
Rectangle()
.cornerRadius(shape.id == 1 ? 55 : 10)
.foregroundColor(chooseColor(shape.metaProperties.color))
.offset(x: shape.metaProperties.offset.x, y: shape.metaProperties.offset.y)
.frame(
width: shape.metaProperties.size.width,
height: shape.metaProperties.size.height
)
.isometric()
.zIndex(1)
.onTapGesture {
self.showingPopover = true
self.setCurrentShape = shape.id
}
}
}
}
}
data:
[
{
"id": 1,
"parentId": 0,
"baseProperties": {
"name": "",
"descr": "",
"contacts": ""
},
"metaProperties": {
"name": "root",
"status": true,
"type": "plain",
"size": {
"width": 350,
"height": 350
},
"offset": {
"x": 0,
"y": 0
},
"color": "blue"
}
},
{
"id": 2,
"parentId": 1,
"baseProperties": {
"name": "object 1",
"descr": "",
"contacts": ""
},
"metaProperties": {
"name": "child 1",
"status": true,
"type": "imobject",
"size": {
"width": 50,
"height": 50
},
"offset": {
"x": 50,
"y": 50
},
"color": "red"
}
},
{
"id": 3,
"parentId": 1,
"baseProperties": {
"name": "object 1",
"descr": "",
"contacts": ""
},
"metaProperties": {
"name": "child 2",
"status": true,
"type": "imobject",
"size": {
"width": 100,
"height": 50
},
"offset": {
"x": 100,
"y": 50
},
"color": "green"
}
}
]
and modifiers…
extension View {
func chooseColor(_ name: String) -> Color {
switch name {
case "red":
return Color.red
case "blue":
return Color.blue
case "brown":
return Color.brown
case "green":
return Color.green
case "yellow":
return Color.yellow
case "white":
return Color.white
default :
return Color.black
}
}
func perspective() -> some View {
self
.rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))
}
func isometric() -> some View {
self
.rotationEffect(Angle.init(degrees: 45))
.scaleEffect(y: 0.5)
}
}
task was easy, instead of using regular solid shapes with "magic methods from black box" do it all manually
convert decart to iso and place it whatever you want together with text or anything else and offset will be fine

How to Expand ( Increase the size of Container ) the particular container on clicked in flutter

Container Size depends on the widget's child length
I want all things in the mobile screen height not the inner child scroll or outer
I expected output like below -
what I am getting -
import 'package:flutter/material.dart';
class ExpandableContainer extends StatefulWidget {
const ExpandableContainer({Key? key}) : super(key: key);
#override
State<ExpandableContainer> createState() => _ExpandableContainerState();
}
class _ExpandableContainerState extends State<ExpandableContainer> {
int selectedIndex = -1;
List dataList = [
{
"title": "Title 1",
"items": [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
],
},
{
"title": "Title 2",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
],
},
{
"title": "Title 3",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
],
},
{
"title": "Title 4",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10",
],
},
{
"title": "Title 5",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
],
},
];
#override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: SizedBox(
height: size.height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
dataList.length,
(index) => GestureDetector(
onTap: () => setState(() {
selectedIndex = index;
}),
child: Container(
height: size.height / dataList.length,
width: size.width,
padding: const EdgeInsets.fromLTRB(32.0, 16.0, 0.0, 16.0),
// alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(width: 8.0, color: Colors.grey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
dataList[index]["title"].toUpperCase(),
style: const TextStyle(
fontSize: 38.0,
color: Colors.black,
fontWeight: FontWeight.w900,
letterSpacing: -2.0,
),
),
for (var item in dataList[index]["items"])
Text(
selectedIndex == index ? item : "",
style: TextStyle(
fontSize: 20.0,
color: Colors.black,
height: selectedIndex == index ? 1.5 : 0.0,
),
),
],
),
),
),
),
),
),
);
}
}
List<String> category = [
"HealthCare",
"Food & Drink",
"Beauty",
"Baby & kids",
"Homeware"
];
List<Color> color = [
Colors.lightBlueAccent,
Colors.green.shade700,
Colors.pinkAccent.shade100,
Colors.blueAccent,
Colors.amber,
];
List<Color> textColor = [
Colors.blueAccent.shade700,
Colors.amber,
Colors.pinkAccent.shade700,
Colors.pinkAccent.shade100,
Colors.white,
];
int? selectedIndex;
List<dynamic> dataList = [
{
"title": "Title 1",
"items": [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
],
},
{
"title": "Title 2",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
],
},
{
"title": "Title 3",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
],
},
{
"title": "Title 4",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10",
],
},
{
"title": "Title 5",
"items": [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
],
},
];
Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
...category.map(
(index) => Expanded(
flex: selectedIndex==category.indexOf(index) ? (dataList[category.indexOf(index)]["items"].length/3).ceil(): 1,
child: GestureDetector(
onTap: () {
print(dataList[category.indexOf(index)]["items"].toString());
print("tap");
setState(() {
selectedIndex = category.indexOf(index);
});
},
child: Container(
color: color[category.indexOf(index)],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
index.toUpperCase(),
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: textColor[category.indexOf(index)]),
),
selectedIndex == category.indexOf(index)
? Column(
children: [
...dataList[category.indexOf(index)]
["items"]
.map(
(e) => Text(e),
),
// ListTile(title:Text(dataList[category.indexOf(index)]["items"].toString()))
],
)
: SizedBox(),
],
),
),
),
),
),
),
],
),
);
Please use ExpansionTile for expansion
ExpansionTile(
iconColor:Colors.blue,
textColor:Colors.black,
title: Text('Privacy Policy',style: TextStyle(fontSize: 16)),
children: [
ListTile(
title: Text('item1',textAlign: justifyText,),
),
ListTile(
title: Text('item2',textAlign: justifyText,),
),
ListTile(
title: Text('item3',textAlign: justifyText,),
),
ListTile(
title: Text('item4',textAlign: justifyText,),
),
],
),
Try this you can test this on dartpad its so now the one is hindering the size was on the container so if you select a certain index make the container height to null so that it will wrap the results inside.
return Scaffold(
backgroundColor: Colors.white,
body: SizedBox(
child: ListView.builder(
shrinkWrap: true,
itemCount: dataList.length,
itemBuilder:(context,index)
=>GestureDetector(
onTap:(){
setState((){
selectedIndex = index;
print( dataList[index]['items'].map((e)=> e).toList());
});
},
child: Container(
height: selectedIndex == index ? null
: size.height / dataList.length,
width: size.width,
padding: const EdgeInsets.fromLTRB(32.0, 16.0, 0.0, 16.0),
// alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(width: 8.0, color: Colors.grey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
dataList[index]["title"].toUpperCase(),
style: const TextStyle(
fontSize: 38.0,
color: Colors.black,
fontWeight: FontWeight.w900,
letterSpacing: -2.0,
),
),
...dataList[index]['items'].map((e)=>
selectedIndex == index ?
Text(
e,
style: TextStyle(
fontSize: 20.0,
color: Colors.black,
height: selectedIndex == index ? 1.5 : 0.0,
) ,
) : Container(),).toList()
],
),
),
),
),
),
);
Solution No 1
To achieve this you can use ExpansionTile class,
This widget is typically used with ListView to create an "expand / collapse" list entry. When used with scrolling widgets like ListView, a unique PageStorageKey must be specified to enable the ExpansionTile to save and restore its expanded state when it is scrolled in and out of view.
this is the link about ExpansionTile
https://api.flutter.dev/flutter/material/ExpansionTile-class.html#:~:text=ExpansionTile%20class%20Null%20safety,expand%20%2F%20collapse%22%20list%20entry.
and this is the demo example link how to do it
https://rrtutors.com/tutorials/Expandable-Listview-Flutter-ExpansionTile
Solution No 2
Use ExpandableGroup
To achieve this you can use ExpandableGroup
this is the link about ExpandableGroup
https://pub.dev/packages/expandable_group
if you don't want to use plugin , just inspect this plugin code , it has simple logic
You can use expansion tile but if the existing expansion tile does not provide the view required for your project, you can create your own custom expansion tile widget.
Like
app_expansion_tile
You should use expansion tile, as far as i have saw in your image you don't want it to be scrollablle, what i suggest is to make a limitation on the expansiontile class or the child, then set restriction limit to with and height and make it be responsive.

Incorrect sequence of Carousel elements in Facebook Messenger Graph API

I am trying to send horizontally scrollable carousel message on Facebook workplace platform using the generic template as described in the messenger platform documentation(Ref: https://developers.facebook.com/docs/messenger-platform/reference/template/generic)
The template that I am using looking something like this:
[
{
"text": "Hi!!!!"
},
{
"attachment": {
"type": "template",
"payload": {
"template_type": "generic",
"elements": [
{
"title": "Some Title 1",
"image_url": "some image url",
"buttons": [
{
"title": "button_title1",
"type": "postback",
"payload": "Title 1"
}
]
},
{
"title": "Some Title 2",
"image_url": "some image url",
"buttons": [
{
"title": "button_title2",
"type": "postback",
"payload": "Title 2"
}
]
},{
"title": "Some Title 3",
"image_url": "some image url",
"buttons": [
{
"title": "button_title3",
"type": "postback",
"payload": "Title 3"
}
]
},
]
}
}
}
]
Everything worked as I expected but, from the past 2 weeks or so, the order of carousel elements(cards in my case) isn't what I am expecting(i.e., Title1, Title2, Title3). The elements in the carousel are shown in a different order every time I use it. I just want to know if anyone else has the same issue or it has something to do with my code?
The code that I have used to send the message in the end is:
return new Promise((resolve, reject) => {
//(async ref:https://www.npmjs.com/package/async)
async.eachSeries(
//facebookMessages is the message template that I am using(posted above)
facebookMessages,
(msg, callback) => {
//sendFBSenderAction sends the message to FB using api(https://graph.facebook.com/v2.6/me/messages?access_token=<PAGE_ACCESS_TOKEN>)
this.sendFBSenderAction(sender, 'typing_on')
.then(() => this.sleep(this.messagesDelay))
.then(() => {
facebookMessages.attachment ? this.sendFbAttachment(msg) : this.sendFBMessage(sender, msg);
facebookMessages.attachment = false;
})
.then(() => callback())
.catch(callback);
},
err => {
if (err) {
console.error(err);
reject(err);
} else {
console.log('Messages sent');
resolve();
}
}
);
});
Looks like there’s already a current bug report for this - https://developers.facebook.com/support/bugs/260229951577494/
Subscribe to that, to be updated when the status of that bug report changes.

Infragistics delete row using string primaryKey

I have an Infragistics grid and am unable to delete a row when the primaryKey of the grid is of dataType string.
I can not set my primaryKey as dataType number because it is of this format: "KIT_001". Is there any clever way of using a delete button and being able to delete rows with this kind of data? Perhaps a way to set an incremented ID and use that for the delete button?
var dataSource = [
{"ProductID": "KIT_001", "Name": "Kit 1", "ProductNumber": "P4857"},
{"ProductID": "KIT_002", "Name": "Kit 2", "ProductNumber": "P4567"},
{"ProductID": "KIT_003", "Name": "Kit 3", "ProductNumber": "P4447"}
]
$(function () {
$("#grid").igGrid({
autoGenerateColumns: false,
width: "100%",
height: "500px",
columns: [
{ headerText: "Product ID", key: "ProductID", dataType: "string", width: "10%" },
{ headerText: "Product Name", key: "Name", dataType: "string", width: "30%" },
{ headerText: "Product Number", key: "ProductNumber", dataType: "string", width: "25%" },
{ headerText: "", key: "Delete", dataType: "string", width: "10%", unbound: true,
template: "<input type='button' onclick='deleteRow(${ProductID})' value='Delete' class='delete-button'/>"},
],
primaryKey: "ProductID",
dataSource: dataSource,
features: [
{
name: "Updating",
enableAddRow: false,
editMode: "row",
enableDeleteRow: false,
}
]
});
});
function deleteRow(rowId) { console.log('rowId ',rowId)
var grid = $("#grid").data("igGrid");
grid.dataSource.deleteRow(rowId);
grid.commit();
}
Enclose the key parameter in quotes in your template:
template: "<input type='button' onclick='deleteRow(\"${ProductID}\")' value='Delete' class='delete-button'/>"

Failed to implement computed property in emberjs

My fixture data contains multiple array.Out of this multiple array cart_items contains some product data.
I am trying to calculate total no of products available in cart data (based on length of cart_items items) but i am not able to calculate no of items are present in cart_items.
In router i have selected application fixture as model for current route,as follow :
Astcart.IndexRoute = Ember.Route.extend({
model: function() {
return Astcart.Application.find();
}
});
Computed property code :
Astcart.IndexController = Ember.ArrayController.extend({
tot_cart_prd: function() {
return this.get("model.cart_items").get('length');
}.property("#each.isLoaded")
});
And my fixture data is :
Astcart.Application.adapter = Ember.FixtureAdapter.create();
Astcart.Application.FIXTURES = [
{
"logo_url": "img/logo.jpg",
"logged_in": {
"logged": true,
"username": "sachin",
"account_id": "4214"
},
"category_list": [
{
"id": "1",
"name": "Mobiles & Accessories"
},
{
"id": "2",
"name": "Computers & Software"
},
{
"id": "3",
"name": "Fashion"
},
{
"id": "4",
"name": "Electronics"
},
{
"id": "5",
"name": "Watches & Jewelry"
},
{
"id": "6",
"name": "Health & Beauty"
},
{
"id": "7",
"name": "Games"
},
{
"id": "8",
"name": "Books & Entertainment"
},
{
"id": "9",
"name": "Gaming"
},
{
"id": "10",
"name": "Shoes & Bags"
}
],
"cart_items": [
{
"id": "1",
"name": "Samsung Galaxy Tab 2",
"qty": "1",
"price": "1245.12",
"subtotal": "7842.23"
},
{
"id": "2",
"name": "Samsung Galaxy Tab 2",
"qty": "1",
"price": "1245.12",
"subtotal": "7842.23"
},
{
"id": "3",
"name": "Samsung Galaxy Tab 2",
"qty": "1",
"price": "1245.12",
"subtotal": "7842.23"
}
]
}
];
I have posted my code here(JSFiddle).
Can any one tell me why this.get("model.cart_items") is returning null?
Because your IndexController receive an array of Astcart.Application, from the route. You need to iterate in each application and get the length of each category list .
Your computed property need to be the following:
Astcart.IndexController = Ember.ArrayController.extend({
tot_cart_prd: function() {
var result = this.get('model').map(function(application) {
return application.get('category_list.length');
});
return result;
}.property('model.#each.category_list.length')
});
Here's an updated fiddle http://jsfiddle.net/marciojunior/PZZym/
I just looked at this and your core issue has something to do with the relationship setup between Application and Cart_items. The reason that this.get("model.cart_items").get('length') is failing is that this.get("model.cart_items") returns null. If you can get your relationship working you should be on the right track. I don't know anything about EmberModel, so I can't be of much help there.