Xamarin CollectionViewCell not instantiating from storyboard - xamarin.ios

I am collaborating with a designer who styles elements on my Xamarin project storyboard. I have trouble referencing his carefully placed elements in code. All the outlets are created, but in a UICollectionViewCell the UILabels etc are not instantiated. Here is code from a simple test app to demonstrate the problem.
The Xamarin generated code-behind:
[Register ("CardCell")]
partial class CardCell
{
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UILabel txtName { get; set; }
void ReleaseDesignerOutlets ()
{
if (txtName != null) {
txtName.Dispose ();
txtName = null;
}
}
}
My attempt to set the UI properties which causes the crash:
public partial class CardCell : UICollectionViewCell
{
public CardCell (IntPtr handle) : base (handle)
{
}
public void Update(string name)
{
txtName.Text = name; // throws exception here, because textName is null
}
}
The view controller with delegate methods:
public partial class TestCollectionController : UICollectionViewController
{
static NSString cardCellId = new NSString ("cardcell");
string[] cards = new string[] {"Red", "Green", "White", "Blue", "Pink", "Yellow"};
public TestCollectionController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
CollectionView.RegisterClassForCell (typeof(CardCell), cardCellId);
}
public override nint NumberOfSections (UICollectionView collectionView)
{
return 1;
}
public override nint GetItemsCount (UICollectionView collectionView, nint section)
{
return (nint)cards.Length;
}
public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
{
CardCell cell = (CardCell)collectionView.DequeueReusableCell (cardCellId, indexPath);
var card = cards [indexPath.Row];
cell.Update(card);
return cell;
}
}
I have tried both the Xamarin iOS Designer and Xcode's Interface Builder, but it does not make a difference. Any help will be greatly appreciated.

Examples of collection views driven by a storyboard are quite scarce, but I found this one and teased through it meticulously till I discovered that it did not include the call to register a class for the cell. So, remove this line:
CollectionView.RegisterClassForCell (typeof(CardCell), cardCellId);
and suddenly everything works as expected.
I was following this recipe and other examples, which all had the call included, which is only needed when you don't use a storyboard.

Related

Show Master on UISplitViewController by Default

When I create a new "Master Detail App" with Visual Studio for Mac (v8.4.5), the default behavior of the UISplitViewController is to show the Detail page first when it appears on an iPhone in Portrait mode.
I would rather (as I think most people would rather) have the Master page show by default. In my case, the Master page is a table view that holds a list of contacts.
This question is similar to: UISplitViewController in portrait on iPhone shows detail VC instead of master but for Xamarin.iOS
Similar to the solutions suggested there, I have attempted to assign a delegate without success:
public class ContactsSplitViewControllerDelegate : UISplitViewControllerDelegate
{
public override bool EventShowViewController(UISplitViewController splitViewController, UIViewController vc, NSObject sender)
{
return true;
}
public override bool EventShowDetailViewController(UISplitViewController splitViewController, UIViewController vc, NSObject sender)
{
return true;
}
}
public partial class ContactsSplitViewController : UISplitViewController
{
public ContactsSplitViewController (IntPtr handle) : base (handle)
{
this.Delegate = new ContactsSplitViewControllerDelegate();
}
}
set the PreferredDisplayMode
public override void ViewDidLoad()
{
base.ViewDidLoad();
this.PreferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible;
}
After some experimentation, it seems that overriding CollapseSecondViewController on the delegate will work though I'm not yet convinced it's the proper solution.
using Foundation;
using System;
using UIKit;
namespace MasterDetailTest
{
public class SplitViewControllerDelegate : UISplitViewControllerDelegate
{
public override bool CollapseSecondViewController(UISplitViewController splitViewController, UIViewController secondaryViewController, UIViewController primaryViewController)
{
return true;
}
}
public partial class MainPageSplitViewController : UISplitViewController
{
public MainPageSplitViewController (IntPtr handle) : base (handle)
{
this.Delegate = new SplitViewControllerDelegate();
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// When implemented in my project, I found I needed to set this
// or the delegate would not be called.
this.SetNeedsFocusUpdate();
}
}
}

UICollectionView - Curious flow layout

