8533. Building iOS App with Xamarin
Xamarin, Xcode, SQLite, and C#


Tutorial for how to build iOS App with Xamarin and C#.

1. iOS App

1.1 Requirement

We will create an app named ‘Game Store’ to manage products. It has the similar layout and functions with the app we created with Xcode and Swift. See more details in the posting Building iOS App with Xcode.

1.2 Creating Project

In Visual Studio, File->New Solution, select Multiplatform->App->Blank Native App(iOS, Android), Next. image
Specify the name and Organization Identifier, Next. image
Specify the location where the source files locate, Create. image
One solution and three projects are created. ‘GameStoreXamarin’ is a Portable .NET project, which contains common functions. ‘GameStoreXamarin.Droid’ and ‘GameStoreXamarin.iOS’ are specifically responsible for UI, one for Android and another for iOS. image
Rename the project ‘GameStoreXamarin’ to ‘GameStoreXamarin.Core’. And Rename project ‘GameStoreXamarin.Droid’ to ‘GameStoreXamarin.Android’.

2. Portable Project

We will use SQLite to store data for this app. And all of the core database operations are in this portable project. Later, it will be shared to iOS project and Android project. This portable project is re-usable.

2.1 Installing Packages

Select the ‘GameStoreXamarin.Core’ project, Project->Add NuGet Package, then NuGet Package Manager will be opened. image
Search ‘sqlite’, select the package named ‘sqlite-net-pcl’, click ‘Add Package’. image
The selected package will be installed to current project. A new file named ‘package.config’ is added to the project. sqlite and its dependencies will be listed in this file.

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="sqlite-net-pcl" version="1.4.118" targetFramework="portable45-net45+win8+wpa81" />
  <package id="SQLitePCLRaw.bundle_green" version="1.1.9" targetFramework="portable45-net45+win8+wpa81" />
  <package id="SQLitePCLRaw.core" version="1.1.9" targetFramework="portable45-net45+win8+wpa81" />
</packages>

Install another package named ‘Xamarin.Forms’. Notice that, new packages are installed into the project’s Packages folder. After the installation, you can include them into your project with ‘using’ keyword.

2.2 Creating Classes

Create a folder named ‘Models’ under the project, and create a file named ‘Product.cs’. We defined four properties for each product, id, name, price and a photo of the product.

using SQLite;

namespace GameStoreXamarin.Core.Models
{
    public class Product
    {
        [PrimaryKey, AutoIncrement]
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public double Price { get; set; }
        public byte[] Image { get; set; }
    }
}

Create an interface named ‘IFileHelper.cs’. This interface defines a method ‘GetLocalFilePath()’ to get the location where the SQLite database file is stored on iOS or Android.

using System;
namespace GameStoreXamarin.Core
{
    public interface IFileHelper
    {
        string GetLocalFilePath(string filename);
    }
}

Create a folder named ‘Data’ under the project, and create a file named ‘GameStoreDatabase.cs’. This class defines the CRUD operations on SQLite.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GameStoreXamarin.Core.Models;
using SQLite;

namespace GameStoreXamarin.Core.Data
{
    public class GameStoreDatabase
    {
        private readonly SQLiteConnection database;
        private String TABLE_NAME = "Product";

        public GameStoreDatabase(string dbPath)
        {
            database = new SQLiteConnection(dbPath);
            database.CreateTable<Product>();
        }

        public List<Product> GetProducts()
        {
            return database.Query<Product>("SELECT * FROM [" + TABLE_NAME + "]");
        }

        public Product GetProduct(int id)
        {
            return database.Table<Product>().Where(i => i.ProductId == id).FirstOrDefault();
        }

        public int SaveProduct(Product product)
        {
            if (product.ProductId != 0)
            {
                return database.Update(product);
            }
            else
            {
                return database.Insert(product);
            }
        }

        public int DeleteProduct(int id)
        {
            Product product = GetProduct(id);
            return database.Delete(product);
        }

        public int DeleteProduct(Product product)
        {
            return database.Delete(product);
        }
    }
}

In ‘Data’ folder, create another class named ‘DatabaseHelper.cs’. Use the singleton pattern to create database instance. Notice we use ‘DependencyService’ to get the location of database file.

using System;
using GameStoreXamarin.Core;
using GameStoreXamarin.Core.Data;
using Xamarin.Forms;

namespace GameStoreXamarin.Core.Data
{
    public static class DatabaseHelper
    {
        static GameStoreDatabase database;

