Nativescript Chat UI iOS Issues - ios

I am creating a chat UI interface in Nativescript and I almost have everything working, but I am having a few issues in iOS that I cannot figure out. Everything in Android works correctly.
Problem 1:
When I focus on the TextView and the keyboard opens up I cannot no longer scroll to the top of the chat. All the content seems to shift up (even without using IQKeyboardManager interestly enough).
Problem 2:
When I start typing in the TextView it instantly shifts to the bottom of the screen, hidden behind the keyboard and I cannot see what I am typing.
Here is a demo project I created on Playground that shows the problem. Playground Demo Project
Below is a GIF showing both of the issues.
Any help is appreciated. Thanks!

Problem 1:
This is because IQKeyboardManager hosts a ScrollView on top of the ViewController (Page) and RadListView has its own scrollable region. The solution would be adjusting the insets when keyboard is active.
The code below takes keyboard height and caches it in application object when its first shown.
import * as application from "#nativescript/core/application";
let observer = application.ios.addNotificationObserver(
UIKeyboardDidShowNotification,
notification => {
application.ios.removeNotificationObserver(
observer,
UIKeyboardDidShowNotification
);
(application as any)._keyboardHeight = notification.userInfo.valueForKey(
UIKeyboardFrameBeginUserInfoKey
).CGRectValue.size.height;
}
);
When text field is focused adjust the insets of list view
textFieldFocus() {
console.log("Focus on TextField");
(this
.radList as any)._originalContentInset = this.radList.ios.contentInset;
if ((application as any)._keyboardHeight) {
this.radList.ios.contentInset = UIEdgeInsetsMake(
(application as any)._keyboardHeight,
0,
0,
0
);
} else {
setTimeout(() => this.textFieldFocus(), 100);
}
}
Problem 2:
This is because the text view layouts itself when text is updated. Preventing that may keep the scroll position intact. The override below checks if the field is focused and ignores layout calls.
import { TextView } from "#nativescript/core/ui/text-view";
TextView.prototype.requestLayout = function() {
if (
!arguments[0] &&
this.nativeViewProtected &&
this.nativeViewProtected.isFirstResponder
) {
this.nativeViewProtected.setNeedsLayout();
IQKeyboardManager.sharedManager().reloadLayoutIfNeeded();
} else {
View.prototype.requestLayout.call(this);
}
};
Updated Playground

The above solution did not work for me. This might seems counter-intuitive, but actually the solution is to add a wrapping ScrollView around the main layout (in my case, the direct and only child of my Page).
Take the following example:
<GridLayout rows="*, auto" width="100%" height="100%">
<ScrollView row="0">
<StackLayout orientation="vertical">
<StackLayout *ngFor="let m of messages" [horizontalAlignment]="m.incoming ? 'left' : 'right'">
<Label class="message" [text]="m.message" [class.incoming]="m.incoming" textWrap="true"></Label>
</StackLayout>
</StackLayout>
</ScrollView>
<GridLayout row="1" columns="*, auto" class="input-layout">
<TextView class="text-view" [(ngModel)]="message"></TextView>
<Button column="1" text="Send"></Button>
</GridLayout>
</GridLayout>
It will become:
<ScrollView>
<GridLayout rows="*, auto" width="100%" height="100%">
<ScrollView row="0">
<StackLayout orientation="vertical">
<StackLayout *ngFor="let m of messages" [horizontalAlignment]="m.incoming ? 'left' : 'right'">
<Label class="message" [text]="m.message" [class.incoming]="m.incoming" textWrap="true"></Label>
</StackLayout>
</StackLayout>
</ScrollView>
<GridLayout row="1" columns="*, auto" class="input-layout">
<TextView class="text-view" [(ngModel)]="message"></TextView>
<Button column="1" text="Send"></Button>
</GridLayout>
</GridLayout>
</ScrollView>
So yes, you will have a nested ScrollView, ListView or RadListView 🤷
Kudos to #beeman and his nativescript-chat-ui sample: https://github.com/beeman/nativescript-chat-ui/blob/master/src/app/messages/containers/message-detail/message-detail.component.html

