How To: Support User Editable Python Macros in A iOS Application

Date Published:
Last Modified: by

Introduction

Last month I published a article on how to use JavascriptCore for extending iOS applications with macro support. While Javascript has many uses, as a way for application customization, it would not be my first choice.

A better choice to me would be a language like python. Being curious, I wondered what it would take to to use python. This article discusses what I found.

Which Python To Use?

Python is a popular language with several good interpreters to choose from. The interpreter you are most likely to run into is CPython. It is written in C and is available almost everywhere, including Mac OS X. It has a well documented hosting API that allows third parties to host the interpreter in an application while extending it with custom modules, functions and datatypes. The documentation can be found here: [https://docs.python.org/3.5/extending/index.html].

CPython has been ported to iOS by the BeeWare Project. This project is currently actively supported and available through the GitHub repository Python-Apple-Support. This repository supports three different version of Python: 2.7, 3.4, and 3.5. Freakboy3742 from Perth Australia is currently providing great support. (Thanks Russell!)

We will be using Python 3.5 for this example.

The End Result

The example application for this article, PythonMacros, is located on GitHub. It is essentially the same application that I created for the JavascriptCore article except it uses python instead of javascript for macro support.

The example has a global model object that records the daily exercise progress of an individual. The main view controller shows that progress with three custom progress indicators. The view controller also offers four buttons that change the progress results in various manners. This is done by calling out to user editable python scripts. Each macro returns a string that is displayed under the progress rings, while randomly updating the progress values.

PythonMacros In Action

The view controller also supports changing the macros through an edit button.

To be useful, the macro engine needs to support both the iOS application calling python functions and the python functions calling swift code.

PythonMacros Application Structure

What We Need To Provide

To clarify, the hosting iOS application must have the ability to call out to python user macros. In addition, the hosting iOS application must publish application services that the user’s python macros can use.

In our case, the iOS application will invoke a python function when the user presses a button. The python function will in turn use services offered by the iOS application to lookup and then change the daily progress values.

The Application Services

The PythonMacros application maintains a global model object that contains the three progress values. This is defined in the DailyProgress swift class.

//
//  DailyProgress.swift
//  PythonMacros
//
//  Created by Rodger Higgins on 7/5/16.
//  Copyright © 2016 Rodger Higgins. All rights reserved.
//

import Foundation


/// Class used to store the global progress model used for this tutorial.  The
/// daily progress is represented as three properties off of the singleton.
///
/// This class also register 6 swift blocks into the CPython runtime.  These
/// python functions are used by python scripts to retrieve/set each of the
/// three progress values.
///
/// This class also supports a delegate for getting notification of
/// any progress changes.
class DailyProgress {

    /// Class property used to retrieve the singleton object
    static var sharedInstance: DailyProgress = DailyProgress()

    .
    .
    .

    /// Property containing the current value for the daily active calories
    var activeCalories: Double = 0.0 {
        didSet {
            delegate?.activeCalorieUpdate(activeCalories)
        }
    }


    /// Property containing the current value for the daily active minutes
    var activity: Double = 0.0 {
        didSet {
            delegate?.activityUpdate(activity)
        }
    }


    /// Property containing the current number of standup hours
    var standup: Double = 0.0 {
        didSet {
            delegate?.standupUpdate(standup)
        }
    }


    /// Property used to register delegate
    var delegate: DailyProgressDelegate?

    .
    .
    .
}


protocol DailyProgressDelegate {
    func activeCalorieUpdate(newValue: Double)
    func activityUpdate(newValue: Double)
    func standupUpdate(newValue: Double)
}

The three activity progress values are implemented as class properties with a protocol that allows for the notification of updates to those values.

During initialization, application services are setup for the python runtime in the setupPythonCallbacks method:

    /// A private method used to register the swift blocks with the
    /// PythonMacroEngine.
    private func setupPythonCallbacks() {
        functions.append(PythonFunction(name: "getActiveCalories", callArgs: [], returnType: .Double, block: { Void -> AnyObject? in
            return self.activeCalories
        }))
        engine.callable?.registerFunction(functions.last!)

        functions.append(PythonFunction(name: "setActiveCalories", callArgs: [.Double], returnType: .Void, block: { (args) -> AnyObject? in
            guard let newValue = args?[0] as? Double else { return nil }

            self.activeCalories = newValue

            return nil
        }))
        engine.callable?.registerFunction(functions.last!)


        functions.append(PythonFunction(name: "getActivity", callArgs: [], returnType: .Double, block: { Void -> AnyObject? in
            return self.activity
        }))
        engine.callable?.registerFunction(functions.last!)

        functions.append(PythonFunction(name: "setActivity", callArgs: [.Double], returnType: .Void, block: { (args) -> AnyObject? in
            guard let newValue = args?[0] as? Double else { return nil }

            self.activity = newValue

            return nil
        }))
        engine.callable?.registerFunction(functions.last!)


        functions.append(PythonFunction(name: "getStandup", callArgs: [], returnType: .Double, block: { Void -> AnyObject? in
            return self.standup
        }))
        engine.callable?.registerFunction(functions.last!)

        functions.append(PythonFunction(name: "setStandup", callArgs: [.Double], returnType: .Void, block: { (args) -> AnyObject? in
            guard let newValue = args?[0] as? Double else { return nil }

            self.standup = newValue

            return nil
        }))
        engine.callable?.registerFunction(functions.last!)
    }

Each “service” is actually a swift block that is called from python by using a custom python module that is defined in ios.m:

//
// ios.m
// MIT License
//
// Copyright (c) 2016 Spazstik Software, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#import <Foundation/Foundation.h>
#import "Python/Python.h"
#import "ios.h"


// Python method used to call registered blocks.  It calls back to swift to
// actually progress the call.  The Python parameters are:
//  1. block name as a string
//  2. tup-le of paramters to pass to the swift block.
//
//  returns:
//  1. Python return type
static PyObject *iOS_callBlock(PyObject *self, PyObject *args)
{
    return ios_process_block(args);
}


// Table that lists the valid Python methods in this module
static PyMethodDef iOS_Methods[] = {
    { "call", iOS_callBlock, METH_VARARGS, "Call Objective-C block"},
    { NULL, NULL, 0, NULL }
};


// Table that contains the definition for this module
static PyModuleDef iOS_Module = {
    PyModuleDef_HEAD_INIT, "ios", NULL, -1, iOS_Methods, NULL, NULL, NULL, NULL
};


// Routine used by CPython to initialize this module
static PyObject *PyInit_iOS(void)
{
    return PyModule_Create(&iOS_Module);
}


// Method used by swift to register this module with CPython.  It appends
// the module definition table to the list of modules that CPython know about.
void init_ios_module() {
    PyImport_AppendInittab("ios", &PyInit_iOS);
}



// Utility function used to return the Py_None object to swift.  Used to return
// null from Python methods implemented as swift blocks.
PyObject *PyNone_Ref() {
    Py_IncRef(Py_None);

    return Py_None;
}

This is a simple module that allows us to create a custom python function that in turn calls into the module and invoke a swift block that is registered. At the swift level, the PythonFunction class supports this. The DailyProgress class initialized a instance of a PythonFunction to support getting the activity progress:

functions.append(PythonFunction(name: "getActivity", callArgs: [], returnType: .Double, block: { Void -> AnyObject? in
            return self.activity
}))
engine.callable?.registerFunction(functions.last!)

The name parameter is the name of the resulting python function. A swift block can be defined with any number of simple value parameters that are listed in the callArgs parameter. The swift block can return a simple value back to the python runtime. The return type is specified in the returnType parameter. The final parameter is the actual swift block that is called from python.

A actual python function is created when the registerFunction method on the global PythonEngine object is called. The python definition for the activity functions is the following:

import ios
def getActivity() -> float:
    return ios.call('getActivity', ())

or:

import ios
def setActivity(a: float):
    ios.call('setActivity', (a, ))

The definition imports the custom module that is part of our interface layer. The iOS module provides one function. This function expects the function name to passed as the first parameter with any parameters passed as a tuple. The parameter decoding and calling the matching swift block is handled by the glue code that I wrote to support this.

The Custom User Python Macros

User python macros are supported through the use of the PythonMacro class. The demo application includes the user python scripts in its resource bundle:

import math

def more():
    oldActiveCalories = getActiveCalories()
    newActiveCalories = oldActiveCalories + random.random()
    if newActiveCalories > 10.0:
        newActiveCalories = 10.0
    setActiveCalories(newActiveCalories)

    oldActivity = getActivity()
    newActivity = oldActivity + random.random()
    if newActivity > 10.0:
        newActivity = 10.0
    setActivity(newActivity)

    oldStandup = getStandup()
    newStandup = oldStandup + random.random()
    if newStandup > 10.0:
        newStandup = 10.0
    setStandup(newStandup)

    return "{:5.2f}, {:5.2f}, {:5.2f}".format(newActiveCalories, newActivity, newStandup)

As you can see it calls the functions that were created as part of the daily progress object to query and then update the activity values. The new values are returned as a string.

The user macro functions are loaded by the activity view controller during its viewDidLoad:

//
// ActivityViewController.swift
// MIT License
//
// Copyright (c) 2016 Spazstik Software, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.


import UIKit


/// View controller class that is used to demonstrate the calling
/// of macros in CPython
class ActivityViewController: UIViewController, DailyProgressDelegate {

    private let dailyProgress: DailyProgress = DailyProgress.sharedInstance

    private var macros: [PythonMacro] = []

    @IBOutlet weak var outerProgressView: CustomProgressView!
    @IBOutlet weak var middleProgressView: CustomProgressView!
    @IBOutlet weak var innerProgressView: CustomProgressView!
    @IBOutlet weak var messageLabel: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        DailyProgress.sharedInstance.delegate = self

        setupMacros()
    }


    /// A private method used to setup the PythonMacros that will get called
    /// when the user selects one of four buttons displayed.
    private func setupMacros() {
        macros.append(PythonMacro(filename: "even_more", functionName: "evenMore"))
        macros.append(PythonMacro(filename: "more", functionName: "more"))
        macros.append(PythonMacro(filename: "less", functionName: "less"))
        macros.append(PythonMacro(filename: "even_less", functionName: "evenLess"))
    }


    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "EditMacros" {
            if let emvc = segue.destinationViewController as? EditMacroViewController {
                emvc.macros = macros
            }
        }
    }


    @IBAction func callPythonMacro(sender: UIButton) {
        let r: String? = macros[sender.tag].call()
        messageLabel.text = r
    }



    func activeCalorieUpdate(newValue: Double) {
        outerProgressView.progress = CGFloat(newValue)
    }


    func activityUpdate(newValue: Double) {
        middleProgressView.progress = CGFloat(newValue)
    }


    func standupUpdate(newValue: Double) {
        innerProgressView.progress = CGFloat(newValue)
    }
}