        public static GameStoreDatabase Database
        {
            get
            {
                if (database == null)
                {
                    String path = DependencyService.Get<IFileHelper>().GetLocalFilePath("GameStoreSQLite.db3");
                    database = new GameStoreDatabase(path);
                }
                return database;
            }
        }
    }
}

2.3 Final Project Structure

The final structure of the portable project. image

3. iOS Project

3.1 Creating Views

1) Right click the ‘GameStoreXamarin.iOS’ project, Add->New File. Select iOS->View Controller, set name to ‘ProductTableViewController’. image
Open the ‘ProductTableViewController.cs’ file, change the base class from ‘UIViewController’ to ‘UITableViewController’.
2) Create another file named ‘ProductTableViewCell.cs’ through Add->New File->iOS->Table View Cell.
3) Create third file named ‘ProductDetailsViewController.cs’ through Add->New File->iOS->View Controller.
4) We will not use the xib files, you can just delete them.

3.2 UI Design with Xcode

Right click on ‘Main.storyboard’, Open With -> Xcode Interface Builder. Notice that all the files from ‘GameStoreXamarin.iOS’ project are showing in Xcode, including the files we just created in Visual Studio. image
Create two navigation controllers, one table view controller to display product list and one view controller to display product details. And bind view controller classes we created in Visual Studio to these Scenes. image
Connect the controls(label, textfield, button, etc) on canvas to code manually. image
Save the storyboard before closing it, return to Visual Studio.

3.3 Views in Visual Studio

When you add controls(label, textfield, button, etc) to the storyboard in Xcode, some changes will be made to the View Controller classes. For example, in file ‘ProductTableViewCell.designer.cs’, you see two labels and one image view, they all have the Outlet attribute. They are generated automatically by Visual Studio. You should never manually change the content inside a ‘designer.cs’ file.

// WARNING
//
// This file has been generated automatically by Visual Studio from the outlets and
// actions declared in your storyboard file.
// Manual changes to this file will not be maintained.
//
using Foundation;
using System;
using System.CodeDom.Compiler;

namespace GameStoreXamarin.iOS
{
    [Register ("ProductTableViewCell")]
    partial class ProductTableViewCell
    {
        [Outlet]
        UIKit.UILabel lblName { get; set; }
        [Outlet]
        UIKit.UILabel lblPrice { get; set; }
        [Outlet]
        UIKit.UIImageView imgPhoto { get; set; }

        void ReleaseDesignerOutlets ()
        {
        }
    }
}

3.4 Installing Packages

Project->Add NuGet Packages, search and install two packages, ‘sqlite-net-pcl’ and ‘Xamarin.Forms’. Finally, the package.config looks like below.

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="sqlite-net-pcl" version="1.4.118" targetFramework="xamarinios10" />
  <package id="SQLitePCLRaw.bundle_green" version="1.1.9" targetFramework="xamarinios10" />
  <package id="SQLitePCLRaw.core" version="1.1.9" targetFramework="xamarinios10" />
  <package id="SQLitePCLRaw.provider.sqlite3.ios_unified" version="1.1.9" targetFramework="xamarinios10" />
  <package id="Xamarin.Forms" version="2.5.0.91635" targetFramework="xamarinios10" />
</packages>

3.5 Location of Database File

Create a file named ‘FileHelper.cs’. Class FileHelper inherits the IFileHelper interface. Register implementation to DependencyService with a metadata attribute ‘[assembly: Dependency(xxx)]’.

using System;
using System.IO;
using Xamarin.Forms;
using GameStoreXamarin.Core;

[assembly: Dependency(typeof(GameStoreXamarin.iOS.FileHelper))]
namespace GameStoreXamarin.iOS
{
    public class FileHelper : IFileHelper
    {
        public string GetLocalFilePath(string filename)
        {
            string docFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            string libFolder = Path.Combine(docFolder, "..", "Library", "Databases");

            if (!Directory.Exists(libFolder))
            {
                Directory.CreateDirectory(libFolder);
            }

            return Path.Combine(libFolder, filename);
        }
    }
}

3.6 Image Handling

Create a file named ‘ImageHelper.cs’. Define two methods to convert image to byte array and vice versa.

using System;
using Foundation;
using UIKit;

namespace GameStoreXamarin.iOS.Helper
{
    public static class ImageHelper
    {
        /// Convert byte array to UIImage
        public static UIImage BytesToUIImage(byte[] imageBytes)
        {
            var data = NSData.FromArray(imageBytes);
            return UIImage.LoadFromData(data);
        }

