We have discovered in our Xamarin app in iOS 13 that the disclosure button action is not firing. Our basic code looks like this in a GridCell Class. It had been firing in iOS 12. Is there another event delegate in iOS 13?
[EventDelegate("accessorySelected")]
public event EventHandler AccessorySelected
{
add
{
accessorySelected += value;
SetAccessory();
}
remove
{
accessorySelected -= value;
SetAccessory();
}
}
private event EventHandler accessorySelected;
private void SelectAccessory(object sender, EventArgs args)
{
var handler = accessorySelected;
if (handler != null)
{
handler(Pair ?? this, EventArgs.Empty);
}
else
{
... Action Code ....
}
}
Delving into this code, it was not what was expected. It actually dates from 2011 and the code to load the disclosure event was unconventional, or as another one of our developers called it during this review, "Anti-pattern".
public class GridCell : UITableViewCell, INotifyPropertyChanged
{
...
public override void LayoutSubviews()
{
base.LayoutSubviews();
// I do not like going off of the frame width. The ContentView frame width would be preferred,
// but we would first need to ensure that its value is correctly adjusted in case of a disclosure.
// This should happen automatically, but it doesn't always happen in time for PerformLayout.
// There also seem to be cases where the adjusted value is not entirely accurate.
float width = (float)Frame.Width;
if (UIDevice.CurrentDevice.CheckSystemVersion(7, 0))
{
width -= (Accessory == UITableViewCellAccessory.DetailDisclosureButton ? 67 :
Accessory == UITableViewCellAccessory.DisclosureIndicator ? 33 : 0);
}
else
{
width -= (Accessory == UITableViewCellAccessory.DetailDisclosureButton ? 33 :
Accessory == UITableViewCellAccessory.DisclosureIndicator ? 20 : 0);
}
width = Math.Max(width, 0);
var size = this.PerformLayout(new iFactr.UI.Size(width, Math.Max(MinHeight - 1, 0)), new iFactr.UI.Size(width, Math.Max(MaxHeight - 1, 0)));
ContentView.Frame = new CGRect(0, 0, (float)size.Width, (float)size.Height);
Frame = new CGRect(Frame.Location, new CGSize(Frame.Width, ContentView.Frame.Height));
// Checking subview for Disclosure - iOS 13 changed 2019 subview
var disclosure = this.GetSubview<UIControl>(c => c.Description.StartsWith("<UITableViewCellDetailDisclosureView") || c.Description.StartsWith("<UIButton"));
var dlog = this.GetSubview<UIControl>();
if (disclosure != null)
{
//Setting Disclosure button (i) event
disclosure.TouchUpInside -= SelectAccessory;
disclosure.TouchUpInside += SelectAccessory;
}
}
...
}
And the change was to this line to look for UIButton now along with UITableViewCellDetailDisclosureView
var disclosure = this.GetSubview<UIControl>(
c => c.Description.StartsWith("<UITableViewCellDetailDisclosureView")
|| c.Description.StartsWith("<UIButton")); // <-- the "fix"
Related
I'm trying to programmatically add a MouseEntered event to a custom NSButton class, and I can't seem to get it to fire. I'm writing a Mac OS application in Visual Studio for Mac, using Xamarin.Mac. I need to add the event in code because I'm creating the buttons dynamically.
Here's my ViewController where these buttons are being created. They're instantiated in the DrawHexMap method near the bottom.
public partial class MapController : NSViewController
{
public MapController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
DrawHexMap();
}
partial void GoToHex(Foundation.NSObject sender)
{
string[] coordinateStrings = ((NSButton)sender).Title.Split(',');
Hex chosenHex = HexRepo.GetHex(coordinateStrings[0], coordinateStrings[1]);
HexService.currentHex = chosenHex;
NSTabViewController tp = (NSTabViewController)ParentViewController;
tp.TabView.SelectAt(1);
}
private void DrawHexMap()
{
double height = 60;
for (int x = 0; x < 17; x++) {
for (int y = 0; y < 13; y++) {
HexButton button = new HexButton(x, y, height);
var handle = ObjCRuntime.Selector.GetHandle("GoToHex:");
button.Action = ObjCRuntime.Selector.FromHandle(handle);
button.Target = this;
View.AddSubview(button);
}
}
}
}
And here's the custom button class.
public class HexButton : NSButton
{
public NSTrackingArea _trackingArea;
public HexButton(int x, int y, double height)
{
double width = height/Math.Sqrt(3);
double doubleWidth = width * 2;
double halfHeight = height/2;
double columnNudge = decimal.Remainder(x, 2) == 0 ? 0 : halfHeight;
Title = x + "," + y;
//Bordered = false;
//ShowsBorderOnlyWhileMouseInside = true;
SetFrameSize(new CGSize(width, height));
SetFrameOrigin(new CGPoint(width + (x * doubleWidth), (y * height) + columnNudge));
_trackingArea = new NSTrackingArea(Frame, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
AddTrackingArea(_trackingArea);
}
public override void MouseEntered(NSEvent theEvent)
{
base.MouseEntered(theEvent);
Console.WriteLine("mouse enter");
}
}
So as you can see, I'm creating a tracking area for the button and adding it in the constructor. Yet I can't seem to get a MouseEntered to fire. I know the MouseEntered override in this class works, because when I call button.MouseEntered() directly from my code, the method fires.
A few other things I've tried include: Commenting out the lines that set the Action and Target in the ViewController, in case those were overriding the MouseEntered handler somehow. Setting those values inside the HexButton constructor so that the Target was the button instead of the ViewController. Putting the MouseEntered override in the ViewController instead of the button class. Creating the tracking area after the button was added as a subview to the ViewController. None of these made a difference.
Any help would be much appreciated! It's quite difficult to find documentation for Xamarin.Mac...
Thanks!
You are adding the Frame as the region tracked, but you are attaching the tracking area to the view that you are creating the region from, thus you need to track the Bounds coords.
_trackingArea = new NSTrackingArea(Bounds, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
Note: If you were tracking from the view that you are adding the buttons to, then you would need to track the "Frame" as it is the tracking region is relative to the view being tracked, not its children.
I have two Buttons on top of my screen .. Button A and Button B.. ByDefault button A will be selected and below that a view with all details related to Button A. When user clicks on Button B , the view will move to left and new view having the details related to button B will come from right with fade in and fade out animation...
Current Status- I am able to change the View on Button Click by hiding one and unhiding other.. But I am stuck with Animation..
Can You Please help me out.
You can use a parent/child-based Animation to control the slide and fade and have them properly sync'd
void RunAnim(Color disableColor, Color backgroundColor, bool disable)
{
new Animation
{
{ 0, 1, new Animation (v => view1.Opacity = v, disable ? 1 : 0, disable ? 0 : 1) },
{ 0, 1, new Animation (v => view1.TranslationX = v, disable ? 0 : -view1.Width, disable ? -view1.Width : 0) },
{ 0, 1, new Animation (v => view2.TranslationX = v, disable ? view1.Width : 0, disable ? 0 : view1.Width) },
{ 0, 1, new Animation (v => view2.Opacity = v, disable ? 0 : 1, disable ? 1 : 0) },
}.Commit(this, "viewAnim", 16, 1000, Easing.CubicInOut, (v, c) =>
{
physicalButton.IsEnabled = !disable;
physicalButton.BackgroundColor = disableColor;
networkButton.IsEnabled = disable;
networkButton.BackgroundColor = backgroundColor;
});
}
void Physical_Clicked(object sender, EventArgs e)
{
var disableColor = Color.Navy;
var backgroundColor = Color.Transparent;
var disable = true;
RunAnim(disableColor, backgroundColor, disable);
}
void Network_Clicked(object sender, EventArgs e)
{
var disableColor = Color.Transparent;
var backgroundColor = Color.Navy;
var disable = false;
RunAnim(disableColor, backgroundColor, disable);
}
Note: The gif looks janky, but the animation is smooth...
First, in your page
private readonly ScreenMetrics _metrics;
private readonly int _formsWidth;
private readonly int _formsHeight;
public MainPage()
{
InitializeComponent();
//calculate screen size, to use in animations
_metrics = DeviceDisplay.ScreenMetrics;
_formsWidth = Convert.ToInt32(_metrics.Width / _metrics.Density);
_formsHeight = Convert.ToInt32(_metrics.Height / _metrics.Density);
}
In OnAppearing, translate the elements you want outside the screen
protected override async void OnAppearing()
{
base.OnAppearing();
await Task.WhenAll(
//translate to the left side of the device, negative
YourViewLeftSide.TranslateTo(_formsWidth, 0, 0, null),
//translate to the right side of the device, negative
YourViewRightSide.TranslateTo(-_formsWidth, 0, 0, null)
);
}
On the button that animates left view
async void OnButton1Clicked(object sender, EventArgs args)
{
//animate left side
YourViewLeftSide.TranslateTo(0, 0, 400, Easing.CubicInOut),
}
On the button that animates right view
async void OnButton2Clicked(object sender, EventArgs args)
{
//animate right side
YourViewRightSide.TranslateTo(0, 0, 400, Easing.CubicInOut),
}
Now you can adapt and customize to your needs!
Finally I used CarouselView to achieve this Functionality
i need to Create a ActionBar with TabbedLayout control in xamarin forms, In xamarin Android i did that Easily but now they want in both platform IOS and Android using Xamarin forms.please share any Example or Give me suggestion for how to make the custom Controls in Xamari Froms.
Below i have attached the Image how i need Action bar with Tabbed layout.
If you are using Xamarin.Forms the Tabbed page, for Android tabbar items will in the top. For iOS, you have to create a renderer to achieve it. However, Showing Tabbar items in the top are against User guidelines of iOS.
Create custom render, override ViewDidLayoutSubviews and add the following lines code.
[assembly: ExportRenderer(typeof(ExtendedTabbedPage), typeof(ExtendedTabbedPageRenderer))]
namespace ExtendedTabbedPage.Pages
{
public class ExtendedTabbedPageRenderer : TabbedRenderer
{
private ExtendedTabbedPage Page => (ExtendedTabbedPage)Element;
public ExtendedTabbedPageRenderer()
{
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
var page = (ExtendedTabbedPage)Element;
page.CurrentPageChanged += Page_CurrentPageChanged;
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
Page_CurrentPageChanged();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
SetTabPostition();
}
void SetTabPostition()
{
if (Element == null)
return;
var element = Element as ExtendedTabbedPage;
this.TabBar.InvalidateIntrinsicContentSize();
nfloat tabSize = 74.0f;
UIInterfaceOrientation orientation = UIApplication.SharedApplication.StatusBarOrientation;
if (UIInterfaceOrientation.LandscapeLeft == orientation || UIInterfaceOrientation.LandscapeRight == orientation)
{
tabSize = 32.0f;
}
CGRect tabFrame = this.TabBar.Frame;
CGRect viewFrame = this.View.Frame;
tabFrame.Height = tabSize;
tabFrame.Y = this.View.Frame.Y;
this.TabBar.Frame = tabFrame;
this.TabBar.ContentMode = UIViewContentMode.Top;
PageController.ContainerArea = new Rectangle(0, tabFrame.Y + tabFrame.Height, viewFrame.Width, viewFrame.Height - tabFrame.Height);
this.TabBar.SetNeedsUpdateConstraints();
}
void Page_CurrentPageChanged()
{
var current = Tabbed.CurrentPage;
//if Tab is more than 5 then more will appear in iOS
if (current == null)
{
CGRect tabFrm = this.TabBar.Frame;
if (this.MoreNavigationController != null)
{
var morenavframe = this.MoreNavigationController.View.Frame;
morenavframe.Y = tabFrm.Y + tabFrm.Height;
this.MoreNavigationController.View.Frame = morenavframe;
foreach (var morecontroller in this.MoreNavigationController.ViewControllers)
{
var morecontframe = morecontroller.View.Frame;
morecontframe.Y = morenavframe.Y + morenavframe.Height;
morecontroller.View.Frame = tabFrm;
}
}
return;
}
var controller = Platform.GetRenderer(current);
if (controller == null)
return;
var frame = controller.ViewController.View.Frame;
CGRect tabFrame = this.TabBar.Frame;
frame.Y = (tabFrame.Y + tabFrame.Height);
controller.ViewController.View.Frame = frame;
this.View.Frame = frame;
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
Page_CurrentPageChanged();
}
}
}
To get a tabbed layout in Xamarin.Forms you'll usually use a TabbedPage. This will give you the tabs you show on Android. On iOS and Windows you'll get the native alternative. This means you'll get the tabs on the bottom of the screen on iOS and on Windows you'll get the tabs on top (similar, but exactly like on Android). See this illustration from the Xamarin docs:
If you want to create your own version you can implement your own version of the MultiPage class.
Now before anyone ignores this as a duplicate please read till the end. What I want to achieve is this
I've been doing some googling and looking at objective c and swift responses on stackoverflow as well. And this response StackOverFlowPost seemed to point me in the right direction. The author even told me to use ClipsToBounds to clip the subview and ensure it's within the parents bounds. Now here's my problem, if I want to show an image on the right side of the entry(Gender field), I can't because I'm clipping the subview.
For clipping, I'm setting the property IsClippedToBounds="True" in the parent stacklayout for all textboxes.
This is the code I'm using to add the bottom border
Control.BorderStyle = UITextBorderStyle.None;
var myBox = new UIView(new CGRect(0, 40, 1000, 1))
{
BackgroundColor = view.BorderColor.ToUIColor(),
};
Control.AddSubview(myBox);
This is the code I'm using to add an image at the beginning or end of an entry
private void SetImage(ExtendedEntry view)
{
if (!string.IsNullOrEmpty(view.ImageWithin))
{
UIImageView icon = new UIImageView
{
Image = UIImage.FromFile(view.ImageWithin),
Frame = new CGRect(0, -12, view.ImageWidth, view.ImageHeight),
ClipsToBounds = true
};
switch (view.ImagePos)
{
case ImagePosition.Left:
Control.LeftView.AddSubview(icon);
Control.LeftViewMode = UITextFieldViewMode.Always;
break;
case ImagePosition.Right:
Control.RightView.AddSubview(icon);
Control.RightViewMode = UITextFieldViewMode.Always;
break;
}
}
}
After analysing and debugging, I figured out that when OnElementChanged function of the Custom Renderer is called, the control is still not drawn so it doesn't have a size. So I subclassed UITextField like this
public class ExtendedUITextField : UITextField
{
public UIColor BorderColor;
public bool HasBottomBorder;
public override void Draw(CGRect rect)
{
base.Draw(rect);
if (HasBottomBorder)
{
BorderStyle = UITextBorderStyle.None;
var myBox = new UIView(new CGRect(0, 40, Frame.Size.Width, 1))
{
BackgroundColor = BorderColor
};
AddSubview(myBox);
}
}
public void InitInhertedProperties(UITextField baseClassInstance)
{
TextColor = baseClassInstance.TextColor;
}
}
And passed the hasbottomborder and bordercolor parameters like this
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
var view = e.NewElement as ExtendedEntry;
if (view != null && Control != null)
{
if (view.HasBottomBorder)
{
var native = new ExtendedUITextField
{
BorderColor = view.BorderColor.ToUIColor(),
HasBottomBorder = view.HasBottomBorder
};
native.InitInhertedProperties(Control);
SetNativeControl(native);
}
}
But after doing this, now no events fire :(
Can someone please point me in the right direction. I've already built this for Android, but iOS seems to be giving me a problem.
I figured out that when OnElementChanged function of the Custom Renderer is called, the control is still not drawn so it doesn't have a size.
In older versions of Xamarin.Forms and iOS 9, obtaining the control's size within OnElementChanged worked....
You do not need the ExtendedUITextField, to obtain the size of the control, override the Frame in your original renderer:
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
// Use the frame size now to update any of your subview/layer sizes, etc...
}
base.Frame = value;
}
}
Rather annoying issue, but it seems as though there's some default behavior on a scroll view, that when you change orientation, it auto-scrolls to top. ShouldScrollToTop is a property available on a UIScrollView but doesn't seem to be available in a XF ScrollView. But it does it anyway? Is there a way to stop?
I just did a quick test on this and only found it to be an issue on iOS. On Android the ScrollView did not scroll to the top after rotation. I did not test UWP/WinPhone. If it is iOS only, a bug should be filed against Xamarin.Forms.
In the meantime, here is a workaround for iOS. You will need to create a custom renderer [1] for your ScrollView. And the renderer code would look like this:
public class MyScrollViewRenderer : ScrollViewRenderer
{
CGPoint offset;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
UIScrollView sv = NativeView as UIScrollView;
sv.Scrolled += (sender, evt) => {
// Checking if sv.ContentOffset is not 0,0
// because the ScrollView resets the ContentOffset to 0,0 when rotation starts
// even if the ScrollView had been scrolled (I believe this is likely the cause for the bug).
// so you only want to set offset variable if the ScrollView is scrolled away from 0,0
// and I do not want to reset offset to 0,0 when the rotation starts, as it would overwrite my saved offset.
if (sv.ContentOffset.X != 0 || sv.ContentOffset.Y != 0)
offset = sv.ContentOffset;
};
// Subscribe to the oreintation changed event.
NSNotificationCenter.DefaultCenter.AddObserver(this, new Selector("handleRotation"), new NSString("UIDeviceOrientationDidChangeNotification"), null );
}
[Export("handleRotation")]
void HandleRotation()
{
if (offset.X != 0 || offset.Y != 0) {
UIScrollView sv = NativeView as UIScrollView;
// Reset the ScrollView offset from the last saved offset.
sv.ContentOffset = offset;
}
}
}
[1] https://developer.xamarin.com/guides/xamarin-forms/custom-renderer/
I have extended #jgoldberger's answer and come up with this:
[assembly: ExportRenderer(typeof(Xamarin.Forms.ScrollView), typeof(MyApp.iOS.CustomRenderers.Controls.FieldStrikeScrollViewRenderer))]
namespace MyApp.iOS.CustomRenderers.Controls
{
public class MyScrollViewRenderer : ScrollViewRenderer
{
CGPoint offset;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
UIScrollView sv = NativeView as UIScrollView;
sv.Scrolled += (sender, evt) => {
// Checking if sv.ContentOffset is not 0,0
// because the ScrollView resets the ContentOffset to 0,0 when rotation starts
// even if the ScrollView had been scrolled (I believe this is likely the cause for the bug).
// so you only want to set offset variable if the ScrollView is scrolled away from 0,0
// and I do not want to reset offset to 0,0 when the rotation starts, as it would overwrite my saved offset.
if (sv.ContentOffset.X != 0 || sv.ContentOffset.Y != 0)
offset = sv.ContentOffset;
};
}
public override void LayoutSubviews()
{
if (offset.X != 0 || offset.Y != 0)
{
UIScrollView sv = NativeView as UIScrollView;
// Reset the ScrollView offset from the last saved offset.
sv.ContentOffset = offset;
}
base.LayoutSubviews();
}
}
}
but he's right, this should really be fixed by Xamarin
Be careful with those answers in the newest Xamarin version. They will cause issues with the scroll offset behaviour when they keyboard is shown/hidden. The Scrollview might stay in an invalid offset position and can't be scrolled down by the user anymore.
I am going to add a new seperate answer. In the later version of Xamarin.Forms (3.0+)
this is a better fix:
[assembly: ExportRenderer(typeof(Xamarin.Forms.ScrollView), typeof(MyApp.iOS.CustomRenderers.Controls.ExtendedScrollViewRenderer))]
namespace FieldStrikeMove.iOS.CustomRenderers.Controls
{
public class ExtendedScrollViewRenderer : ScrollViewRenderer
{
public override CGSize ContentSize
{
get => base.ContentSize;
set
{
if (Element is ScrollView scrollView)
{
if (!scrollView.ContentSize.IsZero && value != scrollView.ContentSize.ToSizeF())
{
return;
}
}
base.ContentSize = value;
}
}
}
}
Linked to this issue