The Native Wolfram Player for iOS

Rob Raguet-Schofield & Jason Harris, Wolfram Research
WTC October (30^th) 2019

In this talk we will walk through some of the capabilities of the native Wolfram Player for iOS. We will demonstrate how to view and interact on your iPad and iPhone with notebook files from your Wolfram Cloud account, websites, email attachments, or other cloud storage. We will briefly talk about the origins of the technology behind the core of the iOS Player and future directions. We will then progress onto persistence,  and loading local and cloud stored packages. Using these techniques we will demonstrate an application which loads an external package dealing with Peer2Peer lending and allows us to do simulations and visualizations of different grades of investments. The second half of of the talk will cover Wolfram Engine on iOS using our CDFKit SDK.

Talk InitCode

This init code makes CloudObject URL’s expand to their full path so that they will work for other non-authenticated users.

ExpandCloudObject[a_]:=a/.
HoldPattern[CloudObject[s_]]:>With[{o=CloudObject[s]},o/;True]

ExpandCloudObject@Hold@CloudObject["test/TestPackage"]

The native Player app for iOS

Wolfram Player for iOS version 12.0 is available now

The app is entirely standalone and entirely native; it has a full local kernel

The app is available for free on the Apple AppStore

The free app  will open and statically display any Notebook document

The free app allows full dynamic interactivity for Signed documents

Optionally, you can make a one time in-app purchase (USD$9.99) which enables full dynamic interactivity for any Notebook  document

(Technically the feature set of the Wolfram Player for iOS is closest to desktop PlayerPro)

A Quick Tour

Typesetting

∫(Sqrt[x+(Sqrt[x])])\[DifferentialD]x

TraditionalForm[%]

Graphics

Plot[Evaluate[Table[BesselJ[n,x],{n,4}]],{x,0,10},Filling->Axis]

Graphics3D

RegionPlot3D[x y z<1,{x,-5,5},{y,-5,5},{z,-5,5},PlotStyle->Directive[Yellow,Opacity[0.5]],Mesh->None,Ticks->None]

Dynamic

Checkbox[Dynamic[foo]]

Graphics[{Red,Disk[{0,.25},.5],Green,Disk[{-.33,-.25},.5],Blue,Disk[{.33,-.25},.5]},Background->Dynamic[If[foo,RGBColor[0,0,0],RGBColor[1,1,1]]]]

Manipulate

Manipulate[ContourPlot3D[x^2+y^2+a z^3==1,{x,-2,2},{y,-2,2},{z,-2,2},Mesh->None,Ticks->None],{a,-2,2}]

Native App

Open documents from anywhere

Supports multiple Wolfram Cloud accounts

Web downloads

E-mail attachments

Other cloud document services (e.g. iCloud, Dropbox, Google Drive, etc.)

Sync with iTunes SideLoading

AirDrop

Local computations

Full Wolfram Engine embedded

Improved interactivity

Does not require network

iOS appropriate interface and user experience

Finger-sized touch targets

Pinch to zoom

Manipulate controls default position doesn’t require covering content with your hand

TouchscreenControlPlacement

TouchPosition

Locator magnifiers

Etc

Examples of touchscreen interface

TouchPosition

MousePosition[]

Style[Dynamic[TouchPosition[]],Background->Green]

TouchscreenControlPlacement

Manipulate[Expand[(α+β)^n],{n,1,100,1},
ControlType->VerticalSlider,
TouchscreenControlPlacement->Right]

Slider

Slider[Dynamic[xx],{1,3}]

Slider[Dynamic[xx],{1,3,1}]

Locator

LocatorPane[{{1,1}/2,{1,-1}/2},Plot[Sin[x],{x,0,10}]]

Native App Directions

Whole New Core

It should be pointed out that this NBLite has a whole new front end core in it. It is multithreaded from the ground up and has a more modern internal architecture which allows many internal improvements. Over time we will be bringing these great new features back to the main desktop FrontEnds.

Current Limitations

Parallel tools: Right now there is a single kernel for each FrontEnd living in the same processor space.

Saving: We can’t “save” a notebook with changes in it. (Persistence must be done via LocalSymbol or other means)

Notebook Programming: Notebook programming generally at this stage does not work well on the iOS Player, but this will change in later releases.

Various complex features: e.g.  Buttons with Method->"Queued" do not yet work (Controls are only working over the Preemptive link at the moment)

Java: Apple prohibits using Java on iOS, instead you can use URLFetch and URLExecute

LocalFile System

On iOS / iPadOS apple is / has moved in the direction of having a “file system” on device.

Side Loading

You can load files and folders directly onto your iPad / iPhone via side loading:

Local Persistence: LocalSymbol

{Slider[Dynamic[nn],{1,10,1}],Dynamic@nn}

