left-icon

Writing Native Mobile Apps in a Functional Language Succinctly®
by Vassili Kaplan

Previous
Chapter

of
A
A
A

CHAPTER 2

Project Structure and Design

Project Structure and Design


“Design is how it works.”

Steve Jobs

Structure of a CSCS mobile project

Figure 2 contains a screenshot of a common Xamarin project using CSCS.

Scripting.Shared is a shared project containing the code common to both Android and iOS. In particular, it contains all the files needed for parsing the CSCS code. Parsing.cs contains the implementation of the split-and-merge algorithm.

ParserFunction.cscs contains the implementation of the base class ParserFunction. When adding a new functionality to CSCS, a new class will override the ParserFunction class (see Code Listing 2 in the previous chapter).

Functions.Flow.cs, Functions.Math.cs, and Functions.OS.cs contain the implementation of the basic functions, not related to the mobile devices (operations with strings, loops, exceptions, mathematical functions, etc.). Interpreter.cs is a class that glues together parsing functions with the parsing flow.

The Shared.Resources folder contains resources used by both platforms. In particular, it contains the CSCS scripts—I placed them directly under the Resources folder. For example, the code mentioned in Code Listing 1 is located in the Resources\fhello.cscs file. The Android and iOS projects have links to this file.

The drawable subfolder contains all the images used by both platforms. Each particular project, iOS, or Android, has its own Resources folder with links to the shared projects—therefore, changes in the shared folder will be automatically propagated to the iOS and Android folders.

You can also set up the project structure differently, for example, with each project having its own set of images of different sizes, depending on the screen size.

The functions common to iOS and Android CSCS are in the CommonFunctions.cs file in the shared project folder.

If iOS and Android code is different, the implementations are in the iOSFunctrions.cs file for iOS and in the DroidFunctions.cs file for Android, respectively.

If the code is different on both platforms, but the differences are minimal, you can still have it in the CommonFunctions.cs file using the preprocessor macros. See Code Listing 3 for an example of extracting the version number for an Android and an iOS device.

Code Listing 3: Implementation of the GetVersionNumberFunction Class

class GetVersionNumberFunctionParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
#if __ANDROID__
    string strVersion = Android.OS.Build.VERSION.Release;
#elif __IOS__
    string strVersion = UIKit.UIDevice.CurrentDevice.SystemVersion;
#endif
    return new Variable(strVersion);
  }
}

General Structure of a CSCS Xamarin Project (On macOS)

Figure 2: General Structure of a CSCS Xamarin Project (On macOS)

Where CSCS code executes

The actual execution of the CSCS code happens inside of the CommonFunctions.RunScript() method shown in Code Listing 4. This method also registers all the common functions with the parser.

Code Listing 4: The Implementation of the RunScript Method

public static void RunScript(string fileName)
{
  RegisterFunctions();

  string script = FileToString(fileName);

  Variable result = null;
  try {
    result = Interpreter.Instance.Process(script);
  } catch (Exception exc) {
    Console.WriteLine("Exception: " + exc.Message);
    Console.WriteLine(exc.StackTrace);
    ParserFunction.InvalidateStacksAfterLevel(0);
    throw;
  }
}

public static void RegisterFunctions()
{
  ParserFunction.RegisterFunction("GetLocation",

                                   new GetLocationFunction());
  ParserFunction.RegisterFunction("AddWidget"new AddWidgetFunction());
  // ... all other functions ...  

}

CommonFunctions.RunScript() is called from CustomInit.InitAndRunScript(), which initializes project-specific data and registers all project-specific widget types and all custom functions with the parser. See Code Listing 5 for an example of such a method.

Code Listing 5: The Implementation of the CustomInit.InitAndRunScript Method

public static void InitAndRunScript()
{
  UIVariable.WidgetTypes.Add(new AdMob());
  UIVariable.WidgetTypes.Add(new SfWidget());

  string fileName = "start.cscs";


  // All custom functions go here ...
  ParserFunction.RegisterFunction("SfPdfNew"new CreatePdf());
  ParserFunction.RegisterFunction("SfPdfOpen"new OpenPdf());
  ParserFunction.RegisterFunction("SfSetPdfText"new SetPdfText());
  ParserFunction.RegisterFunction("SfSetPdfImage"new SetPdfImage());
  ParserFunction.RegisterFunction("SfSetPdfLine"new SetPdfLine());
  ParserFunction.RegisterFunction("SfSetPdfPie"new SetPdfPie());
  ParserFunction.RegisterFunction("SfSetPdfFont"new SetPdfFont());
  ParserFunction.RegisterFunction("SfSavePdf"new SavePdf());

  // ...

  CommonFunctions.RunScript(fileName);
}

