Shiny UITabBar … but colorized.

I looked around for some way of stylizing a UITabBar, since we had decided we wanted the text labels and shiny “selected” state of the tab bar versus a normal toolbar. It seems Apple has this locked down pretty good, but I did find a few examples (links after code)

Since I already had a toolbar with the gradient I wanted, I figured the easiest way to pull this off would be to take a snapshot of the toolbar, strip out a thin section and fill it in. So, here’s the code:

UIView v = new UIView (new RectangleF(0f,0f,NavBar.Frame.Width, NavBar.Frame.Height));
v.BackgroundColor = UIColor.FromPatternImage(UIImage.FromBundle("images/iPhone/tabbar.png"));
NavBar.InsertSubview (v, 0);

Now, as it turns out – Apple does something weird. When I used the image as a background, it got a LOT lighter, like there’s some sort of brightness filter. Easy enough to counteract by decreasing brightness in Gimp – but it seems I lost some of the saturation. More tweaking, and it’s a little better.

Also, note that the Toolbar is 43 or 44 pixels tall (in standard resolution) and the Tab bar is 49px, so I probably lost some quality when I enlarged my original image.

Sure, it’s not perfect, but it’s an easy way to get most of the tab bar functionality while not having to settle for a the standard black background. You could actually put a really nice texture background on there and have a very sharp looking tab bar.

End result:

Links: (these are in obj-c, as I didn’t see any monotouch results)

http://duivesteyn.net/2010/01/16/iphone-custom-tabbar-background-image/

http://stackoverflow.com/questions/675433/custom-colors-in-uitabbar

Yes/No options for UIAlertView

Be sure to keep a reference to the UIAlertView somewhere else, otherwise you’ll get a nullref exception when they click on it.

alert = new UIAlertView ("Storage Room", "Would you like to search the storage room for an item?", null, "Yes", new[] { "No" });
			alert.Dismissed += delegate(object alertSender, UIButtonEventArgs args) {
				if (args.ButtonIndex == 0) {
					var card = game.DrawCard ();
					if (card != null) {
						ShowItemScreen (card.Item, true);
					}
				}
			};
			alert.Show ();

MBProgressHUD for MonoTouch

Okay, so there’s a built-in Progress HUD that looks cool. But it’s not documented. So I found one at:
http://github.com/jdg/MBProgressHUD and decided to port it to monotouch.

SCREENSHOTS at the original Obj-C version in GitHub: (I feel bad posting them directly on my blog)
http://github.com/jdg/MBProgressHUD

MonoTouch version of the code at github HERE (thanks to @detroitPro):
http://github.com/detroitpro/MBProgressHUD-MonoTouch

To use: (I recommend keeping a reference to the object elsewhere in your code, so you can dispose of it properly)

					var hud = new MBProgressHUD (this.View.Window);
					hud.Mode = MBProgressHUDMode.Indeterminate;
					hud.TitleText = "Loading";
					hud.DetailText = "We'll be back shortly...";
					this.View.Window.AddSubview(hud);
				        hud.Show (true);

That’s all! It’s pretty snazzy, IMO. But the credit all goes to someone who isn’t me. Anyhow, here’s the source. Note that it’s designed to sort of attach to a background process, and that part isn’t quite done. However, the Show(animated) and Hide(animated) work just fine. That’s enough for me.

Source:

using System;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
using System.Drawing;
using MonoTouch.CoreGraphics;
namespace Utility
{
	public enum MBProgressHUDMode
	{
		/** Progress is shown using an UIActivityIndicatorView. This is the default. */
		Indeterminate,
		/** Progress is shown using a MBRoundProgressView. */
		Determinate
	}