Store a value locally on this iOS device

Button["StoreLocal", LocalSymbol["x"]=nn!]

Retrieve the value locally stored on this iOS device

Button["PrintLocal",Print@LocalSymbol["x"]]

More on this see:
  - Roman’s past talk  “Persistence and Initialization”.

Local Packages

Button["Notebook Directory", Print@NotebookDirectory[]]

Button["Load Package", Needs["SimulateP2P`",File@FileNameJoin[{NotebookDirectory[],"SimulateP2P.m"}]]]

Button["Simulate One",Print[APR[3][TotalValue@Nest[SimulateMonth,Basket[5000,{},{"B1","B2","B3","B4","B5"}],37]/5000]]]

What about Dynamic timeouts due to loading large packages? DynamicEvaluationTimeout or SessionSubmit to background the loading process.

Cloud

Cloud Connectivity

We can connect to the cloud with something like

Button["connect",Print@CloudConnect["jasonh@wolfram.com",password]]

Now we are going to do some storage and retrieval on a CloudObject “mistercloud”

CloudPut[0,CloudObject["mistercloud"]];
SetPermissions[CloudObject["mistercloud"],"Public"];

Some values:

Row@{Labeled[Dynamic[x],Value,Left],Spacer[15],
Button["reset",x=0], Spacer[15],
Button["increment",If[Not@ValueQ@x,x=0,x++]]}

ExpandCloudObject@Button["StoreIntoCloud",CloudPut[x,CloudObject["mistercloud"]]]

ExpandCloudObject@{Button["LoadFromCloud",cloudValue=Quiet@CloudGet@CloudObject["mistercloud"]],Dynamic@cloudValue}

Similarly we can do operations with CloudExpression, and Databin, DatabinAdd, etc.

Cloud Loading

We can store the local package in the cloud like so:

CopyFile["/Users/jason/Talks/WRI_EWTC_Oxford_2018/SimulateP2P.m",CloudObject["P2P/SimulateP2P.m"]];
SetPermissions[CloudObject["P2P/SimulateP2P.m"], "Public"];

ExpandCloudObject@Button["Load Package",CloudGet@CloudObject["P2P/SimulateP2P.m"]]

Button["simulate One",Print[APR[3][TotalValue@Nest[SimulateMonth,Basket[5000,{},{"B1","B2","B3","B4","B5"}],37]/5000]]]

Auto Loading Packages

Manipulate has the Initialization option, what can we do to get around this?

AutoLoaded On View

We can set up autoloading on “first visualization” i.e. if we just scroll to the place in the notebook which has the following dynamic then it will basically trigger a load of our package. We

Row[{Dynamic[Panel[If[ValueQ@LoadStatus,LoadStatus,"Unloaded"],Background->Switch[LoadStatus,"Loaded",Green,"Loading",Orange,"Unloaded",LightRed]]],Dynamic[If[Not@ValueQ@LoadStatus,LoadStatus="Loading";SessionSubmit[Pause@4;LoadStatus="Loaded"];"",""]]}]

Alternative: Initialization Option

There is the Initialization option  to Dynamic, DynamicModule, etc, but closing and reopening the notebook will cause the initialization to run again and we might prefer this initialization to be linked to the kernel variable.

Peer2Peer Lending

Traditional Banking

TabView[Import/@FileNames["*.png","/Users/jason/Talks/WRIDeveloper2017/P2P/Graphics/BankImages"],ControlPlacement->Bottom]

Peer To Peer Lending

TabView[Import/@FileNames["*.png","/Users/jason/Talks/WRIDeveloper2017/P2P/Graphics/PeerImages"],ControlPlacement->Bottom]

Risks

Import["/Users/jason/Talks/WRIDeveloper2017/P2P/Graphics/Peer2PeerRobots.png"]

Peer to Peer Lenders

In the US:
 - Lending Club
 - Prosper
 - Upstart
 - Funding Circle

In New Zealand:
 - Harmoney (unsecured, fractionalized)
 - Squirrel Money (secured, reserve fund)
 - Lending Crowd (secured, fractionalized)

In Europe:
 - Bondora (Estonia , unsecured, fractionalized)
 - Mintos (buy back, marketplace)
 - Twino, doFinance, RoboCash, ...others

I will talk about Harmoney

Example Peer2Peer Loans Harmoney

FlipView[Import/@FileNames["*.png","/Users/jason/Talks/WRIDeveloper2017/P2P/Graphics/LoanExamples"]]

Peer to Peer Returns

Simulation

The simulation basically runs where each loan in the basket of loans can do one of three things:

Make a payment (interest and principal)

Pay off the entire loan

Default on the loan