In particular, it specifies that the start.cscs script will run. Note that you can import other CSCS script files by calling the ImportFile() CSCS function from start.cscs, for example:

ImportFile("sfhello.cscs");

Tip: In this way you can have different apps with identical C# code but different CSCS code. You just specify a different name when calling the ImportFile() function in start.cscs. There are some examples of this in the accompanying source code.

Where in the workflow do we execute the CSCS script? The answer is different for iOS and Android.

For Android, it’s a bit trickier than for iOS: we have to execute the script after the global layout has been initialized so that the script not only can start adding widgets, but also has access to all of the layout information, like sizes, orientation, main window parameters, and so on.

Unfortunately, it’s too early to run the CSCS script at the end of the MainActivity.OnCreate() method, but fortunately, Android provides a way: we can register a Global Layout listener with a ViewTreeObserver object.

We register it not at the end of the MainActivity.OnCreate() method, but at the end of the MainActivity.OnResume() method, which is the best place according to the Android activity lifecycle. See

Code Listing 6 for details.

Code Listing 6: Running a CSCS Script on Android

// MainActivity

bool m_scriptRun = false;


protected override void OnResume()
{
  base.OnResume();
  if (!m_scriptRun) {
    ViewTreeObserver vto = TheLayout.ViewTreeObserver;
    vto.AddOnGlobalLayoutListener(new LayoutListener());
    m_scriptRun = true;
  }
}

public class LayoutListener : Java.Lang.Object,

                              ViewTreeObserver.IOnGlobalLayoutListener
{
  public void OnGlobalLayout()
  {
    var vto = MainActivity.TheLayout.ViewTreeObserver;
    vto.RemoveOnGlobalLayoutListener(this);

    // Run CSCS Script here:
    CustomInit.InitAndRunScript();
  }
}

We use the Boolean variable m_scriptRun to make sure that the CSCS script is executed only once, because we do not need to run the script every time the device orientation changes. In this case, we run only specific functions responsible for widget placement—we’ll see how to do that in the next chapter.

Note: Remove the m_scriptRun Boolean check if you want the script to run every time the device orientation changes or the application comes back from the background.

The situation is simpler in iOS—we can execute the CSCS script from the AppDelegate.FinishedLaunching() method.

In iOS, I created a custom view controller, with the method Run(), which actually runs the CSCS script: see Code Listing 7.

Code Listing 7: Running CSCS Script on iOS

public void Run()
{
  // If there is no TabBar, move the tabs view down:
  OffsetTabBar();
  this.ViewControllerSelected += OnTabSelected;

  CustomInit.InitAndRunScript();

  if (m_selectedTab >= 0) {
    SelectTab(m_selectedTab);
  } else if (TabBar != null) {
    TabBar.Hidden = true;
  }
}

The base class for all of the widgets is UIVariable, which derives from the core CSCS variable class. It has a common functionality for iOS and for Android. A fragment of this class is described in

Code Listing 8.

Code Listing 8: A Fragment of the UIVariable Class

public class UIVariable : Variable
{
  public Action<stringstring> ActionDelegate;
  public static List<UIVariable> WidgetTypes = new List<UIVariable>();
  protected static int m_currentTag;

  public enum UIType {
    NONE, LOCATION, VIEW, BUTTON, LABEL, TEXT_FIELD, TEXT_VIEW,

    PICKER_VIEW, PICKER_IMAGES,LIST_VIEW, COMBOBOX, IMAGE_VIEW,

    SWITCH, SLIDER, STEPPER, SEGMENTED, CUSTOM
  };

  public UIVariable(UIType type, string name = "",
                    UIVariable refViewX = null,

                    UIVariable refViewY = null)
  {
    WidgetType = type;
    WidgetName = name;
    RefViewX   = refViewX;
    RefViewY   = refViewY;
  }


  public UIType WidgetType     { getset; }
  public string WidgetName     { getset; }
  public int Width             { getset; }
  public int Height            { getset; }
  public int X                 { getset; }
  public int Y                 { getset; }
  public int TranslationX      { getset; }
  public int TranslationY      { getset; }

  public string RuleX          { getset; }
  public string RuleY          { getset; }

  public UIVariable Location   { getset; }
  public UIVariable RefViewX   { getset; }
  public UIVariable RefViewY   { getset; }
  public UIVariable ParentView { getset; }

  public double MinVal         { getset; }
  public double MaxVal         { getset; }
  public double CurrVal        { getset; }
}

The iOSVariable class derives from UIVariable, and has a concrete implementation for iOS. Its fragment is shown in Code Listing 9. Its Android equivalent is shown in Code Listing 10.