	public class MBProgressHUD : UIView
	{
//		private UIView _indicator;
//		private UIView Indicator {
//			get {
//				if (_indicator == null) {
//					if (Mode == MBProgressHUDMode.Determinate) {
//						_indicator = new MBRoundProgressView ();
//					} else {
//						_indicator = new UIActivityIndicatorView (UIActivityIndicatorViewStyle.WhiteLarge);
//						((UIActivityIndicatorView)_indicator).StartAnimating ();
//					}
//					this.AddSubview(_indicator);
//				}
//				return _indicator;
//			}
//			set { _indicator = value; }
//		}
		private UIView Indicator;
		private float Width { get; set; }
		private float Height { get; set; }
		private NSTimer GraceTimer { get; set; }
		private NSTimer MinShowTimer { get; set; }
		private DateTime? ShowStarted { get; set; }
		private MBProgressHUDMode? _mode;
		public MBProgressHUDMode Mode {
			get {
				if (!_mode.HasValue) {
					_mode  = MBProgressHUDMode.Indeterminate;
					this.BeginInvokeOnMainThread (UpdateIndicators);
					this.BeginInvokeOnMainThread (SetNeedsLayout);
					this.BeginInvokeOnMainThread (this.SetNeedsDisplay);
				}
				return _mode.Value;
			}
			set {
				// Dont change mode if it wasn't actually changed to prevent flickering
				if (_mode == value) {
					return;
				}
				_mode = value;
				this.BeginInvokeOnMainThread (UpdateIndicators);
				this.BeginInvokeOnMainThread (SetNeedsLayout);
				this.BeginInvokeOnMainThread (this.SetNeedsDisplay);
			}
		}
		private NSAction MethodForExecution { get; set; }
		private bool UseAnimation { get; set; }
		private float YOffset { get; set; }
		private float XOffset { get; set; }
		private bool TaskInProgress { get; set; }
		private float GraceTime { get; set; }
		private float MinShowTime { get; set; }
		private UILabel Label { get; set; }
		private UILabel DetailsLabel { get; set; }
		private float _progress;
		public float Progress {
			get { return _progress; }
			set {
				if (_progress != value)
					_progress = value;
				if (Mode == MBProgressHUDMode.Determinate) {
					this.BeginInvokeOnMainThread (UpdateProgress);
					this.BeginInvokeOnMainThread (this.SetNeedsDisplay);

				}
			}
		}
		public event EventHandler HudWasHidden;
		private string _titleText;
		public string TitleText {
			get { return _titleText; }
			set {
				if (_titleText != value) {
					_titleText = value;
					this.BeginInvokeOnMainThread (() => Label.Text = value);
					this.BeginInvokeOnMainThread (SetNeedsLayout);
					this.BeginInvokeOnMainThread (this.SetNeedsDisplay);
				}
			}
		}
		private string _detailText;
		public string DetailText {

			get { return _detailText; }
			set {
				if (_detailText != value) {
					_detailText = value;
					this.BeginInvokeOnMainThread (() => DetailsLabel.Text = value);
					this.BeginInvokeOnMainThread (SetNeedsLayout);
					this.BeginInvokeOnMainThread (this.SetNeedsDisplay);
				}
			}
		}
		public float Opacity { get; set; }
		public UIFont TitleFont { get; set; }
		public UIFont DetailFont { get; set; }
		private bool IsFinished { get; set; }

		#region Accessor helpers

		private void UpdateProgress ()
		{
			var indicator = Indicator as MBRoundProgressView;
			if (indicator != null) {
				indicator.Progress = Progress;
			}
		}

		private void UpdateIndicators ()
		{
			if (Indicator != null) {
				Indicator.RemoveFromSuperview ();
			}

			this.Indicator = null;

			if (Mode == MBProgressHUDMode.Determinate) {
				Indicator = new MBRoundProgressView ();
			} else {
				Indicator = new UIActivityIndicatorView (UIActivityIndicatorViewStyle.WhiteLarge);
				((UIActivityIndicatorView)Indicator).StartAnimating ();
			}

			this.AddSubview (Indicator);
		}

		#endregion
		#region Constants

		public const float MARGIN = 20.0f;
		public const float PADDING = 4.0f;

		public const float LABELFONTSIZE = 22.0f;
		public const float LABELDETAILSFONTSIZE = 16.0f;
		#endregion

