Reader Q&A – PDFs in iOS

I got a question from a reader last night who was looking at some code from one of my Xamarin seminars.

Ryan asked about how to extract the content from a pdf file, draw on it, and email it in iOS.

One way to do this is using Core Graphics, as shown in the following snippet:

var pdf = CGPDFDocument.FromFile (Path.Combine (NSBundle.MainBundle.BundlePath, "input.pdf"));
var data = new NSMutableData ();
var rect = new CGRect (0, 0, 400, 400);

UIGraphics.BeginPDFContext (data, rect, null);
UIGraphics.BeginPDFPage ();

var g = UIGraphics.GetCurrentContext (); 
g.ScaleCTM (1, -1);
g.TranslateCTM (0, -400);

var p = pdf.GetPage (1); 

var txf = p.GetDrawingTransform (CGPDFBox.Crop, rect, 0, true);
g.ConcatCTM (txf);
g.DrawPDFPage (p);

g.SetLineWidth (2);
UIColor.Red.SetFill ();
UIColor.Blue.SetStroke ();

var path = new CGPath ();

path.AddLines (new [] {
	new CGPoint (100, 200),
	new CGPoint (160, 100), 
	new CGPoint (220, 200)
});

path.CloseSubpath ();

g.AddPath (path);
g.DrawPath (CGPathDrawingMode.FillStroke);

UIGraphics.EndPDFContent ();

var mail = new MFMailComposeViewController ();
mail.AddAttachmentData (data, "text/x-pdf", "output.pdf");

view raw

cg_pdf.md

hosted with ❤ by GitHub

If you have a question feel free to contact me through my blog. I get lots of questions like this, but I do my best to respond to them all.

CBPeripheralManager for iBeacons in iOS 8

I recently ran into an interesting issue with the code for the FindTheMonkey iBeacon app. Previously I had the code for the app to act as an iBeacon using a CBPeripheralManager, which I created in the ViewDidLoad method. This previously worked fine but someone ran into an issue where ranging for the beacon never discovered it. To resolve this involved a couple small changes:

  1. Move the creation of the CBPeriheralManager into the constructor.
  2. peripheralDelegate = new BTPeripheralDelegate ();
    
    peripheralMgr = new CBPeripheralManager (
      peripheralDelegate, 
      DispatchQueue.DefaultGlobalQueue);
    
  3. Call the StartAdvertising method in ViewDidAppear.
  4. public override void ViewDidAppear (bool animated)
    {
      base.ViewDidAppear (animated);
    
      var power = new NSNumber (-59);
      peripheralData = beaconRegion.GetPeripheralData (power);
      peripheralMgr.StartAdvertising (peripheralData);
    } 
    

With these changes in place, ranging for the beacon discovers it as it had before:

iBeacon FindTheMonkey

Some Things in iOS 8

iOS 8 added a lot of new functionality and APIs. Along the way, several things have changed. Here are a few items I’ve come across:

Documents and Library

UPDATE: Xamarin.iOS now handles getting the folder path correctly when using Environment.SpecialFolder in iOS 8 as well.

Prior to iOS8 it was common for Xamarin.iOS applications to access folder paths using the .NET System.Environment class, which on iOS provided a familiar abstraction around native system folders. For example, you could get to the documents folder like this:

var docs = Environment.GetFolderPath (
  Environment.SpecialFolder.MyDocuments);

However, in iOS 8 the location of some folders, namely the Documents and Library folders, has changed such that they are no longer within the app’s bundle.

Apple describes the changes in Technical Note TN2406.

The proper way of determining the location of these folders is to use the NSFileManager. For example, get the location of the Documents folder as follows:

var docs = NSFileManager.DefaultManager.GetUrls (
  NSSearchPathDirectory.DocumentDirectory, 
  NSSearchPathDomain.User) [0];

Location Manager

To use location in iOS you go through the CLLocationManager class. Before iOS 8 the first time an app attempted to start location services the user was presented with a dialog asking to turn location services on. You could set a purpose string directly on the location manager to tell the user why you need location in this dialog.

In iOS 8 you now have to call either RequestWhenInUseAuthorization or RequestAlwaysAuthorization on the location manager. Additionally you need to add either the concisely named NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription to your Info.plist. Thanks to my buddy James for tracking these down.

AVSpeechSynthesizer


The AVSpeechSynthesizer was added in iOS 7, allowing apps to deliver text to speech functionality with just a few lines of code, like this:

var speechSynthesizer = new AVSpeechSynthesizer ();