Code Listing 9: A Fragment of the iOSVariable Class

public class iOSVariable : UIVariable
{  

  public iOSVariable(UIType type, string name,
                     UIView viewx = nullUIView viewy = null) :
                     base(type, name)
  {
    m_viewX = viewx;
    m_viewY = viewy;
    if (type != UIType.LOCATION && m_viewX != null) {
      m_viewX.Tag = ++m_currentTag;
    }
  }

  public virtual iOSVariable GetWidget(string widgetType,

                   string widgetName, string initArg, CGRect rect)
  { /* ... */ }
  public virtual bool SetValue(string value1, string value2 = "")
  { /* ... */ }

  public virtual double GetValue()
  {

    double result = 0;
    if (m_viewX is UISwitch) {
      result = ((UISwitch)m_viewX).On ? 1 : 0;
    } else if (m_viewX is UISlider) {
      result = ((UISlider)m_viewX).Value;
    } else if (m_viewX is UIStepper) {
      result = ((UIStepper)m_viewX).Value;
    } else if (m_viewX is UIPickerView) {
      result = ((TypePickerViewModel)(((UIPickerView)m_viewX).Model)).

                 SelectedRow;
    } else if (m_viewX is UISegmentedControl) {
      result = ((UISegmentedControl)m_viewX).SelectedSegment;

    }
    /* And so on ... */
    return result;

  }

  public virtual bool SetText(string text, string alignment = null)
  { /* ... */ }
  public virtual string GetText()
  { /* ... */ }

  public virtual void AddData(List<string> data, string varName,

                              string title, string extra)
  { /* ... */ }
  public virtual void AddImages(List<UIImage> images,

                                string varName, string title)
  { /* ... */ }
  public virtual bool SetFont(string name, double size = 0)
  { /* ... */ }
  public virtual bool SetFontSize(double val)
  { /* ... */ }
  public virtual bool SetFontColor(string colorStr)
  { /* ... */ }
  public virtual bool SetBold(double size = 0)
  { /* ... */ }
  public virtual bool SetItalic(double size = 0)
  { /* ... */ }

  public virtual bool SetNormalFont(double size = 0)
  { /* ... */ }

  public virtual bool SetBackgroundColor(string colorStr,

                                         double alpha = 1.0)
  { /* ... */ }

  public static UIView GetView(string viewName, ParsingScript script)
  {/* ... */}

  public UIView GetParentView()
  {
    iOSVariable parent = ParentView as iOSVariable;
    if (parent != null) {
      return parent.ViewX;
    }
    return AppDelegate.GetCurrentView();
  }

  UIView m_viewX;
  UIView m_viewY;

}

Code Listing 10: A Fragment of the DroidVariable Class

public class DroidVariableUIVariable
{

  public DroidVariable(UIType type, string name, View viewx = null,

                       View viewy = null) : base(type, name)
  {
    m_viewX = viewx;
    m_viewY = viewy;
    if (type != UIType.LOCATION && m_viewX != null) {
      m_viewX.Tag = ++m_currentTag;
      m_viewX.Id = m_currentTag;
    }
  }

  public void SetViewLayout(int width, int height)
  {
    DroidVariable refView = Location?.RefViewX as DroidVariable;
    m_viewX = MainActivity.CreateViewLayout(width, height,

                                            refView?.ViewLayout);
  }

  public virtual DroidVariable GetWidget(string widgetType,

                               string widgetName, string initArg,
                                           int width, int height)

  { /* ... */ }

  public static Size GetLocation(View view)
  {
    if (view == null) {
      return null;
    }
    int[] outArr = new int[2];
    view.GetLocationOnScreen(outArr);
    return new Size(outArr[0], outArr[1]);
  }

  public static View GetView(string viewName, ParsingScript script)
  {
    if (viewName.Equals("root"StringComparison.OrdinalIgnoreCase)) {
      return null;
    }
    ParserFunction func = ParserFunction.GetFunction(viewName);

    Variable viewValue = func.GetValue(script);
    DroidVariable viewVar = viewValue as DroidVariable;
    return viewVar.ViewX;
  }


  /* Same SetXXX and GetXXX functions as for iOSVariable class... */

  View m_viewX;
  View m_viewY;
  LayoutRules m_layoutRuleX;
  LayoutRules m_layoutRuleY;
}

Summary

In this chapter, we reviewed the general structure of a project using CSCS scripting and where in the project workflow the CSCS code is executed. It is done differently for iOS and for Android. We skipped a few details, since there is really a lot of code. However, since the devil is in the details, for the full picture, check out the accompanying source code.

In the next chapter, we are going to dig into the details of placing widgets on the screen in a platform-independent way.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.