The PythonMacro class reads the macros from the resource bundle and registers them with the python runtime. The macro is called when the call method is invoked:

@IBAction func callPythonMacro(sender: UIButton) {
    let r: String? = macros[sender.tag].call()
    messageLabel.text = r
}

The progress is updated when the macro uses the services provided to update the daily progress properties. The global daily properties object then notifies the activity view controller of the new values by its delegate protocol methods.

The Interface Code

At the heart of the code is the class PythonMacroEngine. It has the responsibility of starting/stopping and interfacing with the CPython runtime. It uses several classes to enable the support that is required to fulfill the requirements of the demo application. I will not go into the details here other then saying that it was pretty straightforward in its implementation.

In Summary

Overall I think that using Python for extending a iOS application is a viable solution. Python is more likely to enable non-technical people to take advantage of application extensibility then javascript.

Please keep in mind that this is enabled by the work of the BeeWare Project. What they have done makes the adoption of the Python-Apple-Support. over other alternatives a good choice.

That being said, there are some improvements that I would do to what is shown in the PythonMacros demo: * I implemented the interface code in Swift. This is good for the relatively simple requirements for the demo application. I would recommend using objective-c if you need to support more complex requirements. * Editing a macro on a phone or tablet can be tedious. It will be essential to have a comprehensive code editor. I would recommend implementing as much template expansion, auto-indenting, and syntax highlighting support as you could possibly deliver. The demo application has a pretty ruff start at this. * I realized when reviewing the demo’s code that it cannot save the changes to the macros over application starts. This is a simple change that would need to be done.

