Custom UITableViewHeaderFooterView with MvvmCross and XIB? - ios

In the app I'm working on there's a need for custom UITableView section headers and footers. For this I'd like to create a custom control that works with binding and our logic.
For that I've created a XIB and added a backing class that looks like the following:
public partial class HeaderFooterView : MvxTableViewHeaderFooterView
{
public static readonly NSString Key = new NSString("HeaderFooterView");
public static readonly UINib Nib = UINib.FromName("HeaderFooterView", NSBundle.MainBundle);
public HeaderFooterView(IntPtr handle) : base(handle)
{
}
public override void AwakeFromNib()
{
base.AwakeFromNib();
//var binding = this.CreateBindingSet<HeaderFooterView, TableViewSection>();
//binding.Apply();
}
}
The MvxTableViewHeaderFooterView is actually a pretty simple class, combining the stock UITableViewHeaderFooterView with IMvxBindable. Nothing fancy.
However for some reason, even though I register it properly within the TableViewSource constructor:
tableView.RegisterNibForHeaderFooterViewReuse(HeaderFooterView.Nib, HeaderFooterView.Key);
And do the proper way of returning the Header itself only:
public override UIView GetViewForHeader(UITableView tableView, nint section)
{
return tableView.DequeueReusableHeaderFooterView(HeaderFooterView.Key);
}
The app dies with the following error:
2017-07-12 16:56:40.517 MyAppiOS[3833:58706] *** Assertion failure in -[UITableView _dequeueReusableViewOfType:withIdentifier:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.7.47/UITableView.m:6696
2017-07-12 16:56:40.528 MyAppiOS[3833:58706] WARNING: GoogleAnalytics 3.17 void GAIUncaughtExceptionHandler(NSException *) (GAIUncaughtExceptionHandler.m:48): Uncaught exception: invalid nib registered for identifier (HeaderFooterView) - nib must contain exactly one top level object which must be a UITableViewHeaderFooterView instance
My NIB actually contains a single root object, the root view itself, that is set to the HeaderFooterView class (which derives from MvxTableViewHeaderFooterView which in turn derives from UITableViewHeaderFooterView). Yet it claims there's no UITableViewHeaderFooterView instance.
Why isn't it working as it's supposed to?

It's because return tableView.DequeueReusableHeaderFooterView(HeaderFooterView.Key); can return null if there are no HeaderFooterViews to reuse. In that case you have to create your own:
var view = tableView.DequeueReusableHeaderFooterView(HeaderFooterView.Key);
if (view == null){
//instantiate the nib here and set view
}
return view;

I would suggest structuring the backing class as follows:
public partial class HeaderFooterView : MvxTableViewHeaderFooterView
{
public static readonly NSString Key = new NSString("HeaderFooterView");
public static readonly UINib Nib = UINib.FromName("HeaderFooterView", NSBundle.MainBundle);
static HeaderFooterView()
{
//Adding this alone should allow your tableview to properly instantiate the view.
Nib = UINib.FromName("HeaderFooterView", NSBundle.MainBundle);
}
public static HeaderFooterView Create()
{
// However you can add this static method and create and return the view yourself.
var arr = NSBundle.MainBundle.LoadNib(nameof(HeaderFooterView ), null, null);
var v = Runtime.GetNSObject<HeaderFooterView >(arr.ValueAt(0));
return v;
}
public HeaderFooterView(IntPtr handle) : base(handle)
{
// Note: this .ctor should not contain any initialization logic.
}
public override void AwakeFromNib()
{
base.AwakeFromNib();
}
}
Adding the static constructor by itself should be enough to allow your table view to properly instantiate the Nib. However if you still end up having problems like that you can use the static method 'Create' to instantiate the nib yourself as so:
public override UIView GetViewForHeader(UITableView tableView, nint section)
{
HeaderFooterView theView = HeaderFooterView.Create()
return theView;
}
Try those suggestions, one or both should work for you.

Okay, found the issue.
While my initial XIB was correct, for some reason the root object's type was erased, and Interface Builder refused to accept mine.
However using VS2017 for Mac, I was able to set the proper root view class, and now everything works fine.

Related

UITableViewDelegate disables itemselected

I am trying to make a simple change to a tableview for IOS. As my project is using Xamarin forms I use a custom renderer.
The changes I want to make is to close the keyboard is the table is scrolled, simple as that.
When I make the following changes it will work for my new scrolled override event but stops item (cell) selected from triggering (some of my cells navigate away from the page) and must be tapped.
public class CustomTableRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
var tableView = Control as UITableView;
TableDelegate tableDelegate;
tableDelegate = new TableDelegate();
tableView.Delegate = tableDelegate;
}
}
public class TableDelegate : UITableViewDelegate
{
#region Constructors
public TableDelegate()
{
}
public TableDelegate(IntPtr handle) : base(handle)
{
}
public TableDelegate(NSObjectFlag t) : base(t)
{
}
#endregion
#region Override Methods
public override void Scrolled(UIScrollView scrollView)
{
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
//base.Scrolled(scrollView);
}
#endregion
}
I have tried changing the UITableViewDelegate to UITableViewController but seems to cause more problems.
Adding in the override for all other methods calling base also does not seem to help. Not sure what I am missing.
Any idea why this is caused?
Note: If more information is needed please comment to let me know. Same with if the question is not clear. Comments would help.

