Blog/Software Development

How to Create a Simple macOS Cocoa Application Using C# and Xamarin.Mac

How to Create a Simple macOS Cocoa Application Using C# and Xamarin.Mac

Visual Studio is a development environment from Microsoft that also has a version for macOS. To use the macOS version of Visual Studio, creativity and adaptability are essential. While you can use Visual Studio 2022 on Windows to build console or ASP.NET applications for macOS, Visual Studio for Mac is better suited if you want to build macOS native apps.

For .NET developers, who are used to developing applications exclusively for Windows, it might come as a surprise that Xamarin.Mac makes it possible for them to develop fully native macOS applications with C#.

In this blog post, we will show you how you can build a simple macOS Cocoa application using C# and Xamarin.Mac. Let’s jump right in.

Starting a project

When learning how to code in a new environment, it can be useful to learn from the experience of others. For this reason, we recommend trying to recreate existing applications. There are numerous applications that developers have created time and time again, offering valuable insights into the development process and different solutions to the same problem.

Starting a new project in Visual Studio for Mac is no different from starting a regular development project in any other development environment and can be done in just a few steps.

For this simple development project, you will need the following:

  1. Macbook running macOS Monterey, Version 12.6;
  2. Visual Studio Community 2022 For Mac, Version 17.3.8 (build 5);
  3. Xcode, Version 13.3 (13E113).

Step 1:

To begin, open Visual Studio and create a Cocoa App* with C#.

Screenshot of Visual Studio showing the option to create a Cocoa App* with C#

*Cocoa is a set of Apple's native frameworks.

Step 2:

Choose the name for the app and target macOS version.

Screenshot showing how to choose the name for the app and target macOS version

Step 3:

Configure solution names, location and version control option.

Screenshot showing how to configure solution names, location and version control