var speechUtterance = new AVSpeechUtterance (text) {

  Rate = AVSpeechUtterance.MaximumSpeechRate/4,

  Voice = AVSpeechSynthesisVoice.FromLanguage ("en-US"),

  Volume = 1.0f

};


speechSynthesizer.SpeakUtterance (speechUtterance);

The above code worked on either the simulator or a device prior to iOS 8. However, when run on an iOS 8 simulator, you are now greeted with the following error message:

Speech initialization error: 2147483665

However it does appear to work on a device. There is an open bug here: http://openradar.appspot.com/17299966

Thanks to René Ruppert for discovering this. Incidentally, René has a blog post on a few other iOS 8 issues worth checking out: http://krumelur.me/2014/09/23/my-ios8-adventure-as-a-xamarin-developer/

Input Accessory Views

Before iOS 8 you could set the InputAccessoryView on a UITextField from a view contained in another controller.

aTextField.InputAccessoryView = aViewController.SomeView;

While this worked before iOS 8, it did not guarantee the view controller hierarchy would be set up properly. A better approach, even before iOS 8, would be to set the InputAccessoryView directly to an instance of UIView subclass, not one contained in another UIViewController. Practically speaking, people would take the view controller approach because it let them set things up via a xib. Therefore, to handle the view controller case, iOS 8 introduced the InputAccessoryViewController property on UIResponder. It’s still easier to just use a UIView subclass imho, but if you need to use a UIViewController, set it to InputAccessoryViewController.

Action Sheets

Apple states in their documentation that you should not add a view to a UIActionSheet’s view hierarchy. Before iOS 8 adding subviews to a UIActionSheet would actually work, although it was never the intention (nor should it be subclassed). Code that took this approach should have presented a view controller.

In iOS 8 subclassing UIActionSheet or adding subviews to it will no longer work. Additionally UIActionSheet itself has been deprecated. Instead, you should use a UIAlertController in iOS 8 (UIAlertController should also be used in iOS 8 in place of the deprecated UIAlertView) as I discussed in my iOS 8 webinar.

Video – Build Your First iOS App with Visual Studio and Xamarin

Here’s a video I made for Microsoft’s Flashcast series showing how to develop an iOS application using Visual Studio:



My colleague James did one for Android as well: http://flashcast.azurewebsites.net/stream/episode/5

Later this month we’ll have more Flashcasts, including one that shows how to use Xamarin’s iOS Designer for Visual Studio, and another on Xamarin.Forms. Keep an eye out for these, as well as other great topics at http://flashcast.azurewebsites.net

iOS 8 Scene Kit sample in F#

Here’s an F# iOS 8 Scene Kit port of the C# code from the Xamarin blog, ported with some help from Larry O’Brien.

namespace FSHelloSceneKit
    
open System
open MonoTouch.UIKit
open MonoTouch.Foundation
open MonoTouch.SceneKit
 
type FSHelloSceneKitViewController () =
    inherit UIViewController()
   
    let CreateDiffuseLightNode (color: UIColor, position: SCNVector3, lightType: NSString): SCNNode = 
        new SCNLight ( Color = color, LightType = lightType )
        |> fun lightNode -> new SCNNode ( Light = lightNode, Position = position )
 
    override this.ViewDidLoad () =
        let scene = new SCNScene ()
     
        let view = new SCNView (this.View.Frame, Scene = scene, AutoresizingMask = UIViewAutoresizing.All, AllowsCameraControl = true)
 
        new SCNCamera (XFov = 40.0, YFov = 40.0)
        |> fun c -> new SCNNode (Camera = c, Position = new SCNVector3(0.0F, 0.0F, 40.0F))
        |> scene.RootNode.AddChildNode
 
        let material = new SCNMaterial ()
        material.Diffuse.Contents <- UIImage.FromFile ("monkey.png")
        material.Specular.Contents <- UIColor.White
 
        new SCNNode( Geometry = SCNSphere.Create(10.0F), Position = new SCNVector3(0.0F, 0.0F, 0.0F) )
        |> fun node -> node.Geometry.FirstMaterial <- material; node
        |> scene.RootNode.AddChildNode
 
        new SCNLight ( LightType = SCNLightType.Ambient, Color = UIColor.Purple)
        |> fun lightNode -> new SCNNode ( Light = lightNode )
        |> scene.RootNode.AddChildNode
 
        [|
            ( UIColor.Blue, new SCNVector3 (-40.0F, 40.0F, 60.0F) );
            ( UIColor.Yellow, new SCNVector3 (20.0F, 20.0F, -70.0F) );
            ( UIColor.Red, new SCNVector3 (20.0F, -20.0F, 40.0F) );
            ( UIColor.Green, new SCNVector3 (20.0F, -40.0F, 70.0F) )
        |]
        |> Seq.map (fun (color, pos) -> CreateDiffuseLightNode(color, pos, SCNLightType.Omni))
        |> Seq.iter scene.RootNode.AddChildNode
 
        this.View.Add(view)