Xamarin.iOS: Create a custom button with an additional object

In my Xamarin.iOS application there are many buttons with the same scheme but different from to standart UIButton. I created one class for the buttons because most of the features are same but for example the textcolor or the backgroundcolor are different.
So how can I put an extra information about any button in the storyboard?
And how can I react on it in code?
You can make your custom element visible in for the designer with the DesignTimeVisible and the Register Attribute like
[Register ("CustomButton"), DesignTimeVisible (true)]
public class CustomButton: UIButton {
[Export ("CustomProperty "), Browsable (true)]
public int CustomProperty {get; set;}
public CustomButton(IntPtr handle) : base (handle) { }
public CustomButton()
{
// Called when created from code.
Initialize ();
}
public override void AwakeFromNib ()
{
// Called when loaded from xib or storyboard.
Initialize ();
}
void Initialize ()
{
// Common initialization code here.
CustomProperty = 0xB00B5;
}
}
For all properties that you want to set in the Designer you just hae to add Export and Browsable (true). In the Initializeyou can set all the vlaues of the common properties.
It will appear in the Toolbox under Custom Components. You might have to rebuild.
And the Custom Property can be modified in the Properties pane
More info: https://developer.xamarin.com/guides/ios/user_interface/designer/ios_designable_controls_overview/

Outlets in Xamarin custom TableViewCell are null

I am trying to use a custom UITableViewCell. I am able to get the cell to instantiate, so that it is not null. However, all of the UILabel outlets in the cell are null. Thus, I am getting a null pointer exception.
This is similar to iOS custom TableViewCell class subviews return null and Label in custom cell is null when populating UITableViewController. Nevertheless, neither solutions have solved my problem. They suggest to make sure the identifier matches, which it does. They also tell me to register the cells for reuse, which I also do.
I have made sure that my identifier in the Xib file of the cell matches what I am using.
In my Source class:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
tableView.RegisterClassForCellReuse(typeof(BoardListCell), new NSString(BoardListCell.Key));
var cell = (BoardListCell) tableView.DequeueReusableCell(BoardListCell.Key);
if (cell == null)
{
var nib = UINib.FromName(BoardListCell.Key, NSBundle.MainBundle);
cell = (BoardListCell)nib.Instantiate (null, null) [0];
}
cell.setData("top", "not top"); //Sample data
return cell;
}
In my BoardListCell class:
public partial class BoardListCell : UITableViewCell
{
public static readonly UINib Nib = UINib.FromName ("BoardListCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("BoardListCell");
public BoardListCell() : base()
{
}
public BoardListCell (IntPtr handle) : base (handle)
{
}
public void setData(String top, String bottom)
{
exp.Text = top;
playTime.Text = bottom;
}
}
When I debug, the cell is being created. However, its outlets are null. If I instantiate them in the constructor, I don't get the error, but this obviously defeats the purpose of making prototypes in xcode as it wipes out the layout, etc.
I was having the same issue described above. The solution for me was to remove the call to TableView.RegisterClassForCellReuse(typeof(YourCustomeTableCell), "YourTableCellName");
If you created the Cell in the Xcode designer, there is no need to register the Cell.
Hope this helps anyone still having this issue.

Unable to cast UIKit.UIView to type UIKit.UILabel?

In my project I am trying to get a reference to a label within a view. The view has 2 items in it both of which are UILabels.
var headerViews = NSBundle.MainBundle.LoadNib(nibName,this,null);
// Only 1 view in the XIB as shown in the image below.
UIView headerView = headerViews.GetItem<UIView>(0);
UILabel nameLabel = (UILabel)headerView.ViewWithTag(1);
This is throwing the runtime error that it cannot cast the view to a label. Why not? This cast is valid in Objective-C and Swift.
This is what headerView looks like:
I would suggest to work with a more safe approach.
Create a UIView subclass, e.g. MyDialogView, in your XIB designer set this class to the main View and add Outlets for each control.
Then add a method that you can pass a model class or values you want to update the View with.
In your code you can work using your MyDialogView class and invoke the method to update the view with data.
This way you don't need to work with casting and tags.
[EDIT] Code Example.
Create a class and derive from UIView, add the Register attribute to make it visible in XCode. It should be like the below.
[Register("MyCustomView")]
public partial class MyCustomView : UIView
{
CGSize _intrinsicContentSize;
public UIView View { get; set; }
public string Text {
get { return myLabel.Text; }
set { myLabel.Text = value; }
}
public MyCustomView ()
{
SetupView ();
}
public MyCustomView (IntPtr handle) : base (handle)
{
SetupView ();
}
private UIView LoadViewFromXib()
{
NSBundle bundle = NSBundle.FromClass (this.Class);
UINib nib = UINib.FromName ("MyCustomView", bundle);
return nib.Instantiate (this, null)[0] as UIView;
}
private void SetupView()
{
this.View = LoadViewFromXib ();
AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
this.Bounds = this.View.Bounds;
_intrinsicContentSize = this.Bounds.Size;
TranslatesAutoresizingMaskIntoConstraints = false;
AddSubview (this.View);
}
public override CGSize IntrinsicContentSize {
get {
return _intrinsicContentSize;
}
}
}
In your XIB file don't set the View's class to MyCustomView but the File's Owner to MyCustomView.
In your ViewController somewhere, e.g. ViewDidLoad add the following.
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Perform any additional setup after loading the view, typically from a nib.
MyCustomView customView = new MyCustomView ();
customView.Text = "Test Label Text";
Add (customView);
}
Run and you should see the update label in your custom view.