I'm experiencing with the UICollectionView(Controller) for the first time. Actually it should be as simple as working with TableViews, it's not though.
Rather than showing all images in a flow (several rows) just the top row is displayed. All the other images are somewhere... scrolling is enabled but nothing happens, no bouncing, no scrolling, ...
And after an orientation change (and back) some more images are visible but they appear randomly. After every orientation change they appear at an other location.
My example should have 7 images.
My properties in IB:
First time:
After rotating (and back):
And my source code to implement the photo gallery.
using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Collections.Generic;
using Xamarin.Media;
using MonoTouch.AssetsLibrary;
using MonoTouch.CoreGraphics;
using System.Diagnostics;
using System.Linq;
using System.Drawing;
namespace B2.Device.iOS
{
public partial class TagesRapportDetailRegieBilderCollectionViewController : UICollectionViewController
{
private const string Album = "Rapport";
public TagesRapportDetailRegieBilderSource Source { get; private set; }
private TagesRapportDetailRegieBilderDelegate _delegate;
public TagesRapportDetailRegieBilderCollectionViewController (IntPtr handle) : base (handle)
{
Source = new TagesRapportDetailRegieBilderSource(this);
_delegate = new TagesRapportDetailRegieBilderDelegate(this);
// Delegate - Muss im konstruktor sein. ViewDidLoad geht nicht!
CollectionView.Delegate = _delegate;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Cell Klasse registrieren
CollectionView.RegisterClassForCell (typeof(ImageCell), new NSString("imageCell"));
// DataSource
CollectionView.Source = Source;
// Bilder laden
LoadImages();
}
private void LoadImages()
{
Source.Images.Clear();
var assetsLibrary = new ALAssetsLibrary();
assetsLibrary.Enumerate(ALAssetsGroupType.Album, GroupsEnumeration, GroupsEnumerationFailure);
}
private void GroupsEnumeration(ALAssetsGroup group, ref bool stop)
{
if (group != null && group.Name == Album)
{
//notifies the library to keep retrieving groups
stop = false;
//set here what types of assets we want,
//photos, videos or both
group.SetAssetsFilter(ALAssetsFilter.AllPhotos);
//start the asset enumeration
//with ALAssetsGroup's Enumerate method
group.Enumerate(AssetsEnumeration);
CollectionView.ReloadData();
}
}
private void AssetsEnumeration(ALAsset asset, int index, ref bool stop)
{
if (asset != null)
{
//notifies the group to keep retrieving assets
stop = false;
//use the asset here
var image = new UIImage(asset.AspectRatioThumbnail());
Source.Images.Add(image);
}
}
private void GroupsEnumerationFailure(NSError error)
{
if (error != null)
{
new UIAlertView("Zugriff verweigert", error.LocalizedDescription, null, "Schliessen", null).Show();
}
}
}
public class TagesRapportDetailRegieBilderDelegate : UICollectionViewDelegateFlowLayout
{
private TagesRapportDetailRegieBilderCollectionViewController _controller;
public TagesRapportDetailRegieBilderDelegate(TagesRapportDetailRegieBilderCollectionViewController controller)
{
_controller = controller;
}
public override System.Drawing.SizeF GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
var size = _controller.Source.Images[indexPath.Row].Size.Width > 0
? _controller.Source.Images[indexPath.Row].Size : new SizeF(100, 100);
size.Width /= 3;
size.Height /= 3;
return size;
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, int section)
{
return new UIEdgeInsets(50, 20, 50, 20);
}
}
public class TagesRapportDetailRegieBilderSource : UICollectionViewSource
{
private TagesRapportDetailRegieBilderCollectionViewController _controller;
public List<UIImage> Images { get; set; }
public TagesRapportDetailRegieBilderSource(TagesRapportDetailRegieBilderCollectionViewController controller)
{
_controller = controller;
Images = new List<UIImage>();
}
public override int NumberOfSections(UICollectionView collectionView)
{
return 1;
}
public override int GetItemsCount(UICollectionView collectionView, int section)
{
return Images.Count;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = collectionView.DequeueReusableCell(new NSString("imageCell"), indexPath) as ImageCell;
cell.Image = Images[indexPath.Row];
return cell;
}
}
public class ImageCell : UICollectionViewCell
{
UIImageView imageView;
[Export ("initWithFrame:")]
public ImageCell (System.Drawing.RectangleF frame) : base (frame)
{
imageView = new UIImageView(frame);
imageView.AutoresizingMask = ~UIViewAutoresizing.None;
ContentView.AddSubview (imageView);
}
public UIImage Image
{
set
{
imageView.Image = value;
}
}
}
}
If all you want to do is displaying the cells in an uniform grid, overriding GetSizeForItem shouldn't be necessary in the first place. Just configure the cellsize property of your flow layout either in IB or programatically during ViewDidLoad and be done with it.
There's another problem with your code:
group.Enumerate(AssetsEnumeration)
This will run asynchronously. That means:
CollectionView.ReloadData();
Will only cover a small subset of your images. It would be better to issue a ReloadData() when group == null, which indicates that the enumeration is complete.
You could also avoid ReloadData alltogether and call CollectionView.InsertItem() everytime you've added an image. This has the benefit of your items become immediately visible instead of all of them at once after everything has been enumerated - which may take some time (on the device). The downside is that you've got to be careful to not run into this.