		#region Lifecycle methods

		public MBProgressHUD (UIWindow window) : base(window.Bounds)
		{
			Initialize ();
		}

		public MBProgressHUD (UIView view) : base(view.Bounds)
		{
			Initialize ();
		}

		public MBProgressHUD (RectangleF frame) : base(frame)
		{
			Initialize ();
		}

		void Initialize ()
		{
			this.Mode = MBProgressHUDMode.Indeterminate;
			this.TitleText = null;
			this.DetailText = null;
			this.Opacity = 0.9f;
			this.TitleFont = UIFont.BoldSystemFontOfSize (LABELFONTSIZE);
			this.DetailFont = UIFont.BoldSystemFontOfSize (LABELDETAILSFONTSIZE);
			this.XOffset = 0.0f;
			this.YOffset = 0.0f;
			this.GraceTime = 0.0f;
			this.MinShowTime = 0.0f;

			this.AutoresizingMask = UIViewAutoresizing.FlexibleTopMargin | UIViewAutoresizing.FlexibleBottomMargin | UIViewAutoresizing.FlexibleLeftMargin | UIViewAutoresizing.FlexibleRightMargin;

			// Transparent background
			this.Opaque = false;
			this.BackgroundColor = UIColor.Clear;

			// Make invisible for now
			this.Alpha = 0.0f;

			// Add label
			Label = new UILabel (this.Bounds);

			// Add details label
			DetailsLabel = new UILabel (this.Bounds);

			TaskInProgress = false;
		}

		public void Dispose ()
		{
			this.Indicator = null;
			Label.Dispose ();
			Label = null;
			DetailsLabel.Dispose ();
			DetailsLabel = null;
			GraceTimer.Dispose ();
			GraceTimer = null;
			MinShowTimer.Dispose ();
			MinShowTimer = null;
			base.Dispose ();
		}

		#endregion
		#region Layout