I found this source on embedding python. I found it useful during development of the demo application.

Rodger Higgins is the founder of Spazstik Software, LLC. He has created StackCalc, The Visual Touch Calculator and SPZTracker.

Recent Articles

A Reusable Observer Protocol Written In Swift

One design pattern that I use a lot is the observer pattern. The observer pattern is used when you have an object that needs to notify a list of objects that state changes have happened. This article discusses a reusable component, in Swift, I developed to speed up my development process.


Read More...
How To: Support User Editable Python Macros In A I Os Application

Last month I published a article on how to use JavascriptCore for extending a iOS application with macro support. While Javascript has many uses, as a way for application customization, it would not be my first choice.

A better choice to me would be a language like Python. Being curious, I wondered what it would take to to use Python. This article discusses what I found.


Read More...
How To Example: Extend A I Os Using Javascript Core As A Macro Engine

JavascriptCore is a framework that offers the ability for a iOS application to interact with javascript code. Primarily used for cross platform code sharing, it can also be used to extend a iOS application with macro capabilities. This framework offers a world of possibilities for extending any iOS application.


Read More...
How To: Custom I Os Activity Tracker View Using Ca Layers

The Apple Watch shipped with a captivating activity tracker. The center piece is a really cool spiral animation scheme showing the amount of activity during the day. This image is also shown on the matching iPhone Activity App. I have always wanted to see what it would take to implement this myself. The examples that I see typically use a custom drawRect override, but I always wanted to see what it would take to do with CAShapeLayers.