The rest is tracking the default rate over time, the collected interest and principal payments and reinvesting them, etc.

ParallelNeeds["SimulateP2P`"]

Needs["SimulateP2P`"]

GenerateValues[grades_,n_]:=
APR[3]/@Table[TotalValue@Nest[SimulateMonth,Basket[5000,{},grades],37]/5000,n]

SetSharedVariable[j]

j=0;Monitor[(dataB2=Flatten@ParallelTable[j++;GenerateValues[{"B2"},20],{32*8}]),Row[{j,ProgressIndicator[j,{1,32*8}]}]];//AbsoluteTiming

j=0;Monitor[(dataB5=Flatten@ParallelTable[j++;GenerateValues[{"B5"},20],{32*8}]),Row[{j,ProgressIndicator[j,{1,32*8}]}]];//AbsoluteTiming

j=0;Monitor[(dataB25=Flatten@ParallelTable[j++;GenerateValues[{"B2","B5"},20],{32*8}]),Row[{j,ProgressIndicator[j,{1,32*8}]}]];//AbsoluteTiming

Visualization

Histogram[{dataB2,dataB5,dataB25},100,"PDF",ImageSize->550,ChartLabels->Placed[{"B2","B5","B25"},Center]]

Putting it All Together

We can put this all together into a mini application!

Summary

- A fully native player for iOS with full wolfram engine
- This is just one incarnation of the core
- A new modern core fully multi-threaded from the ground up
- Continued fleshing out of functionality
- Unification!

Wolfram Engine

The Free Wolfram Engine for Developers is available for pre-production software development.

You can use this product to :

Develop a product for yourself or your company

Conduct personal projects at home, at school, at work

Explore the Wolfram Language for future production projects

Additional licensing options for the Wolfram Engine :

Find out about production and OEM licensing of the Wolfram Engine

Find out about free use of the Wolfram Engine in open software projects

Introducing CDFKit

CDFKit Distribution

CDFKit Build Settings

Add CDFKit to your framework search path

Copy CDFKit frameworks into your app bundle when building

# First, create a frameworks directory inside the app if none exists:
if [ ! -d "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" ]; then
    mkdir "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
fi

# Copy over & code-sign the frameworks for the link paclets, depending on whether this is a device or simulator build:
IFS=$'\n'
for THE_FRAMEWORK in `find "{{the path to CDFKit goes here}}/Release-${PLATFORM_NAME}" -name "*.framework" -type d -print`; do
    THE_FRAMEWORK_BASE_NAME=$(basename "${THE_FRAMEWORK}")

    echo "Copying ${THE_FRAMEWORK} to ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
    cp -R "${THE_FRAMEWORK}" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
    echo "Code-signing ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${THE_FRAMEWORK_NAME}"
    codesign --deep -f -s "${EXPANDED_CODE_SIGN_IDENTITY}" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${THE_FRAMEWORK_BASE_NAME}"
done

CDFKit classes

CDFSession

CDFSession *session = CDFSession.sharedSession;

let session = CDFSession.shared()

Display notebooks

CDFDocument

CDFDocument *document = [CDFDocument documentWithSession:CDFSession.sharedSession];

let document = CDFDocument.init(session: CDFSession.shared())

CDFViewController

CDFViewController *controller = [CDFViewController controllerWithCDFDocument:document options:CDFViewControllerOptionsEnableScroll];

let controller = CDFViewController.init(cdfDocument: document, options: CDFViewControllerOptions.enableScroll.rawValue)

Perform evaluations

CDFExpr

CDFExpr *expr = CDFFunc(CDFSym(@"ToBoxes"), CDFParse(@"2+2"), CDFSym(@"StandardForm"));

let expr = CDFExpr.init(parts: [CDFExpr.init(symbol: "ToBoxes")!, CDFExpr.init(parsedString: "2+2", fuzzy: false)!, CDFExpr.init(symbol: "StandardForm")!])

CDFEvaluator

[CDFSession.sharedSession.evaluator evaluateExpr:inputExpr preemptive:NO queue:dispatch_get_main_queue() handler:^(CDFExpr *input, CDFExpr *output, NSError *error) {
	// Do something with `output`
}];

CDFSession.shared()?.evaluator.evaluateExpr(expr, preemptive: false, queue: DispatchQueue.main, handler: { (input, output, error) in
	// Do something with `output`
})

CDFKitTest

View

// Create an empty CDFDocument.
CDFDocument *document = [CDFDocument documentWithSession:CDFSession.sharedSession];

// Create a view controller to handle the display of the new document.
self.cdfController = [CDFViewController controllerWithCDFDocument:document options:CDFViewControllerOptionsEnableScroll];

// Use the `view` property of the returned controller to get the CDF view and add that to our view hierarchy.
[self.view addSubview:self.cdfController.view];