		public override void LayoutSubviews ()
		{
			RectangleF frame = this.Bounds;

			// Compute HUD dimensions based on indicator size (add margin to HUD border)
			RectangleF indFrame = Indicator.Bounds;
			this.Width = indFrame.Size.Width + 2 * MARGIN;
			this.Height = indFrame.Size.Height + 2 * MARGIN;

			// Position the indicator
			indFrame = new RectangleF ((float)Math.Floor ((frame.Size.Width - indFrame.Size.Width) / 2) + this.XOffset, (float)Math.Floor ((frame.Size.Height - indFrame.Size.Height) / 2) + this.YOffset, indFrame.Size.Width, indFrame.Size.Height);
			Indicator.Frame = indFrame;

			// Add label if label text was set
			if (null != this.TitleText) {
				// Get size of label text
				SizeF dims = StringSize (TitleText, this.TitleFont);

				// Compute label dimensions based on font metrics if size is larger than max then clip the label width
				float lHeight = dims.Height;
				float lWidth;
				if (dims.Width <= (frame.Size.Width - 2 * MARGIN)) {
					lWidth = dims.Width;
				} else {
					lWidth = frame.Size.Width - 4 * MARGIN;
				}

				// Set label properties
				Label.Font = this.TitleFont;
				Label.AdjustsFontSizeToFitWidth = false;
				Label.TextAlignment = UITextAlignment.Center;
				Label.Opaque = false;
				Label.BackgroundColor = UIColor.Clear;
				Label.TextColor = UIColor.White;
				Label.Text = this.TitleText;

				// Update HUD size
				if (this.Width < (lWidth + 2 * MARGIN)) {
					this.Width = lWidth + 2 * MARGIN;
				}
				this.Height = this.Height + lHeight + PADDING;

				// Move indicator to make room for the label
				indFrame = new RectangleF (indFrame.Location.X, indFrame.Location.Y - (float)(Math.Floor (lHeight / 2 + PADDING / 2)), indFrame.Width, indFrame.Height);
				Indicator.Frame = indFrame;

				// Set the label position and dimensions
				RectangleF lFrame = new RectangleF ((float)Math.Floor ((frame.Size.Width - lWidth) / 2) + XOffset, (float)Math.Floor (indFrame.Location.Y + indFrame.Size.Height + PADDING), lWidth, lHeight);
				Label.Frame = lFrame;

				this.AddSubview (Label);

				// Add details label delatils text was set
				if (null != this.DetailText) {
					// Get size of label text
					dims = StringSize (DetailText, this.DetailFont);

					// Compute label dimensions based on font metrics if size is larger than max then clip the label width
					lHeight = dims.Height;
					if (dims.Width <= (frame.Size.Width - 2 * MARGIN)) {
						lWidth = dims.Width;
					} else {
						lWidth = frame.Size.Width - 4 * MARGIN;
					}

					// Set label properties
					DetailsLabel.Font = this.DetailFont;
					DetailsLabel.AdjustsFontSizeToFitWidth = false;
					DetailsLabel.TextAlignment = UITextAlignment.Center;
					DetailsLabel.Opaque = false;
					DetailsLabel.BackgroundColor = UIColor.Clear;
					DetailsLabel.TextColor = UIColor.White;
					DetailsLabel.Text = this.DetailText;

					// Update HUD size
					if (this.Width < lWidth) {
						this.Width = lWidth + 2 * MARGIN;
					}
					this.Height = this.Height + lHeight + PADDING;

					// Move indicator to make room for the new label
					indFrame = new RectangleF (indFrame.Location.X, indFrame.Location.Y - ((float)Math.Floor (lHeight / 2 + PADDING / 2)), indFrame.Width, indFrame.Height);
					Indicator.Frame = indFrame;

					// Move first label to make room for the new label
					lFrame = new RectangleF (lFrame.Location.X, lFrame.Location.Y - ((float)Math.Floor (lHeight / 2 + PADDING / 2)), lFrame.Width, lFrame.Height);
					Label.Frame = lFrame;

					// Set label position and dimensions
					RectangleF lFrameD = new RectangleF ((float)Math.Floor ((frame.Size.Width - lWidth) / 2) + XOffset, lFrame.Location.Y + lFrame.Size.Height + PADDING, lWidth, lHeight);
					DetailsLabel.Frame = lFrameD;

					this.AddSubview (DetailsLabel);
				}
			}
		}

		#endregion

		#region Showing and execution

		public void Show (bool animated)
		{
			UseAnimation = animated;

			// If the grace time is set postpone the HUD display
			if (this.GraceTime > 0.0) {
				this.GraceTimer = NSTimer.CreateScheduledTimer (this.GraceTime, HandleGraceTimer);
				// ... otherwise show the HUD imediately
			} else {
				this.SetNeedsDisplay ();
				ShowUsingAnimation (UseAnimation);
			}
		}

		public void Hide (bool animated)
		{
			UseAnimation = animated;

			// If the minShow time is set, calculate how long the hud was shown,
			// and pospone the hiding operation if necessary
			if (this.MinShowTime > 0.0 && ShowStarted.HasValue) {
				double interv = (DateTime.Now - ShowStarted.Value).TotalSeconds;
				if (interv < this.MinShowTime) {
					this.MinShowTimer = NSTimer.CreateScheduledTimer ((this.MinShowTime - interv), HandleMinShowTimer);
					return;
				}
			}

			// ... otherwise hide the HUD immediately
			HideUsingAnimation (UseAnimation);
		}

		void HandleGraceTimer ()
		{
			// Show the HUD only if the task is still running
			if (TaskInProgress) {
				this.SetNeedsDisplay ();
				ShowUsingAnimation (UseAnimation);
			}
		}

		void HandleMinShowTimer ()
		{
			HideUsingAnimation (UseAnimation);
		}

		public void ShowWhileExecuting (NSAction method, bool animated)
		{

			MethodForExecution = method;

			// Launch execution in new thread
			TaskInProgress = true;
			//TODO: THIS PROBABLY DOES NOT WORK!
			LaunchExecution ();

			// Show HUD view
			this.Show (animated);
		}