Implementing a 0-100% control is straight forward when using CAShapeLayer. But how do you implement a progress indicator that support progress values greater then 100%? This How To discusses a solution that I came up with along with it’s potential limitations.


Read More...
How To Display Custom Content On A External Screen From A I Os Device

Being able to display content on a external screen or device is a great capability to add to a iOS application. Especially how easy it is. This article will show the step required to to do this.


Read More...

Follow us on

Articles by published month

Articles by subject matter

Rails Thor Compass Susy Modernizr Rspec Capybara Bettererrors Railspanel Aws Rack Railscasts Http Aws-elastic-beanstalk Ruby-on-rails Rack-rewrite Http-response-codes Pow-amazon-route-53 Stackcalc Iphone Ios Mobile Application Skeumorphic Dns Web-site Elastic-beanstalk Elastic-ip Elastic-load-balancer Tutorial Howto Javascript Javascriptcore Macro Example Swift Design-patterns Observer Python Macros Alamofire External-screen-support Apple-watch Activity-tracker Office Status S100 Cloud Astronomy Picture Mars Apple Usb-c Leonard-nimoy William-shatner Geotag Gps Spztracker Geotagging Photos Secret Marketing Watch Watch-repair Head-transplants Perception Diabeties Sugar Health Rosette-nebula News Spock Comet-lovejoy

Click here to receive free tips and tutorials!

This web site uses javascript exclusively for automating html elements. Please enable javascript to fully experience the features offered on this site.