A project is represented by a file (.csproj for C# projects) that contains xml to define the file and folder hierarchy, file paths, and project-specific configurations, like build settings.

If the project contains all the files (source code, images, data files, etc.) that are necessary to compile the executable, then the “solution” is a container that groups together one or more related projects. Despite its name, the solution does not mean “the answer”. Instead, it stores all the Visual Studio window settings and miscellaneous files that do not relate to any particular project.

Solutions are described by a text file (extension .sln) with its own unique format.

An empty application window

Visual Studio creates a Xamarin.Mac application and some default files for you, so we can just build and run the project at the very start of development. It will be an empty application window for now, but not for long.

Building GUI

In order to be able to build a GUI (Graphical User Interface), we will have to switch back and forth between Visual Studio and Xcode, as Visual Studio is used for behavior and Xcode for visuals. This means that we will design the application`s graphical interface in Xcode. Designing a GUI in Xcode Interface Builder is very similar to the one in Windows Forms (the one available for Windows) where you drag and drop controls and objects to the desired positions. You can basically use any control from Apple’s AppKit framework that handles not only objects, but also manages the events and interactions for the macOS application.

To get started with the Xcode Interface Builder, in the Solution Explorer, right-click on the Main.storyboard file > Open With > Xcode Interface Builder.

Xcode Interface Builder

The Xcode Interface Builder allows Cocoa developers to create interfaces for applications using a graphical user interface. We use the Library section for finding controls and objects to place into the designer to graphically build the user interface. The library can be accessed via a toolbar button “+”, the View > Show Library, or the ⇧⌘L keyboard shortcut.

In the scope of this project, we will build a calculator desktop application, as calculators encourage the use of multiple strategies and conceptual understanding. Therefore, creating this simple app would be a great way to learn and understand the basics.

Let’s start building:

  1. Drag a Push Button from the Library Section;
  2. Drop the button onto the View (under the Window Controller) in the Interface Editor;
  3. Click on the Title property in the Attribute Inspector and change the button's title to “1”;
  4. Repeat for remaining buttons, changing Title properties to meet logical needs;
  5. Drag and drop a TextField from the Library Section just above the buttons;
  6. Click on the Title property in the Attribute Inspector and change the text field's title to “0”;
  7. Drag and drop a Label from the Library Section just above the TextField.
Xcode Interface Builde

As Xcode Interface Builder offers a lot of properties, elements and collections, you are free to experiment and get original with your design.

Actions and outlets

In order to add some actions to our newly-created button, we should rearrange Xcode windows so that the ViewController.h (ViewController header) file is open as well. To do this, go to: Xcode Menu > Navigate > Open in Next Editor, or the ⌥⌘, keyboard shortcut.

ViewController.h is where we will continue our drag-and-dropping. To create a basic desktop app, in our case a calculator, we will need Buttons for actions, one TextField and one Label for outlets.

If we wire up an object to an Outlet, it will be exposed to the code via property and can call methods on it, for instance. When an Action is performed on a control, it will automatically call a method in the code. It is possible to wire up many controls to one Action.

To establish a connection you should choose the desired object, hold down the Control (⌃) button on the keyboard and drag the object to the ViewController header file (which in our example we have opened in the editor to the right) just below the @interface ViewController : NSViewController {} code:.

For our calculator, we can choose multiple buttons to respond to the same Action. For example, we can define all numeric buttons (0, 1, 2…9) as one Action – we just drag and drop the first button and define what connection we are creating and its name in the connection popup: Action or Outlet. After that, we can drag and drop our objects to the same defined connection in the header file to Connect Action.

Action
Action
Outlet
Outlet

With an understanding of what Actions and Outlets are, we will expose Label and TextField as Outlets.

If we go back to the Visual Studio, in the ViewController.designer.cs file, there is auto-generated code that represents the link between C# and GUI, as the ViewController.h file mirrors the ViewController.designer.cs file.

For example, in the ViewController.designer.cs file for the Numbers_Only action—which we previously created in Xcode—an access code is created, as Visual Studio automatically updates the file once controls are added to the app's view. These should not be updated directly.

ViewController.designer.cs file for the Numbers_Only action

Now we can go to the ViewController.cs file in Visual Studio and expand it, giving the desired behavior to our action and buttons in question, respectively:

ViewController.cs file in Visual Studio

Creating the final application

Creating a calculator will help you learn about event handling or Actions and Outlets. A calculator contains numbers and mathematical operations. Multiple UI elements, including TextField, which is located at the top and outputs the answer and also prevents pasting or typing non-numeric characters. Label displays your input. When the user clicks on a button, you can keep updating a string that stores the ongoing mathematical equation.

The main difference between Visual Studio versions for Windows and macOS is that for Windows you can use WinForms or WPF for desktop application development, while for macOS you would use Cocoa to keep your application native to the environment you are working with. Another thing to keep in mind is that frameworks that are built for Windows are not compatible with macOS.

Additionally, in comparison to Visual Studio for Windows, in Visual Studio for Mac you can parse keyboard key equivalent in the button’s properties as Key Equivalent without using the KeyEventArgs class in code. It specifies the key the user presses or the character that was composed as a result of each key press.

This is what our final desktop application—a calculator—would look like:

And here is the corresponding C# code:

using System;
using System.Linq;
using AppKit;
using CloudKit;
using CoreServices;
using Foundation;
using GameController;

namespace MyFirstMacApp
{
	public partial class ViewController : NSViewController
	{
		public ViewController (IntPtr handle) : base (handle)
		{
        }
        Double result = 0;
        String operation = "";
        bool enter_value = false;

        public override void ViewDidLoad ()
		{
			base.ViewDidLoad ();

            TxtDisplay.Formatter = new NumberOnlyFormatter();
            View.AddSubview(TxtDisplay);
        }

		public override NSObject RepresentedObject {
			get {
				return base.RepresentedObject;
			}
			set {
				base.RepresentedObject = value;
			}
		}

        //not accepting non-numeric characters
        class NumberOnlyFormatter : NSNumberFormatter
        {
            public override bool IsPartialStringValid(string partialString, out string newString, out NSString error)
            {
                newString = partialString;
                error = new NSString("");
                if (partialString.Length == 0)
                    return true;
                if (partialString.All(char.IsDigit))
                    return true;
                newString = new string(partialString.Where(char.IsDigit).ToArray());
                return false;
            }
        }

        partial void Numbers_Only(AppKit.NSButton sender)
        {
            if ((TxtDisplay.StringValue == "0") || (enter_value))
                TxtDisplay.StringValue = "";
            enter_value = false;

            if (sender.StringValue == ".") 
            {
                if (!TxtDisplay.StringValue.Contains("."))
                    TxtDisplay.StringValue = TxtDisplay.StringValue + sender.Title;
            }
            else
            {
                TxtDisplay.StringValue = TxtDisplay.StringValue + sender.Title;
            }
        }

        partial void Operators_Click(AppKit.NSButton sender)
        {
            if (result != 0)
            {
                BtnEquals_Click(sender);
                enter_value = true;
                operation = sender.Title;
                LblShowOps.StringValue = System.Convert.ToString(result) + "   " + operation;
            }
            else
            {
                operation = sender.Title;
                result = Double.Parse(TxtDisplay.StringValue);
                TxtDisplay.StringValue = "";
                LblShowOps.StringValue = System.Convert.ToString(result) + "   " + operation;
            }
        }
        

        partial void BtnEquals_Click(AppKit.NSButton sender)
        {
            LblShowOps.StringValue = "";
            switch (operation)
            {
                case "+":
                        TxtDisplay.StringValue = (result + Double.Parse(TxtDisplay.StringValue)).ToString();
                        break;
                case "-":
                    TxtDisplay.StringValue = (result - Double.Parse(TxtDisplay.StringValue)).ToString();
                    break;
                case "*":
                    TxtDisplay.StringValue = (result * Double.Parse(TxtDisplay.StringValue)).ToString();
                    break;
                case "%":
                    TxtDisplay.StringValue = (result % Double.Parse(TxtDisplay.StringValue)).ToString();
                    break;
                case "/":
                    if (Double.Parse(TxtDisplay.StringValue) == 0)
                    {
                        LblShowOps.StringValue = "cannot divide by zero";
                        TxtDisplay.StringValue = "0";
                        result = 0;
                        return;
                    }
                    TxtDisplay.StringValue = (result / Double.Parse(TxtDisplay.StringValue)).ToString();
                    break;
                default:
                    break;
            }
            result = Double.Parse(TxtDisplay.StringValue);
            operation = "";
        }

        partial void BtnBackspace_Click(AppKit.NSButton sender)
        {
            if (TxtDisplay.StringValue.Length > 0)
            {
                TxtDisplay.StringValue = TxtDisplay.StringValue.Remove(TxtDisplay.StringValue.Length - 1, 1);
            }
            if (TxtDisplay.StringValue == "")
            {
                TxtDisplay.StringValue = "0";
            }
        }

        partial void C_Click(AppKit.NSButton sender)
        {
            TxtDisplay.StringValue = "0";
        }

        partial void CE_Click(AppKit.NSButton sender)
        {
            TxtDisplay.StringValue = "0";
            result = 0;
        }

        partial void OneOf_Click(AppKit.NSButton sender)
        {
            Double a = Convert.ToDouble(1.0 / Convert.ToDouble(TxtDisplay.StringValue));
            TxtDisplay.StringValue = System.Convert.ToString(a);
        }

        partial void PlusMinus_Click(AppKit.NSButton sender)
        {
            TxtDisplay.StringValue = (Double.Parse(TxtDisplay.StringValue) * -1).ToString();
        }

        partial void SqrRoot_Click(AppKit.NSButton sender)
        {
            double sq = Double.Parse(TxtDisplay.StringValue);
            LblShowOps.StringValue = System.Convert.ToString(TxtDisplay.StringValue);
            sq = Math.Sqrt(sq);
            TxtDisplay.StringValue = System.Convert.ToString(sq);
        }

        partial void Double_Click(AppKit.NSButton sender)
        {
            Double a = Convert.ToDouble(TxtDisplay.StringValue) * Convert.ToDouble(TxtDisplay.StringValue);
            TxtDisplay.StringValue = System.Convert.ToString(a);
        }
    }
}

Conclusion

In this blog post we have demonstrated how to create a very simple macOS Cocoa application using C# and Xamarin.Mac, in our case a calculator. Of course, applications and scenarios can be far more complex and in this example we aimed to cover only the basics to give you a common understanding of the main process.

There are many simple apps that you can recreate while learning all the capabilities of the Xcode Interface Builder and how a Cocoa application works. You can think of various apps, like converters, calculators, file managers, or games. You can start creating these apps from scratch using a new Cocoa app project. Follow our instructions and build your own calculator application—or use it as inspiration to build something else.

Do you need help building or testing your application? Contact us with your project details to learn more about how we can help you and your team.

QA engineer having a video call with 5-start rating graphic displayed above

Deliver a product made to impress

Build a product that stands out by implementing best software QA practices.

Get started today