Evaluation

// Attempt to parse the text in the text field into a Wolfram Language expression.
CDFExpr *inputExpr = CDFParse(self.textField.text);

// Wrap the expression to evaluate in ToBoxes[_, StandardForm] so we can display the result in the CDF view.
inputExpr = CDFFunc(CDFSym(@"ToBoxes"), inputExpr, CDFSym(@"StandardForm"));

// Perform the evaluation. It doesn’t need to preempt any currently running evaluation (since this app only allows one evaluation at a time).
// Tell CDFKit to call our completion handler on the main thread (dispatch queue) since we update the user interface in the handler.
[CDFSession.sharedSession.evaluator evaluateExpr:inputExpr preemptive:NO queue:dispatch_get_main_queue() handler:^(CDFExpr *input, CDFExpr *output, NSError *error) {
	// Display the resulting content in the CDF view.
	[self.cdfController setExpr:output];
}];

ExpressionTree

View

CDFDocument *document = [CDFDocument documentWithExpr:CDFParse(@"Notebook[{}, StyleDefinitions->Notebook[{Cell[StyleData[\"Output\"],CellMargins->0]}]]") session:CDFSession.sharedSession];

self.cdfController = [CDFViewController controllerWithCDFDocument:document options:0];
[self.view addSubview:self.cdfController.view];

// Set the contents of the CDF view to display a DynamicBox whose contents depend on the values of the dynamic variables we will update in the `parse` method.
[self.cdfController setExpr:CDFParse(@"DynamicBox[ToBoxes[Quiet[TreeForm[ExpressionTree`expr, ImageSize->ExpressionTree`size]], StandardForm]]")];

Evaluation

CDFExpr *e = [CDFExpr exprWithParsedString:self.textField.text fuzzy:self.fuzzyParseSwitch.on];

// ExpressionTree`expr = input
e = CDFSet("ExpressionTree`expr", e);

// ExpressionTree`size = {width, height}
CGSize size = self.cdfController.view.bounds.size;
CDFExpr *updateSize = CDFSet("ExpressionTree`size", CDFList(CDFReal(size.width), CDFReal(size.height)));

// ExpressionTree`expr = input; ExpressionTree`size = {width, height}
[CDFSession.sharedSession.evaluator evaluateExpr:CDFCompoundExpr(updateSize, e) preemptive:YES queue:NULL handler:nil];

Molecules

Data

// Find Salts.dat and read its contents into memory.
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Salts" ofType:@"dat"]];

// Create a CDFExpr instance from the Salts.dat data, which is the output of the Wolfram Language Compress[] function.
CDFExpr *entries = [CDFExpr exprWithCompressedData:data];

View

// Create a Notebook[] with a custom stylesheet that puts 0 margins around Output cells and doesn’t show any cell brackets.
CDFDocument *document = [CDFDocument documentWithExpr:CDFParse(@"Notebook[{}, StyleDefinitions->Notebook[{Cell[StyleData[\"Output\"],CellMargins->0,ShowCellBracket->False]}]]") session:CDFSession.sharedSession];

// Create a CDF view controller using our custom notebook.
self.cdfController = [CDFViewController controllerWithCDFDocument:document options:0];
[self.view addSubview:self.cdfController.view];

// Each entry is of the form `{StandardName, Name, MoleculePlot}`
// Use StandardName as the title of this view controller
self.title = [self.entry part:1].stringValue;

// Figure out the dimensions of the view so we can size the Graphics3DBox appropriately.
CGSize size = self.cdfController.view.bounds.size;
CGFloat dim = MIN(size.width, size.height);

// Extract the Graphics3DBox[] expression that displays the molecule plot
CDFExpr *box = [self.entry part:3];
NSMutableArray *boxParts = [NSMutableArray arrayWithArray:box.parts];
// Add ImageSize->{dim, dim} to the end of the Graphics3DBox expression.
[boxParts addObject:CDFRule(CDFSym(@"ImageSize"), CDFList(CDFReal(dim), CDFReal(dim)))];
box = [CDFExpr exprWithParts:boxParts];

// Replace the contents of the currently displayed notebook with the new Graphics3DBox[__, ImageSize->{width, height}] expression.
[self.cdfController setExpr:box];

SwiftCDFTest

View

// Load TestNB.nb from the app bundle.
let cdfDocument = CDFDocument.init(filePath: Bundle.main.path(forResource: "TestNB", ofType: "nb"), session: CDFSession.shared())

// Create a CDF view controller from TestNB.nb
let cdfController = CDFViewController.init(cdfDocument: cdfDocument, options: CDFViewControllerOptions.enableScroll.rawValue)
view.addSubview(cdfController!.view)