        /// Convert UIImage to byte array
        public static byte[] UIImageToBytes(UIImage image)
        {
            byte[] imageBytes;
            using (NSData imageData = image.AsPNG())
            {
                imageBytes = new Byte[imageData.Length];
                System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, imageBytes, 0, Convert.ToInt32(imageData.Length));
            }

            return imageBytes;
        }
    }
}

In the detail view, we need to enable user to select photos from gallery when he/she taps the image control(named ‘imgPhoto’). In ‘ProductDetailsViewController.cs’, add GestureRecognizer for ‘imgPhoto’ control.

// Tap gesture
imgPhoto.UserInteractionEnabled = true;
imgPhoto.AddGestureRecognizer(new UITapGestureRecognizer(tap =>
{
    txtName.ResignFirstResponder();
    txtPrice.ResignFirstResponder();

    // UIImagePickerController is a view controller that lets a user pick media from their photo library.
    _imagePicker = new UIImagePickerController();

    // Only allow photos to be picked, not taken.
    _imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary;
    _imagePicker.MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary);

    // Events
    _imagePicker.FinishedPickingMedia += Handle_FinishedPickingMedia;
    _imagePicker.Canceled += Handle_Canceled;

    // Show Image Picker
    NavigationController.PresentModalViewController(_imagePicker, true);
})
{
    NumberOfTapsRequired = 1 // Signle tap
});

Add event to handle the scenario when user selects one image, that is, load the image to image view control.

protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
    // determine what was selected, video or image
    bool isImage = false;
    switch (e.Info[UIImagePickerController.MediaType].ToString())
    {
        case "public.image":
            isImage = true;
            break;
        case "public.video":
            break;
    }

    if (isImage)
    {
        UIImage originalImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
        if (originalImage != null)
        {
            imgPhoto.Image = originalImage;
            _imageChanged = true;
        }
    }
    // dismiss the picker
    _imagePicker.DismissModalViewController(true);
}

Add another event to handle the scenario that user clicks Cancel button, that is, do nothing but dismiss the image picker control.

protected void Handle_Canceled(object sender, EventArgs e)
{
    _imagePicker.DismissModalViewController(true);
}

3.8 Adding Images

Add three images to assets.xcassets. These images are used as product’s photo. image

3.9 Dummy Data

Create three products with images in assets. If there is no data in SQLite database, call this method get initial products.

private void CreateDummyData()
{
    Product product = new Product();
    product.ProductName = "Xbox 360";
    product.Price = 299.00;
    product.Image = ImageHelper.UIImageToBytes(UIImage.FromBundle("xbox360"));
    DatabaseHelper.Database.SaveProduct(product);

    product = new Product();
    product.ProductName = "Wii";
    product.Price = 269.00;
    product.Image = ImageHelper.UIImageToBytes(UIImage.FromBundle("wii"));
    DatabaseHelper.Database.SaveProduct(product);

    product = new Product();
    product.ProductName = "Wireless Controller";
    product.Price = 19.99;
    product.Image = ImageHelper.UIImageToBytes(UIImage.FromBundle("controller"));
    DatabaseHelper.Database.SaveProduct(product);

    _productList = DatabaseHelper.Database.GetProducts();
}

3.10 Customizing Table Cell

In the list view, we can customize the display style for each row. You can set the position, font, color, etc.

using System;
using CoreGraphics;
using Foundation;
using UIKit;

namespace GameStoreXamarin.iOS
{
    public class CustomProductTableViewCell : UITableViewCell
    {
        UILabel headingLabel, subheadingLabel;
        UIImageView imageView;
        public CustomProductTableViewCell(NSString cellId) : base(UITableViewCellStyle.Default, cellId)
        {
            SelectionStyle = UITableViewCellSelectionStyle.Gray;
            //ContentView.BackgroundColor = UIColor.FromRGB(218, 255, 127);
            imageView = new UIImageView();
            headingLabel = new UILabel()
            {
                //Font = UIFont.FromName("Cochin-BoldItalic", 22f),
                //TextColor = UIColor.FromRGB(127, 51, 0),
                BackgroundColor = UIColor.Clear
            };
            subheadingLabel = new UILabel()
            {
                //Font = UIFont.FromName("AmericanTypewriter", 12f),
                //TextColor = UIColor.FromRGB(38, 127, 0),
                //TextAlignment = UITextAlignment.Center,
                BackgroundColor = UIColor.Clear
            };
            ContentView.AddSubviews(new UIView[] { headingLabel, subheadingLabel, imageView });

        }
        public void UpdateCell(string name, string price, UIImage image)
        {
            imageView.Image = image;
            headingLabel.Text = name;
            subheadingLabel.Text = "$" + price;
        }
        public override void LayoutSubviews()
        {
            base.LayoutSubviews();
            imageView.Frame = new CGRect(0.0, 0.0, 90, 90);
            headingLabel.Frame = new CGRect(98, 4, 268, 21);
            subheadingLabel.Frame = new CGRect(98, 43, 268, 21);
        }
    }
}