		void LaunchExecution ()
		{
			using (NSAutoreleasePool pool = new NSAutoreleasePool ()) {
				var th = new System.Threading.Thread (() =>
				{
					MethodForExecution.Invoke ();
					this.BeginInvokeOnMainThread (CleanUp);
				});
				th.Start ();
			}
		}

		void AnimationFinished ()
		{
			this.Done ();
		}

		void Done ()
		{
			IsFinished = true;

			// If delegate was set make the callback
			this.Alpha = 0.0f;

			if (HudWasHidden != null) {
				HudWasHidden(this, EventArgs.Empty);
			}
		}

		void CleanUp ()
		{
			TaskInProgress = false;

			this.Indicator = null;

			this.Hide (UseAnimation);
		}

		#endregion
		#region Fade in and Fade out

		void ShowUsingAnimation (bool animated)
		{
			this.ShowStarted = DateTime.Now;
			// Fade in
			if (animated) {
				UIView.BeginAnimations (null);
				UIView.SetAnimationDuration (0.40);
				this.Alpha = 1.0f;
				UIView.CommitAnimations ();
			} else {
				this.Alpha = 1.0f;
			}
		}

		void HideUsingAnimation (bool animated)
		{
			// Fade out
			if (animated) {
				if (animated) {
					UIView.BeginAnimations (null);
					UIView.SetAnimationDuration (0.40);
					this.Alpha = .02f;
					NSTimer.CreateScheduledTimer (.4, AnimationFinished);
					UIView.CommitAnimations ();

				} else {
					this.Alpha = 0.0f;
					this.Done ();
				}
			}
		}
		#endregion
		#region BG Drawing

		public override void Draw (RectangleF rect)
		{
			// Center HUD
			RectangleF allRect = this.Bounds;
			// Draw rounded HUD bacgroud rect
			RectangleF boxRect = new RectangleF (((allRect.Size.Width - this.Width) / 2) + this.XOffset, ((allRect.Size.Height - this.Height) / 2) + this.YOffset, this.Width, this.Height);
			CGContext ctxt = UIGraphics.GetCurrentContext ();
			this.FillRoundedRect (boxRect, ctxt);
		}

		void FillRoundedRect (RectangleF rect, CGContext context)
		{
			float radius = 10.0f;
			context.BeginPath ();
			context.SetGrayFillColor (0.0f, this.Opacity);
			context.MoveTo (rect.GetMinX () + radius, rect.GetMinY ());
			context.AddArc (rect.GetMaxX () - radius, rect.GetMinY () + radius, radius, (float)(3 * Math.PI / 2), 0f, false);
			context.AddArc (rect.GetMaxX () - radius, rect.GetMaxY () - radius, radius, 0, (float)(Math.PI / 2), false);
			context.AddArc (rect.GetMinX () + radius, rect.GetMaxY () - radius, radius, (float)(Math.PI / 2), (float)Math.PI, false);
			context.AddArc (rect.GetMinX () + radius, rect.GetMinY () + radius, radius, (float)Math.PI, (float)(3 * Math.PI / 2), false);
			context.ClosePath ();
			context.FillPath ();
		}

	}

	#endregion

	public class MBRoundProgressView : UIProgressView
	{
		public MBRoundProgressView () : base(new RectangleF (0.0f, 0.0f, 37.0f, 37.0f))
		{
		}