iOS custom TableViewCell class subviews return null

I use Xamarin iOS designer to design custom TableViewCell class for my TableView.
But all cell subviews properties (outlets) return null except cell itself.
My custom cell class:
partial class VehiclesTableCell : UITableViewCell
{
public VehiclesTableCell(IntPtr handle) : base(handle) { }
public void UpdateCell(string licensePlate) {
licensePlateLabel.Text = licensePlate; //hit the null reference exception for licensePlateLabel
}
}
Generated partial class:
[Register ("VehiclesTableCell")]
partial class VehiclesTableCell
{
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UILabel licensePlateLabel { get; set; }
void ReleaseDesignerOutlets ()
{
if (licensePlateLabel != null) {
licensePlateLabel.Dispose ();
licensePlateLabel = null;
}
}
}
And GetCell of my Table Source class:
public class VehiclesTableSource : UITableViewSource
{
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {
// Dequeueing the reuse cell
var cell = (VehiclesTableCell)tableView.DequeueReusableCell(new NSString(typeof(VehiclesTableCell).Name));
// Set cell properties
cell.UpdateCell(LicensePlate);
// Return the cell with populated data
return cell;
}
}
As we see genereted code has an outlet for licensePlate so why is its property null?
Is storyboard not supposed to instantiate all of its subviews automatically?
At least its happening in all other situations.
I was having the same problem with my custom TableViewCell. I found out the issue was with TableView.RegisterClassForCellReuse (typeof(MyCell), MyCellId). I had to change it to TableView.RegisterNibForCellReuse(UINib.FromName("MyCell", NSBundle.MainBundle), MyCellId) in order to get my .xib loaded.
I came across this issue myself. This is what I did to resolve it:
Instantiate the components of the subview in the constructor of the custom cell. In your case, something like:
public VehiclesTableCell(IntPtr handle) : base(handle) {
licensePlateLabel = new UILabel();
}
Override the method LayoutSubviews:
public override void LayoutSubviews () {
base.LayoutSubviews ();
licensePlateLabel.Frame = new RectangleF(63, 5, 33, 33); // Your layout here
}
The info on this guide got me this far, but ideally there would be a way to accomplish this without instantiating and laying out the subviews components manually as they're already defined in the IOS designer. Hopefully someone can chime in with a better way to do this...
EDIT: I came across this answer which explained why I wasn't able to bind the custom table cell I created in the designer. TLDR: Ensure that Identity -> Class and Table View Cell -> Identifier are both set to the custom table view cell class name in the inspector window.
I had this same issue, and as far as I have found out, if you are using storyboard you can just enter your custom class name in the properties of your table cell in the iOS designer and it should work. Calling RegisterClassForCellReuse() is what causes the null sub views issue. Once I removed that method call it seemed to work

Resources