How can I provide a header or a footer for an MvxCollectionViewController?

I'm attempting to provide a header and footer view from within an MvxCollectionViewController and I am having trouble. Normally, with a UICollectionViewController, I would override the GetViewForSupplementaryElement method like so:
public override UICollectionReusableView GetViewForSupplementaryElement (UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
{
var someHeaderOrFooterView = (HeaderOrFooterView) collectionView.DequeueReusableSupplementaryView (elementKind, elementId, indexPath);
return someHeaderOrFooterView;
}
MvxCollectionViewControllers don't seem to get delegate callbacks to the GetViewForSupplementaryElement method like a UICollectionViewController does.
Is there another method for specifying the header and footer of a CollectionView using MvxCollectionViewController?
The standard steps should work (they work here...):
Provide a size for the header in the layout
HeaderReferenceSize = new System.Drawing.SizeF(100, 100),
Implement a class for the header
public class Header : UICollectionReusableView
{
UILabel label;
public string Text
{
get { return label.Text; }
set { label.Text = value; SetNeedsDisplay(); }
}
[Export("initWithFrame:")]
public Header(System.Drawing.RectangleF frame)
: base(frame)
{
label = new UILabel() { Frame = new System.Drawing.RectangleF(0, 0, 300, 50), BackgroundColor = UIColor.Yellow };
AddSubview(label);
}
}
register that class
CollectionView.RegisterClassForSupplementaryView(typeof(Header), UICollectionElementKindSection.Header, headerId);
Implement GetViewForSupplementaryElement
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
{
var headerView = (Header)collectionView.DequeueReusableSupplementaryView(elementKind, headerId, indexPath);
headerView.Text = "Supplementary View";
return headerView;
}
Just tested these steps here and they work in my sample app (based on https://github.com/slodge/NPlus1DaysOfMvvmCross/tree/master/N-11-KittenView_Collections).
Aside> There is an open issue to provide bindable supplementary views - https://github.com/slodge/MvvmCross/issues/339 - but this issue shouldn't effect the basic collection view operation.
I was having the same problem when using MvvmCross, MvxCollectionViewController never call to
public override UICollectionReusableView
GetViewForSupplementaryElement
and I resolved the problem using the answer of "Perfem_element".
public class MyCollectionViewSource: MvxCollectionViewSource
{
public MyCollectionViewSource(UICollectionView collectionView, NSString defaultCellIdentifier) : base(collectionView, defaultCellIdentifier)
{
}
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
{
var headerView = (MyHeader)collectionView.DequeueReusableSupplementaryView(elementKind, MyHeader.Key, indexPath);
headerView.Text = "Supplementary View";
return headerView;
}
and finally in MvxViewController:
var source = new MyCollectionViewSource (CollectionView, ListadoCollectionCell.Key);
Thanks
I was having the same problem with MvvmCross. I was able to solve it by subclassing the MvxCollectionViewSource class and overriding the GetViewForSupplementaryElement method:
public class MyCollectionViewSource: MvxCollectionViewSource
{
public TimelineSource(UICollectionView collectionView, NSString defaultCellIdentifier) : base(collectionView, defaultCellIdentifier)
{
}
public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
{
var headerView = (MyHeader)collectionView.DequeueReusableSupplementaryView(elementKind, MyHeader.Key, indexPath);
headerView.Text = "Supplementary View";
return headerView;
}
}

Monotouch.Dialog: crash with EnableSearch and custom RootElement

i use Monotouch.Dialog (Xamarin.iOS Version: 6.2.0.65) to create a RadioGroup with RadioElements. I only want the 'search' functionality in the second screen (where the user selects from the long list), so i created a separate DialogViewController where i enabled the search filter (enableSearch = true).
When typing in the search box, the app crashes in the GetCell method of the RadioElement.
"Object reference not set to an instance of an object" on line 1064 of Element.cs. Do i have GC problems? I managed to simulate it in a small app... Don't pay attention some weird field members, thats me testing if i'm begin GC...
using MonoTouch.Dialog;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
namespace SearchCrash
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
// class-level declarations
UIWindow window;
public UINavigationController NavigationController { get; set; }
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
window = new UIWindow(UIScreen.MainScreen.Bounds);
this.NavigationController = new UINavigationController();
this.NavigationController.PushViewController(new FirstViewController(this.NavigationController), true);
window.RootViewController = this.NavigationController;
window.MakeKeyAndVisible();
return true;
}
}
public class FirstViewController : DialogViewController
{
private UINavigationController controller;
private LocatiesRootElement locRootElement;
private RadioGroup radioGroup;
public FirstViewController(UINavigationController c) : base(new RootElement("Test"), true)
{
this.controller = c;
}
public override void ViewDidLoad()
{
Section section = new Section();
radioGroup = new RadioGroup(0);
locRootElement = new LocatiesRootElement("test", radioGroup, this.controller);
section.Add(locRootElement);
Root.Add(section);
}
}
public class LocatiesRootElement : RootElement
{
private UINavigationController navigationController;
private LocatiesViewController locatiesViewController;
public LocatiesRootElement(string selectedLocatie, RadioGroup group, UINavigationController controller) : base("Locatie", group)
{
this.navigationController = controller;
this.locatiesViewController = new LocatiesViewController(this);
}
public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath path)
{
this.navigationController.PushViewController(this.locatiesViewController, true);
}
}
public class LocatiesViewController : DialogViewController
{
public LocatiesViewController(LocatiesRootElement root) : base(root, true)
{
this.EnableSearch = true;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
Section sectionList = new Section();
RadioElement radioElement1 = new RadioElement("item 1");
RadioElement radioElement2 = new RadioElement("item 2");
sectionList.Add(radioElement1);
sectionList.Add(radioElement2);
Root.Add(sectionList);
}
}
}
Try enabling the search after you instantiate the DialogViewController.
Like this :
public LocatiesRootElement(string selectedLocatie, RadioGroup group, UINavigationController controller) : base("Locatie", group)
{
this.navigationController = controller;
this.locatiesViewController = new LocatiesViewController(this);
this.locatiesViewController.EnableSearch = true;
}
Hopefully that will work for you.
The bug report here includes a workaround for the root cause of the issue you're experiencing, but also talks about how filtering will then cause a usability issue of marking the nth element as selected even after a filter has been applied.
https://github.com/migueldeicaza/MonoTouch.Dialog/issues/203
If you don't want to update the core MTD code, you could use that same technique by putting it in your own UIBarSearchDelegate. Unfortunately, the default SearchDelegate class is internal, so you'll need to add all of the code in your delegate. I was able to do this and get it working without changing the MTD source:
public override void LoadView()
{
base.LoadView();
((UISearchBar)TableView.TableHeaderView).Delegate = new MySearchBarDelegate(this);
}
And then you use this instead of the base method:
public override void TextChanged (UISearchBar searchBar, string searchText)
{
container.PerformFilter (searchText ?? "");
foreach (var s in container.Root)
s.Parent = container.Root;
}