Related

Xamarin forms iOS UIView Renderer intermittent OnElementChanged in some cases

I am doing video matrix views (1x1-2x2-3x3 views) using FlowListView. For iOS app, UIView renderer is used to integrate with a third party video SDK.
Here is the FlowListView.
<flv:FlowListView x:Name="VideoFlowList" FlowColumnCount="{Binding ColumnCount}" RowHeight="{Binding RowHeight, Mode=TwoWay}"
SeparatorVisibility="None" HasUnevenRows="false" BackgroundColor="Transparent"
FlowColumnMinWidth="80" FlowTotalRecords="{Binding TotalRecords, Mode=TwoWay}" FlowItemsSource="{Binding Items}">
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Grid x:Name="VideoGrid" Padding="2" BackgroundColor="{Binding SelectedBorderColour, Mode=TwoWay}" RowSpacing="1" ColumnSpacing="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<video:CameraView x:Name="MyCameraView" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="Black" />
<Image x:Name="AddIcon" Source="panel_add.png" Grid.Row="0" Grid.Column="0" IsVisible="{Binding CameraNotAssigned}"
HorizontalOptions="Center" VerticalOptions="Center" Aspect="AspectFit" BackgroundColor="Transparent" WidthRequest="50" HeightRequest="50">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding BindingContext.AddCameraCommand, Source={x:Reference BrowseItemsPage}}"
CommandParameter="{x:Reference VideoGrid}" NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
<Label x:Name="Label" HorizontalOptions="Fill" HorizontalTextAlignment="Center" VerticalOptions="End"
BackgroundColor="Silver" Opacity="0.5" Text="{Binding CameraName, Mode=TwoWay}" TextColor="Black"/>
</Grid>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
On the startup of the matrix views, it displays 4 views (ColumnCount = 2, ToTalRecords = 4) in the view model) and works everytime. Switching to any other views works too. But ocassionaly when switching to 4 views from a single view, there are only 2 new Elements instead of 4 in overrided OnElementChanged. How to ensure to get 4 new Elements everytime?
Please note that this issue doesn't happen when switching to 4 views from 9 views.
Here is the code of the ViewRenderer.
public class VideoRender : ViewRenderer<CameraView, UIVideoView>
{
private UIVideoView _videoView;
protected override void OnElementChanged(ElementChangedEventArgs<CameraView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//Unsubscribe
_videoView.Tapped -= OnVideoViewTapped;
Log.Debug($"OldElement CameraIndex {e.OldElement.CameraIndex}, PlayWndHandle {e.OldElement.PlayWndHandle}");
}
if (e.NewElement != null)
{
//Control is TNativeView, CameraView
if (Control == null)
{
_videoView = new UIVideoView(Element);
SetNativeControl(_videoView);
}
//Element.PlayWndHandle = Handle;
if (_videoView.Subviews.Length > 0)
{
Element.PlayWndHandle = _videoView.Subviews[0].Handle;
}
App.PlayWndHandles.Add(Element.PlayWndHandle);
Log.Debug($"NewElement CameraIndex {Element.CameraIndex}, PlayWndHandle {Element.PlayWndHandle}");
Log.Debug($"App.PlayWndHandles[{App.PlayWndHandles.Count - 1}]: {Element.PlayWndHandle}");
// Subscribe
_videoView.Tapped += OnVideoViewTapped;
}
}
}
I added a subview in the UIView UIVideoView and play the video in the subview. Is the issue related to the way I use the subview?
void Initialize()
{
//place the video in subview
var subView = new UIView();
subView.UserInteractionEnabled = true;
AddSubview(subView);
if (Subviews.Length > 0)
{
Subviews[0].Frame = new CoreGraphics.CGRect(0, 0, 100, 100);
}
}
It appears that using 4 separate lists of play window handles (one for each matrix size) can overcome the handle management issue. A single view is specially handled by clearing the handles in the list because the handle will always be removed and re-created.