		public override void Draw (RectangleF rect)
		{

			RectangleF allRect = this.Bounds;
			RectangleF circleRect = new RectangleF (allRect.Location.X + 2, allRect.Location.Y + 2, allRect.Size.Width - 4, allRect.Size.Height - 4);

			CGContext context = UIGraphics.GetCurrentContext ();

			// Draw background
			context.SetRGBStrokeColor (1.0f, 1.0f, 1.0f, 1.0f);
			// white
			context.SetRGBFillColor (1.0f, 1.0f, 1.0f, 0.1f);
			// translucent white
			context.SetLineWidth (2.0f);
			context.FillEllipseInRect (circleRect);
			context.StrokeEllipseInRect (circleRect);

			// Draw progress
			float x = (allRect.Size.Width / 2);
			float y = (allRect.Size.Height / 2);
			context.SetRGBFillColor (1.0f, 1.0f, 1.0f, 1.0f);
			// white
			context.MoveTo (x, y);
			context.AddArc (x, y, (allRect.Size.Width - 4) / 2, -(float)(Math.PI / 2), (float)(this.Progress * 2 * Math.PI) - (float)(Math.PI / 2), false);
			context.ClosePath ();
			context.FillPath ();
		}

	}

}

AutoMapper and MonoTouch

Sadly, it doesn’t seem to work. v1.1 of Automapper breaks the compiler completely, and v1.0 just crashes. Something about “cannot cast this object to that object” … Too bad. That would have been pretty slick.

If anyone else has done this with success, please leave a comment!

UIScrollView deliciousness… keeping the keyboard out of the way…

So I’ve got a really tall form for users to enter their profile information. The app works by swapping out views into a single content area (UIScrollView). Now, the hard part is keeping the keyboard from covering up whatever a user’s trying to enter. Enter some sugar:

On the form view controller:
(somewhere I’m calling myTextField.EditingDidBegin += HandleTfEditingDidBegin;)
void HandleTfEditingDidBegin (object sender, EventArgs e)
{
 var tf = sender as UITextField;
 if (tf != null) {
  var loc = tf.Frame.Location; // this is the textfield's placement in the view.
  if (loc.Y > 210) { // 210 happens to be the optimal LOWEST point in my case. Adjust it and see.
  if (this.NeedsScroll != null)
   this.NeedsScroll (null, new ScrollEventArgs (loc.Y - 210f)); //take the 210 back out.
  }
 }
}

And the view that manages the ScrollView:

void HandleAccountVCNeedsScroll (object sender, PhoneManageAccountViewController.ScrollEventArgs e)
{
 var offset = this.ContentAreaView.ContentOffset;
 if (e.ScrollY > offset.Y) { // only scroll down! whee!
   this.ContentAreaView.SetContentOffset (new PointF (offset.X, e.ScrollY), true);
  }
}

Oh, and finally - when I add a new view to the main UIScrollView, I use this handy method:

private void DisplayContent (UIView newView)
{
 this.ContentAreaView.AddSubview (newView);
 this.ContentAreaView.ContentSize = newView.Bounds.Size;
}

Emulating backgroundTap in Monotouch to hide keyboard

I searched and searched, and couldn’t figure out how to hide the keyboard when a user clicks on the view…

Finally, I dun figured it out on my own. It probably should be obvious to most, but here it is anyhow (place this in the code for your view controller):

public override void TouchesEnded (NSSet touches, UIEvent evt)
{
	foreach (var item in this) {
		var tf = item as UITextField;
		if (tf != null &amp;&amp; tf.IsFirstResponder) {
			tf.ResignFirstResponder ();
		}
	}
	base.TouchesEnded (touches, evt);
}

Unwire your events to make Garbage Collector happy.

In Monotouch, I have an application that was eating up a lot of memory. When digging into Instruments, I found that it was multiple copies of images being retained well past their needed lifetime.

Generally, I love me some anonymous delegates when invoking simple actions. That is,

someButton.TouchUpInside += delegate { DoSomething(); }

However, if you are swapping views often, this becomes problematic as the UIButton will not release the image from memory, even if you manually call .Dispose() on the button and its imageview and it’s imageview’s image.

So, to prevent that behavior, I moved to using explicit methods for the delegates. This way, I can manually unhook them from the object in the .Dispose() or my personal .Cleanup() methods. This allows the button to be completely disposed and GC to work as expected. Without the underlined snippet below, I was seeing memory usage explode – to the tune of 100s of images being loaded (even though there are only ~16 images in total, and never more than 4 displayed at once).
Sample:


