Monthly Archives: September 2010

UITextField.MaxLength (or something like it)

So, I need to enforce: Numbers only, and a maximum length (Zip code, whee!) of a UITextField on my iPhone app.

Here’s what I came up with (later, I shall implement some sort of RegEx style of doing this)

               bool CheckText(UITextField fld, NSRange rng, string newChar, int maxLength, bool numbersOnly)
		{
			const string numbers = "0123456789";
			if (fld.Text.Length >= maxLength && rng.Length == 0) {
					return false;
				} else {
					if (!numbersOnly)
					return true;
					if (numbers.IndexOf(newChar) >=0)
					return true;
						return false;
				}
		}

and then to attach it to a text field:

               ZipInput.ShouldChangeCharacters = (fld, rng, str) => CheckText(fld,rng,str,5,true);

Easy as pie.

Why doesn’t apple just show the network activity indicator?

So, for real: Why doesn’t apple just turn on the network activity indicator when there’s traffic? I find it silly that I have to manage that. I mean, beyond just single-connection scenarios, if I have some process (let’s say, a UITableView) that is going to download multiple items, now I have to manually monitor the number of threads in use, and when that hits zero shut the indicator off.

And if I don’t, Apple is quite likely to reject my app, as it does not adhere to the development guidelines/standards they set up.

It seems like something this trivial should be handled by the FRAMEWORK, since they won’t allow me to directly access the API that, I’m rather sure KNOWS when the network is in use.

That said, here’s some code to do it for you – just call AddNetworkConnection and RemoveNetworkConnection (you can do it in a simpler format, but this is the more awesome way with read and writer locks);


public static class Utility
{
private static ReaderWriterLock rwl = new ReaderWriterLock();
private  static int Connections = 0;

public static void AddNetworkConnection()
{
rwl.AcquireReaderLock(TimeSpan.FromSeconds(1)); //You don't actually have to check, you can just always set this to true
if (Connections == 0)  // but I like to be fancy.
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
rwl.UpgradeToWriterLock(TimeSpan.FromSeconds(1));
Connections++;
rwl.ReleaseLock();
}

public static void RemoveNetworkConnection()
{
rwl.AcquireWriterLock(TimeSpan.FromSeconds(1));
Connections--;
if (Connections < 0)
Connections = 0; //just in case, y'know?
if (Connections <= 0)
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
rwl.ReleaseLock();
}
}

And that’s it, you’ve got a simple way to keep the Network Activity indicator updated.
If I were more awesome, I’d find a way to attach it to the thread accessing the network, and automatically decrement the count. But, this way works with both blocking and non-blocking calls. So meh.

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 ();
		}

	}

}