How to use GridLayout codrrect in platform IOS Nativescript

I have this code. In android work very good, in IOS I can't change map.
<GridLayout class="page">
<GridLayout columns="*" rows="*,auto,auto">
<StackLayout col='0' row='0'>
<MapView class="map" (mapReady)="onMapReady1($event)" (markerSelect)="onMarkerSelect($event)"
(cameraChanged)="onCameraChanged($event)"></MapView>
</StackLayout>
<StackLayout class="input-field" col='0' row='0' style="margin-top: 10">
<GridLayout columns="*,*" rows="*">
<StackLayout col='0' row='0'>
<DropDown itemsTextAlignment="center" itemsPadding="10" title="Comune di:" hint="Comune di:"
[items]="citiess" (selectedIndexChanged)="onchangecity($event)" (opened)="onopen()"
(closed)="onclose()"
style="background-color: rgba(255, 255, 255, 0.918); border-color:slategray; padding: 3%;
font-size: 18; color: rgb(70, 70, 70); size: 14px; margin-left: 6%; margin-top: 0.7%; border-style: solid; text-align: center;">
</DropDown>
</StackLayout>
</GridLayout>
</StackLayout>
</GridLayout>
</GridLayout>
When I put DropDown outside GridLayout, I can change map, but I want to put over map a button that call dropdown.
Have you any idea please?
Your StackLayout is overlaying on the whole of MapView. In iOS all topmost views block events from propagating below. So only the topmost StackLayout after MapView receives your click/drag event even though you are not explicitly listening for it.
You can just have the DropDown element with exact width and height it needs to occupy and align it by using margins. This will prevent anything else overlaying on the MapView.
On a sidenote: If you just want to overlay other elements while keeping the bottom element interactable you can add isUserInteractionEnabled="false" to the overlay element.

Keyboard auto-hides on iOS when typing in a ListView's TextField element

I'm working on a NativeScript app and I've setup a ListView element to hold dynamically generated TextField elements. When I start typing in one of the ListView's Textfield elements on iOS, it closes the keyboard immediately.
This behavior only happens on iOS and not on the Android version of the application. I found an forum post about this type of issue happening with iOS in a NativeScript Vue application but I'm unsure how to use what they did to help me with Angular.
https://github.com/nativescript-vue/nativescript-vue/issues/334
component.html
<StackLayout formArrayName="ingredients" row="5" colSpan="3">
<ListView height="100" [items]="form.get('ingredients').value">
<ng-template
let-ingredient="item"
let-i="index"
>
<StackLayout>
<TextField
hint="Add Ingredient"
width="300"
[formControlName]="i"
></TextField>
</StackLayout>
</ng-template>
</ListView>
</StackLayout>
component.ts
ngOnInit() {
this.form = new FormGroup({
name: new FormControl(null, {updateOn: 'blur'}),
description: new FormControl(null, {updateOn: 'blur'}),
ingredients: new FormArray([new FormControl(null)]),
steps: new FormArray([new FormControl(null)]),
imageNames: new FormArray([])
});
}
onAddIngredient() {
const control = new FormControl(null);
(<FormArray>this.form.get('ingredients')).push(control);
}
Playground: Playground version of app

How to remove default tooltip from menu item in collapsable state with sidebar?

