Hello, Mac in F#.

DuaneCDuaneC CAMember ✭✭
edited July 2016 in Xamarin.iOS

There don't seem to be too many (any?) examples of Mac apps written in F#. I recently responded to a forum question, describing how to use Interface Builder for the UI design in C#, and then use the C# code, as a library, in an F# Mac app. Here's an example of an F# Mac app, without using (too much) C# code. It's the F# code for the Xamarin 'Hello, Mac' guide.

Create a new Xamarin Solution, using the F# Cocoa App template. Follow along in the Hello, Mac guide to design the UI in Interface Builder, up to, but not including, the Outlets and Actions section. Although the 'Main.storyboard' file changes made in Interface Builder will be synchronized back to the F# app, at the time of writing, in the stable channel, there is no synchronization, or infrastructure, for Outlets and Actions in the F# Cocoa App template.

To add Outlets and Actions in Interface Builder, we'll need to manually add the ViewController.m and ViewController.h files which are automatically added in the shim Xcode Project for C# Mac apps. The easiest way to do this is to create a C# Mac App and open the Main.storyboard file in Interface Builder. This will create the shim Xcode Project in the 'obj' folder of the Xamarin Studio C# Mac App. These files disappear when Xcode is closed, so copy the 'ViewController.h' and 'ViewController.m' files while Interface Builder is open, and save them somewhere else, outside of the 'obj/Xcode' folder, so that you can retrieve them later. Open back up the F# Mac app, open 'Main.storyboard' in Interface Builder, and add the 'ViewController.h' and 'ViewController.m' files that you've just copied.

Alternatively, the code for each file is below. You can just copy and paste it into empty class files in your Interface Builder project.

ViewController.h

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>


@interface ViewController : NSViewController {
}

@end

ViewController.m

#import "ViewController.h"

@implementation ViewController

@end 

Proceed with the Hello, Mac guide to add the Outlet and Action, linking them to the same location described in the guide in the 'ViewController.h' file that we've added to the Xcode project.

Switch back to Xamarin Studio. Changes made to 'Main.storyboard' in Interface Builder, including the Outlet and Action connections, should have been synchronized in the Xamarin project.

Add a new F# source file to the project called 'ViewController.fs'. The code for this file, which translate the C# code looks like the following.

namespace [Project Namespace]
open System
open AppKit
open Foundation

[<Register("ViewController")>]
type ViewController(handle:nativeint) =      // type ViewController(handle:IntPtr) is also fine
    inherit NSViewController(handle)

    let mutable numberOfTimesClicked = 0

    [<Outlet>]
    member val ClickedLabel = new NSTextField() with get, set

    [<Action("ClickedButton:")>]
    member this.ClickedButton (sender:NSObject) =
        numberOfTimesClicked <- numberOfTimesClicked + 1
        if numberOfTimesClicked < 2 then
            this.ClickedLabel.StringValue <- String.Format("The button has been clicked {0} time", numberOfTimesClicked)
        else
            this.ClickedLabel.StringValue <- String.Format("The button has been clicked {0} times", numberOfTimesClicked)


    override this.ViewDidLoad() =
        base.ViewDidLoad()

        this.ClickedLabel.StringValue <- "Button has not been clicked yet."

The colon in the Action attribute selector is significant, as it specifies the (single) number of arguments in the corresponding method. The new 'ViewController.fs' file will need to be moved up, in the Solution explorer, so that 'Main.fs' comes last in the order.

Alternatively, you may wish to add the UI controls programmatically, instead of using Interface Builder. Here is some code that does that.

namespace [Project namespace]
open System
open AppKit
open Foundation
open CoreGraphics
open ObjCRuntime

[<Register("ViewController")>]
type ViewController(handle:nativeint) =
    inherit NSViewController(handle)

    let mutable numberOfTimesClicked = 0

    let mutable myLabel = new NSTextField ( Frame = new CGRect ( 140.0, 216.0, 322.0, 25.0),
                                    StringValue = "Label",
                                    Editable = false,
                                    Bezeled = false,
                                    DrawsBackground = false,
                                    Selectable = false
                                    )

    let myButton = new NSButton (Frame = new CGRect (23.0, 216.0, 100.0, 32.0),
                                Title = "Click me",
                                BezelStyle = NSBezelStyle.Rounded,
                                Action = new Selector ("ClickedButton:")
                                )


    [<Outlet>]
    member this.ClickedLabel with get() = myLabel
                             and set value = myLabel <- value


    [<Action("ClickedButton:")>]
    member this.ClickedButton (sender:NSObject) =
        numberOfTimesClicked <- numberOfTimesClicked + 1
        if numberOfTimesClicked < 2 then
            this.ClickedLabel.StringValue <- String.Format("The button has been clicked {0} time", numberOfTimesClicked)
        else
            this.ClickedLabel.StringValue <- String.Format("The button has been clicked {0} times", numberOfTimesClicked)


    override this.ViewDidLoad() =
        base.ViewDidLoad()

        this.View.AddSubview (myButton)
        this.View.AddSubview (myLabel)
        this.ClickedLabel.StringValue <- "Button has not been clicked yet."


    override this.RepresentedObject with get () = base.RepresentedObject
                                    and set value = base.RepresentedObject <- value

Hope this is helpful.

Cheers,
Duane

Tagged:

Posts

  • DuaneCDuaneC CAMember ✭✭
    edited December 1

    These days, I would forget about manually adding stub files, as I originally posted. Instead, to do the Hello, Mac starter project in F#, I would also create a new C# Xamarin.Mac project, removing its Main.storyboard file, and adding a link to the Main.storyboard file in the F# Hello, Mac starter project. Then, I would do all of the Interface Builder steps in the C# project, to take advantage of the ability to add Outlets and Actions in Xcode for C# projects.

    You no longer need to call the base method in the override of ViewDidLoad(). The simpler code for the ViewController.fs file, for Hello, Mac is:

    namespace Hello_Mac
    
    open AppKit
    open Foundation
    
    [<Register ("ViewController")>]  // Use the value of the Main.storyboard's ViewController's customClass attribute.
    type ViewController(handle:nativeint) =
        inherit NSViewController(handle)
    
        let mutable numberOfTimesClicked = 0
    
        [<Outlet>]
        member val ClickedLabel = new NSTextField() with get, set
    
        [<Action("ClickedButton:")>]
        member this.ClickedButton(sender:NSObject) =
            numberOfTimesClicked <- numberOfTimesClicked + 1
            this.ClickedLabel.StringValue <- sprintf "The button has been clicked %i time%s." numberOfTimesClicked (if numberOfTimesClicked < 2 then "" else "s")
    
        override this.ViewDidLoad() =
    
            this.ClickedLabel.StringValue <- "Button has not been clicked yet."
    
Sign In or Register to comment.