You can read more about Scene Kit on the Xamarin blog.

Draw a PDF in Landscape with Core Graphics

I just got a question from a Core Graphics presentation I gave a while back (from the first Xamarin Seminar) about how to use Core Graphics to render a PDF with 2 pages side-by-side in landscape. Since this question has come up a couple times in the past I figured I’d write a blog post about it.

The solution I use for this is to create a rectangle to display each page and then apply a transform to render the page within the rectangle. CGPDFPage has a handy GetDrawingTransform function that returns the transform. To get back a transform that crops the page to the rectangle while preserving the aspect ratio simply call:

CGAffineTransform transform = 
    pdfPage.GetDrawingTransform (CGPDFBox.Crop, pageRectangle, 0, true);

To do this for 2 pages, use the SaveState and RestoreState functions of the CGContext to get the transformation matrix back to its intial state, so that the transformation of the first page isn’t applied to the second page.

The following code shows how to implement this in a UIView subclass:

public override void Draw (RectangleF rect)
{
    base.Draw (rect);

    var rect1 = new RectangleF (0, 0, Bounds.Width / 2, Bounds.Height);
    var rect2 = new RectangleF (Bounds.Width / 2, 0, Bounds.Width / 2, Bounds.Height);
    
    using (CGContext gctx = UIGraphics.GetCurrentContext ()) {
        gctx.TranslateCTM (0, Bounds.Height);
        gctx.ScaleCTM (1, -1);

        gctx.SaveState ();

        using (CGPDFPage page = pdf.GetPage (page1)) {
            CGAffineTransform transform = 
                page.GetDrawingTransform (CGPDFBox.Crop, rect1, 0, true);
            gctx.ConcatCTM (transform);
            gctx.DrawPDFPage (page);
        }

        gctx.RestoreState ();

        using (CGPDFPage page = pdf.GetPage (page2)) {
            CGAffineTransform transform = 
                page.GetDrawingTransform (CGPDFBox.Crop, rect2, 0, true);
            gctx.ConcatCTM (transform);
            gctx.DrawPDFPage (pdfPg);
        }
    }
}

This renders the PDF to the screen as shown below:

landscapepdf

Core Animation Resources for Xamarin.iOS Developers

If you are an iOS developer, you owe it to yourself to try and learn the Core Animation framework because it isn’t just about animation. Core Animation makes much of what you see in iOS applications possible.

Here are some resources to get  you started:

Going through these resources will give an understanding of how the Core Animation framework is at the heart of iOS, which will help you to create experiences that differentiate your application.

UISplitViewController and UICollectionView

Continuing from the previous post that used a UICollectionView to display a grid of images from Bing, the following example adds a UICollectionView to a UISplitViewController.

When the row is selected, a new search is issued and the resulting images are updated in the UICollectionView. To connect the selection in the UISplitViewController’s master controller, which is an MT.D DialogViewController in this case, to the UICollectionViewController contained in the UISplitViewController’s detail controller, an event is raised when a row is selected:

public AnimalsController () : base (null)
{
    Root = new RootElement ("Animals") {
        new Section () {
            from animal in animals
            select (Element) new StringElement(animal, () => {
            if(AnimalSelected != null)
                AnimalSelected(this, new AnimalSelectedEventArgs{Animal = animal});
            })
         }};
 }

Then the UISplitViewController’s implementation handles this event to communicate to the UICollectionViewController:

animalImageController = new BingImageGridViewController (layout);

animalsController.AnimalSelected += (sender, e) => {
    animalImageController.LoadImages (e.Animal);
};

In the LoadImages method, the UICollectionViewController calls Bing for the selected item and uploads the UICollectionView when the data is returned by calling ReloadData:


public void LoadImages (string search)
{
    UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;

    bing = new Bing ((results) => {
        InvokeOnMainThread (delegate {
            imageUrls = results;
            CollectionView.ReloadData ();
            UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
        });
    });

    bing.ImageSearch (search);
}

The sample is available at https://github.com/mikebluestein/BingImageGrid/tree/master/BingImageGridSplit

Note: you’ll need a Bing API key, which you can get at http://datamarket.azure.com