How to remove default tooltip from a menu item in a collapsible state with sidebar? It seems like the same question asked in the ant design GitHub also, but no response. Code and screenshot below.
<Menu.Item key="profile">
<Link to={`${baseConfig.page.profile.url}`}>
<span className="isoMenuHolder" style={{color:'#ffffff'}}>
<i className="icon-admin" />
<span className="nav-text">
Settings
</span>
</span>
</Link>
</Menu.Item>
Take it out with css
.ant-tooltip {
display: none;
}
Not sure if my workaround helps you or not but I have a collapse/expand button within the Menu and it displays the color of the button as the tooltip. So i removed the Menu.Item and the tooltip disappeared
<Menu
mode="inline"
theme="light"
inlineCollapsed={this.state.menuCollapsed}
className="CollapseButtonMenu"
>
<Button type="primary" onClick={this.toggleCollapsed} style={{ marginBottom: 16 }}>
<Icon type={this.state.menuCollapsed ? 'menu-unfold' : 'menu-fold'} />
</Button>
</Menu>
since the new changes on antd (> 4.23.3), the <Menu.Item /> component was deprecated, with the new API they provide a way to make the tooltip disappear is to define the title attribute as null or as an empty string on the items list like:
const items = [
{
key: "0",
title: "", //this makes the tooltip disappear
label: "Any Label"
}
];

How do you prevent the bottom area in a React Native SafeAreaView from overlapping over the content?

I'm implementing a <SafeAreaView> on my React Native app. Most of my screens are in a ScrollView. When I add the <SafeAreaView>, it obstructs the content. While I want this bottom area to be "safe", I'd like the user to be able to see the content behind it, otherwise, the space is wasted.
How do I implement a "transparent" safe area?
Simplified example:
class ExampleScreen extends Component {
render() {
return (
<SafeAreaView>
<Scrollview>
<Text>Example</Text>
<Text>Example</Text>
<Text>Example</Text>
(etc)
</Scrollview>
</SafeAreaView>
);
}
}
Output:
Desired Output:
In most you do not want to have your ScrollView/FlatList have as a descendant of a SafeAreaView. Instead you only want to wrap your Header and TabBar into a SafeAreaView. Some examples:
Instead of this (WRONG EXAMPLE)
<SafeAreaView>
<Header />
<ScrollView>
<Content />
</ScrollView>
</SafeAreaView>
you only wrap the header
<View>
<SafeAreaView>
<Header />
</SafeAreaView>
<ScrollView>
<Content />
</ScrollView>
</View>
Also even if you do not really have a Header, you only want to avoid drawing behind the status bar, you can use the SafeAreaView as padding.
<View>
<SafeAreaView /> // <- Now anything after this gonna be rendered after the safe zone
<Content />
</View>
Maybe this late answer but you can easily use
class ExampleScreen extends Component {
render() {
return (
<SafeAreaView edges={['right', 'left', 'top']} >
<Scrollview>
<Text>Example</Text>
<Text>Example</Text>
<Text>Example</Text>
(etc)
</Scrollview>
</SafeAreaView>
);
}
}
You could try react-navigation's SafeAreaView. Just set it's forceInset prop to { bottom: 'never' } and you'll see it behaves as your expectation.
Example: https://github.com/react-navigation/react-navigation/blob/master/examples/SafeAreaExample/App.js
<>
<SafeAreaView style={{ paddingTop: Platform.OS === 'android' ? 20 : 0 }} />
<ScrollView>
<View>
<Text>Text</Text>
</View>
</ScrollView>
</>
<ScrollView contentInsetAdjustmentBehavior="never">
...
</ScrollView>
Use this in scrollView
In ScrollView, contentInsetAdjustmentBehavior controls inserting this safe area padding. Just set it as automatic
<KeyboardAwareScrollView
contentInsetAdjustmentBehavior="automatic"
{...otherProps}
/>
Docs:
/**
* This property specifies how the safe area insets are used to modify the content area of the scroll view.
* The default value of this property must be 'automatic'. But the default value is 'never' until RN#0.51.
*/
contentInsetAdjustmentBehavior?: 'automatic' | 'scrollableAxes' | 'never' | 'always';
Well, even on 0.64 I had to set it manually. Whatever is up with that.

Resources