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.
Related
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.
I've answered this question myself - just leaving it here now for people with the same issue.
My code is fairly simple - A custom table with a datasource to provide data asynchronously from a web service.
In order to get the nicest user experience I would like to have the UIRefreshControl animate the process of loading whenever this controller appears, instead of just when the list has been pulled down.
Unfortunately the UIRefreshControl does not appear at all if I call my method during ViewDidAppear.
I've tried suggestions from answers these questions, but none of them seemed to work for me:
public partial class ControlCenterSelectionController : UITableViewController
{
public ControlCenterSelectionController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.RefreshControl = new UIRefreshControl();
TableView.RefreshControl.ValueChanged += RefreshControlOnValueChanged;
}
private async void RefreshControlOnValueChanged(object sender, EventArgs eventArgs)
{
await UpdateDataSourceAsync();
}
public override async void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
TableView.SetContentOffset(new CGPoint(0, -TableView.RefreshControl.Frame.Size.Height), true);
await UpdateDataSourceAsync();
}
private async Task UpdateDataSourceAsync()
{
TableView.RefreshControl.BeginRefreshing();
var ccClient = DI.Instance.Get<IControlCenterRestClient>();
var controlCenters = await ccClient.GetAllAsync();
var source = new GenericViewSource<ControlCenter>(controlCenters, item => item.Name, ControlCenterSelected);
TableView.Source = source;
TableView.RefreshControl.EndRefreshing();
}
private void ControlCenterSelected(ControlCenter controlCenter)
{
var controller = this.InstantiateController(Constants.ViewControllerIds.ControlCenter) as ControlCenterController;
controller.ControlCenter = controlCenter;
this.NavigateTo(controller);
}
}
UITableView UIRefreshControl Does Not Show Its View The First Time
UIRefreshControl on viewDidLoad
UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController
All I currently get is a screen which looks empty, loads data without indication and then updates the screen once it's done.
Can someone spot an error? I can't really find one, since this code is rather simple.
The desperation of posting it on SO + desperate coding attempts were key:
public override async void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
TableView.SetContentOffset(new CGPoint(0, -(TableView.RefreshControl.Frame.Size.Height + 20)), true);
TableView.RefreshControl.BeginRefreshing();
await UpdateDataSourceAsync();
}
private async Task UpdateDataSourceAsync()
{
var ccClient = DI.Instance.Get<IControlCenterRestClient>();
var controlCenters = await ccClient.GetAllAsync();
var source = new GenericViewSource<ControlCenter>(controlCenters, item => item.Name, ControlCenterSelected);
TableView.Source = source;
TableView.RefreshControl.EndRefreshing();
}
This change in particular made the difference:
-(TableView.RefreshControl.Frame.Size.Height + 20)
Turns out that, while in the other questions the issue was fixed by simply applying a scroll based on RefreshControl would fire the animation, in my case I had to add some extra y delta to make the animation fire.
Correction
this extra offset seems to be required only if extended edges is set to extend under top bars.
I have a situation that I cannot seem to work out Where the specific problem lies and I'm hoping for a little input.
I have a very simple view that at the moment just has a UIScrollView. In the ViewDidLoad method I register to listen for the view models PropertyChanged event like this:
public override void ViewDidLoad()
{
base.ViewDidLoad();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyCollection")
{
BuildUi();
}
}
When that event is fired it kicks off building the view since the data is now available within the view model. In the BuildUi method I have this code:
View Code
private void BuildUi()
{
_mainScrollView = new UIScrollView(View.Frame)
{
ShowsHorizontalScrollIndicator = false,
AutoresizingMask = UIViewAutoresizing.All
};
Add(_mainScrollView);
View.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
View.AddConstraints(
_mainScrollView.AtLeftOf(View),
_mainScrollView.AtTopOf(View),
_mainScrollView.WithSameWidth(View),
_mainScrollView.WithSameHeight(View));
foreach(var ctx in ViewModel.MyCollection)
{
var contextScoreControl = new ContextScoreView(new CGRect(100,100,100,100), ctx);
_mainScrollView.Add(contextScoreControl);
//var btn = ViewHelpers.CreateDefaultButton("test");
//_mainScrollView.Add(btn);
}
_mainScrollView.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
var constraints = _mainScrollView.VerticalStackPanelConstraints(new Margins(20, 10, 20, 10, 5, 5), _mainScrollView.Subviews);
_mainScrollView.AddConstraints(constraints);
}
Control Code
[Register("MyControl")]
public class MyControl : UIView
{
private MyViewModel _vm;
private UILabel _ctxTitle;
private UITextView _explanationText;
private UISlider _scoreSlider;
public MyControl(MyViewModel vm)
{
_vm = vm;
BackgroundColor = UIColor.Cyan;
}
public MyControl(IntPtr handle) : base(handle) { }
public MyControl(CGRect rect, MyViewModel vm)
{
_vm = vm;
BackgroundColor = UIColor.Cyan;
}
public MyControl(CGRect rect) : base(rect)
{
BackgroundColor = UIColor.Cyan;
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
_ctxTitle = ViewHelpers.CreateTitleLabel();
_ctxTitle.Text = _vm.Text;
_explanationText = ViewHelpers.CreateAutoExpandingReadOnlyTextView();
_explanationText.Text = _vm.ScoringHint;
_scoreSlider = ViewHelpers.CreateDefaultSlider();
_scoreSlider.Value = _vm.Score;
AddSubview(_ctxTitle);
AddSubview(_explanationText);
AddSubview(_scoreSlider);
this.AddConstraints(
_ctxTitle.AtTopOf(this, IosConstants.UIPadding),
_ctxTitle.AtLeftOf(this, IosConstants.UIPadding),
_ctxTitle.WithSameWidth(this).Minus(IosConstants.UIPadding * 2),
_explanationText.Below(_ctxTitle, IosConstants.UIPadding),
_explanationText.WithSameLeft(_ctxTitle),
_explanationText.WithSameRight(_ctxTitle),
_scoreSlider.Below(_explanationText, IosConstants.UIPadding),
_scoreSlider.WithSameLeft(_explanationText),
_scoreSlider.WithSameRight(_explanationText),
_scoreSlider.AtBottomOf(this)
);
}
}
The issue I'm seeing right now is that the Draw method is never called which means nothing appears in the view.
I've tried lots and lots of permutations of the constructors you can see in the control code. Using CGRect.Empty and as is in the code a manually generated CGRect but no matter what I try I cannot seem to get this flow of code to work.
As you can see from the code if I swap out my custom control for a simple UIButton everything works as expected so the main view code where the UIScrollView is added with constraints is working and laying itself out as expected and desired. The problem seem to lie either in the control code itself or the hierarchy of the constraints themselves. I just cannot figure out what needs to happen to make this work.
Under certain circumstances I was seeing the following error:
Unable to simultaneously satisfy constraints.
I've used this "pattern" of code many times in other views within the app and it works in those situations which is why I'm slightly confused about what is going on in this situation. The difference is the looping over a dynamic number of controls in this situation.
Without changing the current structure of the view (to say using a UITableView instead) how can I get this playing nicely and render the custom UIView controls?
I am getting started with MvvmCross in iOS.
public class MainView : MvxTabBarViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
var vm = (MainViewModel)this.ViewModel;
if (vm == null)
return;
}
}
Setting a breakpoint to the line where access the ViewModel, shows me, that ViewModel is null.
I can workaround this by calling ViewDidLoad() in the constructor. Then, ViewModel is null during the constructor call, but valid in the default ViewDidLoad call. But that looks like a workaround. can anybody help?
I'm guessing here the problem here will be specific to the way that TabBarViewController is constructed.
ViewDidLoad is a virtual method and it is called the first time the View is accessed.
In the case of TabBarViewController this happens during the iOS base View constructor - i.e. it occurs before the class itself has had its constructor called.
The only way around this I've found is to add a check against the situation in ViewDidLoad, and to make a second call to ViewDidLoad during the class constructor.
You can see this in action N-25 - https://github.com/MvvmCross/NPlus1DaysOfMvvmCross/blob/976ede3aafd3a7c6e06717ee48a9a45f08eedcd0/N-25-Tabbed/Tabbed.Touch/Views/FirstView.cs#L17
Something like:
public class MainView : MvxTabBarViewController
{
private bool _constructed;
public MainView()
{
_constructed = true;
// need this additional call to ViewDidLoad because UIkit creates the view before the C# hierarchy has been constructed
ViewDidLoad();
}
public override void ViewDidLoad()
{
if (!_constructed)
return;
base.ViewDidLoad();
var vm = (MainViewModel)this.ViewModel;
if (vm == null)
return;
}
}
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