UITableView navigation in monotouch

I'm using mono develop 3.1.1 to build an IOS application. I'm receiving an object reference error (see >>>) from my reference to the navigation controller that I've not declared properly.
My question is what's the best way to declare and instantiate the controller so I'm able to show another view from the point the table cell is selected.
Can someone help me with the correct syntax please?
public class TableHelper : UITableViewSource {
protected string[] tableItems;
protected string cellIdentifier = "TableCell";
public TableHelper (string[] items)
{
tableItems = items;
}
public override int RowsInSection (UITableView tableview, int section)
{
return tableItems.Length;
}
public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
switch (tableItems[indexPath.Row])
{
case "one":
var DetailViewController = new SupportContactsDetailsScreen ();
UINavigationController controller = new UINavigationController();
// Pass the selected object to the new view controller.
>>>controller.NavigationController.PushViewController(DetailViewController, true);
break;
default:
//Console.WriteLine("Default case");
break;
}
}
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell (cellIdentifier);
if (cell == null)
cell = new UITableViewCell (UITableViewCellStyle.Default, cellIdentifier);
cell.TextLabel.Text = tableItems[indexPath.Row];
return cell;
}
}
The way that I usually go about doing this is to keep a reference to the main UIViewController (the view controller that holds the UITableView) for that particular set of views and access that Navigation Controller through the NavigationController property. (Another technique taken by Xamarin in the code sample linked below is to pass the UINavigationController in directly.)
So I would alter your class by adding:
UIViewController parentViewController;
public TableHelper(string[] items, UIViewController vc)
{
tableItems = items;
parentViewController vc;
}
public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
switch (tableItems[indexPath.Row])
{
case "one":
var DetailViewController = new SupportContactsDetailsScreen ();
UINavigationController controller = new UINavigationController();
// Pass the selected object to the new view controller.
parentViewController.NavigationController.PushViewController(DetailViewController, true);
break;
default:
//Console.WriteLine("Default case");
break;
}
}
Xamarin has a document on their docs site as well as some code on their Github that talks about this further. Another important note is what the type of view controller is (regular UIViewController, UITableViewController, etc.).

Resources