3.11 List View

When app is started, products should be displayed in the list view. Call the Database method defined in Core project to get data from SQLite. If there is no data, create three products locally.

public override void ViewDidLoad()
{
    base.ViewDidLoad();
    Xamarin.Forms.Forms.Init();

    NavigationItem.LeftBarButtonItem = EditButtonItem;

    _productList = DatabaseHelper.Database.GetProducts();
    if (_productList == null || _productList.Count == 0) {
        CreateDummyData();
    }
}

Override the NumberOfSections,RowsInSection,GetCell methods of UITableViewController to display the data from ‘_productList’.

public override nint NumberOfSections(UITableView tableView) {
    return 1;
}

public override nint RowsInSection(UITableView tableView, nint section)
{
    return _productList.Count;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {

    // Table view cells are reused and should be dequeued using a cell identifier.
    var cellIdentifier = "ProductTableViewCell";

    var cell = tableView.DequeueReusableCell(cellIdentifier) as CustomProductTableViewCell;

    if (cell == null)
    {
        cell = new CustomProductTableViewCell(NSString.FromData(cellIdentifier, NSStringEncoding.UTF8));
    }

    // Fetches the appropriate product for the data source layout.
    var product = _productList[indexPath.Row];
    cell.UpdateCell(product.ProductName, Convert.ToString(product.Price), ImageHelper.BytesToUIImage(product.Image));

    return cell;
}

3.11 Navigation from List View to Detail View

First, you need to add Segue from table view cell to Navigation controller in storyboard. Then, override RowSelected and PrepareForSegue methods.

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
    this.PerformSegue("ShowDetail", indexPath); // pass indexPath as sender
}

public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
    base.PrepareForSegue(segue, sender);

    switch (segue.Identifier)
    {
        case "AddItem":
            break;
        case "ShowDetail":
            var pdvc = segue.DestinationViewController as ProductDetailsViewController;
            if (pdvc != null) {
                var indexPath = sender as NSIndexPath;
                if (indexPath != null)
                {
                    var selectedProduct = _productList[indexPath.Row];
                    pdvc._product = selectedProduct;
                }
            }
            break;
        default:
            break;
    }        
}

3.12 Navigation from Detail View to List View

First, define an action method in ‘ProductTableViewController.cs’.

[Action("UnwindToProductTableViewController:")]
public void UnwindToProductTableViewController(UIStoryboardSegue segue)
{
    var sourceViewController = segue.SourceViewController as ProductDetailsViewController;
    if (sourceViewController != null) {
        var product = sourceViewController._product;
        if (product != null) {
            var selectedIndexPath = TableView.IndexPathForSelectedRow;
            if (selectedIndexPath != null)  {
                // Update an existing product.
                _productList[selectedIndexPath.Row] = product;
            }
            else {
                // Add a new product.
                _productList.Add(product);
            }
            TableView.ReloadData();
            DatabaseHelper.Database.SaveProduct(product);
        }
    }
}

Second, unwind segue to Exit for the Cancel button, select the action method defined in list view. image
When tapping the Cancel button in the detail view, screen will return to the list view. For the Save button, we need to do more. Override the ‘PrepareForSegue’ method, save the change.

public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
    base.PrepareForSegue(segue, sender);

    var button = sender as UIBarButtonItem;
    if (button != null) {
        if (_product == null)
        {
            _product = new Product();
        }
        _product.ProductName = txtName.Text;
        _product.Price = Convert.ToDouble(txtPrice.Text);
        _product.Image = ImageHelper.UIImageToBytes(imgPhoto.Image);
    }
}

Manually perform the Segue in the Save event.

this.PerformSegue("UnwindSave", btnSave);

3.12 Final Project Structure

The final structure of the iOS project. image

4. Testing

In Visual Studio, click the arrow button(or Run->Start Without Debugging) to run the app in iOS Simulator. image
Product list. image
Edit product. image
Select photo. image
Delete product. image
In landscape view. image

5. Source Files

6. References