public UIImage image;
public override void ViewDidLoad ()
{
	this.button.SetImage(image,UIControlState.Normal);
	this.button.TouchUpInside += HandleButtonhandleTouchUpInside;
}
int Z;
void HandleButtonhandleTouchUpInside (object sender, EventArgs e)
{
	var x = 13;
	var y = 12;
	var z = x + y;
	Z = z;
}
public void Cleanup()
{
	this.button.TouchUpInside -= HandleButtonhandleTouchUpInside;
	this.button.Dispose();
	base.Dispose();
}

Aborting an NSTimer in MonoTouch

It’s really easy: timer.Invalidate();
Not being accustomed to timers, I was looking for an .Abort() method. But, it seems that Invalidate encompasses the same function.

Of course, that means you have to keep a reference around. This is in reference to my previous post about checking the fire date of a timer – It wasn’t working quite perfectly for me, so I ended up invalidating it, then initializing a new timer instance to fire the event in the future. And now, somehow, I managed to make it work.

In related news: Having too many UIView animations can cause some interesting things to happen – views showing or hiding when they’re not supposed to, random “ghost” images of your view… etc etc. Be sure to clean up and keep track of everything in the timeline when using UIView Animations.

Final Code:

private NSTimer timer;
void DoStuff()
{
	if (timer != null &&  timer.IsValid)
	{
		timer.Invalidate();
	}
	timer = NSTimer.CreateScheduledTimer(4,ReallyDoStuff);
}

UIScrollView in MonoTouch: The easy way.

Background: I’ve got an “instructions” view xib that I created in Interface Builder (because I *like* IB!). To set it up for multiple pages, I figured I’d just make it 3x the normal width: 960 px.

The Problem: Easy way to scroll, page controller, and snapping to position.

Anyhow, I found the easiest way is to do it like so:

public override void ViewDidLoad ()
{
	this.contentArea.ContentSize = new SizeF (960, 377); // set the initial content size.
	this.contentArea.AddSubview (instructionsView);
	this.pageControl.ValueChanged += HandlePageControlhandleValueChanged;
	this.contentArea.DraggingEnded += HandleContentAreahandleDraggingEnded;
	this.contentArea.DecelerationEnded += HandleContentAreahandleDecelerationEnded;
}

void HandleContentAreahandleDecelerationEnded (object sender, EventArgs e)
{
	SnapToView();
}

private void SnapToView ()
{
	int pageCount = 0;
	float offset = contentArea.ContentOffset.X;
	if (offset <= 160) {
		pageCount = 0;
	} else if (offset > 160 && offset <= 480) {
		pageCount = 1;
	} else if (offset > 480) {
		pageCount = 2;
	}
	pageControl.CurrentPage = pageCount;
	contentArea.ScrollRectToVisible (new RectangleF (320 * pageCount, 0, 320, 377), true);
}

void HandleContentAreahandleDraggingEnded (object sender, DraggingEventArgs e)
{
	SnapToView ();
}
void HandlePageControlhandleValueChanged (object sender, EventArgs e)
{
	contentArea.ScrollRectToVisible (new RectangleF (320 * pageControl.CurrentPage, 0, 320, 377), true);
}

Voila! Snaps to position when user scrolls (with or without deceleration) and also scrolls cleanly when using the scrollview with a subview larger than the displayable screen real estate.

Multiplayer in MonoTouch

The easy way is to use GameKit, good starting article at :

http://mikebluestein.wordpress.com/2010/06/14/using-gamekit-with-monotouch-2/

Though what I *really* want to do is create an ad-hoc wifi network… So far it seems that apple has restricted devs to only access existing wifi networks (IE you aren’t allowed to create your own ad-hoc network on the iphone). I’m assuming this is a “feature” designed to make it more difficult to find a way to tether your iPhone to another device.

Oh well. Buzzwordingo should have chat capabilities soon enough.