What I'm trying to do is store some data from a MacOS App with NSDocument provided class in a file. I decided to use SwiftUI , but all tutorials I found are using Storyboards. And from those I cannot adapt how to get the data from my textfield into my NSDocument class.
As far as I got it I need to init my variables in the NSDocument class like this
class Document: NSDocument {
#objc dynamic var contents = "Foo"
public init(contentString: String) {
self.contents = contentString
}
/* ... */
}
and in the same class I can save this string using
override func data(ofType typeName: String) throws -> Data {
return contents.data(using: .utf8) ?? Data()
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
So in my view generated with SwiftUI I can access this using
struct MainTableView: View {
#State var doc = Document.init()
var body: some View {
TextField("My text", text: self.$doc.contents)
}
}
But - as I'm using only an instance it always saves "Foo" - no matter what I type into my TextField.
Besides - another question that will follow up right away: On the long run I don't want to store a string only. I'll have 3 different 2D-Arrays with different data-structures. Is NSDocument able to handle this by itself or do I need to convert those to JSON/XML/...-String and store this as a file?
Related
I am saving an Int in UserDefaults and this is reduced by one by clicking a button. I don't know if that is important but I have added an extension to UserDefaults to load an initial value if the app starts for the first time:
extension UserDefaults {
public func optionalInt(forKey defaultName: String) -> Int? {
let defaults = self
if let value = defaults.value(forKey: defaultName) {
return value as? Int
}
return nil
}
}
The UserDefaults are used as ObservableObject and accessed as EnvironmentObject within the app like this:
class Preferences: ObservableObject {
#Published var counter: Int = UserDefaults.standard.optionalInt(forKey: COUNTER_KEY) ?? COUNTER_DEFAULT_VALUE {
didSet {
UserDefaults.standard.set(counter, forKey: COUNTER_KEY)
}
}
}
I am now trying to test that the value in the UserDefaults decreases when the button is clicked.
I am trying to read the UserDefaults in the test with:
XCTAssertEqual(UserDefaults.standard.integer(forKey: "COUNTER_KEY"), 9)// default is 10
I have tried it with normal UnitTests where the methods behind the Button are called and with UITests but both do not work. In the UnitTests I get back the COUNTER_DEFAULT_VALUE and in the UiTests I get back 0.
I am trying to access the UserDefaults directly in the test, instead of using the Preferences object, because I have not found a way to access that as it is an ObservableObject.
I have checked in the Emulator that the UserDefaults are saved/loaded correctly when using the app. Is it not possible to access the UserDefaults in the tests or am I doing it wrong?
The key to success is Dependency injection. Instead of directly accessing the shared user defaults object (UserDefaults.standard), declare an object of type UserDefaults within the class:
let userDefaults: UserDefaults
In the view where you declare the model, you are free to use the shared object:
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
But inside your test, create an dedicated UserDefaults object and pass it to the initializer like so:
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
The benefit is clear: You control the state of UserDefaults. And that means the code works in every environment. To keep it simple, I haven't incorporated your extension, yet. But I'm sure you will manage to get it working.
TL;DR
Please see my working example below:
ContentView.swift
import SwiftUI
import Foundation
class Preferences: ObservableObject {
let userDefaults: UserDefaults
#Published var counter: Int {
didSet {
self.userDefaults.set(counter, forKey: "myKey")
}
}
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
self.counter = userDefaults.integer(forKey: "myKey")
}
func decreaseCounter() {
self.counter -= 1
}
}
struct ContentView: View {
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
var body: some View {
HStack {
Text("Value: \(self.model.counter)")
Button("Decrease") {
self.model.decreaseCounter()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
InjectionTests.swift
import XCTest
#testable import Injection
class InjectionTests: XCTestCase {
func testPreferences() throws {
// arrange
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
// act
let valueBefore = userDefaults.integer(forKey: "myKey")
model.decreaseCounter()
let valueAfter = userDefaults.integer(forKey: "myKey")
// assert
XCTAssertEqual(valueBefore - 1, valueAfter)
}
}
I know how to add an AccessoryView to an NSOpenPanel (and that works correctly).
Now I would like to make the options that the user selects in the AccessoryView available to the document that is opened.
Any suggestions how that can be doen (if at all?)
I have not found a standard solution, so I created my own:
Introduced a dictionary in the NSDocumentController that associates file URLs with option sets
Override the runModalOpenPanel and wrap the runModalOpenPanel of super with first the setup of the accessory view, and afterwards the evaluation of the options and adding of the options to the dictionary for the associated urls.
When a document is opened, the document can -through the shared NSDocumentController- access the dictionary and retrieve the options.
I am not blown away by this solution, but I also do not see an easier path.
Example code:
struct OptionsAtFileOpen {
let alsoLoadFormat: Bool
}
class DocumentController: NSDocumentController {
var fileOptions: Dictionary<URL, OptionsAtFileOpen> = [:]
var accessoryViewController: OpenPanelAccessoryViewController!
override func runModalOpenPanel(_ openPanel: NSOpenPanel, forTypes types: [String]?) -> Int {
// Load accessory view
let accessoryViewController = OpenPanelAccessoryViewController(nibName: NSNib.Name(rawValue: "OpenPanelAccessoryView"), bundle: nil)
// Add accessory view and make sure it is shown
openPanel.accessoryView = accessoryViewController.view
openPanel.isAccessoryViewDisclosed = true
// Run the dialog
let result = super.runModalOpenPanel(openPanel, forTypes: types)
// If not cancelled, add the files to open to the fileOptions dictionary
if result == 1 {
// Return the state of the checkbox that selects the loading of the formatting file
let alsoLoadFormat = accessoryViewController.alsoLoadFormatFile.state == NSControl.StateValue.on
for url in openPanel.urls {
fileOptions[url] = OptionsAtFileOpen(alsoLoadFormat: alsoLoadFormat)
}
}
return result
}
}
And then in Document
override func read(from data: Data, ofType typeName: String) throws {
...
if let fileUrl = fileURL {
if let dc = (NSDocumentController.shared as? DocumentController) {
if let loadFormat = dc.fileOptions[fileUrl]?.alsoLoadFormat {
...
}
}
}
}
My app has a hierarchy of classes for creating custom view controllers.
The first class is AppViewController. It extends NSViewController and contains methods common to all of my view controllers, like displaying alerts, retrieving data from the database, and so forth. It does not define any variables.
class AppViewController: NSViewController
{
...
}
The next class is ListViewController and is common to all of my "list" views. These are views that contain a single NSTableView with a list of all of the records from the associated database table. It extends AppViewController and conforms to the usual protocols.
Note that this class is generic so that it can properly handle the different views and data models.
class ListViewController<Model: RestModel>: AppViewController,
NSWindowDelegate,
NSTableViewDataSource,
NSTableViewDelegate
{
...
}
ListViewController defines a number of variables, including an IBOutlet for an NSTableView. That outlet is not wired to anything in the storyboard. The plan is to set it at run-time.
ListViewController also defines various functions including viewDidLoad(), viewWillAppear(), a number of app-specific functions, and so on.
The last class is specific to a database model and view, in this case, the Customers view. It extends ListViewController.
class Clv: ListViewController<CustomerMaster>
{
...
}
CustomerMaster is a concrete class that conforms to the RestModel protocol.
The problem:
The strange thing is that the last class, Clv, does not show up in the storyboard's Custom Class: Class pull-down menu, meaning that I cannot specify it as the custom class for my view.
I tried just typing it in, but that results in a run-time error
Unknown class _TtC9Inventory3Clv in Interface Builder file ...
If I remove the <Model: RestModel> from the ListViewController class definition and also remove the <CustomerMaster> from the Clv class definition, the Clv class then appears in the Class menu (of course that doesn't really help, just an observation).
AppViewController and ListViewController both do appear in that menu.
I am at a loss.
Earlier this year I created a similar architecture for an app, and I have to tell you: It can't work with storyboards, as those don't know anything about generics during instantiation.
What works is using nibs though, as you than still can init your view controller yourself.
an example:
import UIKit
class ViewController<Model: Any>: UIViewController {
var model:Model?
}
You can instantiate this view controller like
let vc = ViewController<ListItem>(nibName: "ListViewController", bundle: nil)
or subclass it
class ListViewController: ViewController<ListItem> {
}
and instantiate it like
let vc = ListViewController(nibName: "ListViewController", bundle: nil)
Now it compiles and runs, but you haven't gained much yet, as you cannot wire up your nib with generic properties.
But what you could do is to have a UIView-typed IBOutlet in a non-generic base view controller, subclass it with a generic view controller that has two generic contracts: one for the model, one for the view, ass you most likely want this to be adapted for your model. But now you must have some code that knows how to bring your model on the view. I call this renderer, but you will also find many examples were such an class is called Presenter.
The view controllers:
class BaseRenderViewController: UIViewController {
var renderer: RenderType?
#IBOutlet private weak var privateRenderView: UIView!
var renderView: UIView! {
get { return privateRenderView }
set { privateRenderView = newValue }
}
}
class RenderedContentViewController<Content, View: UIView>: BaseRenderViewController {
var contentRenderer: ContentRenderer<Content, View>? {
return renderer as? ContentRenderer<Content, View>
}
open
override func viewDidLoad() {
super.viewDidLoad()
guard let renderer = contentRenderer, let view = self.renderView as? View else {
return
}
do {
try renderer.render(on: view)
} catch (let error) {
print(error)
}
}
}
The renderers:
protocol RenderType {}
class Renderer<View: UIView>: RenderType {
func render(on view: View) throws {
throw RendererError.methodNotOverridden("\(#function) must be overridden")
}
}
class ContentRenderer<Content, View: UIView>: Renderer<View> {
init(contents: [Content]) {
self.contents = contents
}
let contents: [Content]
override func render(on view: View) throws {
throw RendererError.methodNotOverridden("\(#function) must be overridden")
}
}
You can now subclass ContentRenderer and overwrite the render method to show your content on the view.
tl;dr
By using the approach I just illustrated you can combine any generic view controller with different models, renderers and views. You gain an incredible flexibility — but you won't be able to use storyboards with it.
The answer by #vikingosegundo, while explaining Xcode's complaint and being generally very informative, didn't help me solve my particular problem. My project was started in Xcode 8.3.3 and I already have lots of windows and views in the storyboard so I don't really want to abandon or work around the storyboard/generic issue.
That being said, I did some more research and came to the realization that many people prefer delegation to class inheritance so I decided to explore that approach. I was able to get something working that satisfies my needs.
I present here, a simplified, but functional approach.
First, a protocol that our data models must conform to:
protocol RestModel
{
static var entityName: String { get }
var id: Int { get }
}
Next, a data model:
///
/// A dummy model for testing. It has two properties: an ID and a name.
///
class ModelOne: RestModel
{
static var entityName: String = "ModelOne"
var id: Int
var name: String
init(_ id: Int, _ name: String)
{
self.id = id
self.name = name
}
}
Then, a protocol to which all classes that extend our base class must conform:
///
/// Protocol: ListViewControllerDelegate
///
/// All classes that extend BaseListViewController must conform to this
/// protocol. This allows us to separate all knowledge of the actual data
/// source, record formats, etc. into a view-specific controller.
///
protocol ListViewControllerDelegate: class
{
///
/// The actual table view object. This must be defined in the extending class
/// as #IBOutlet weak var tableView: NSTableView!. The base class saves a weak
/// reference to this variable in one of its local variables and uses that
/// variable to access the actual table view object.
///
weak var tableView: NSTableView! { get }
///
/// This method must perform whatever I/O is required to load the data for the
/// table view. Loading the data is assumed to be asyncronous so the method
/// must accept a closure which must be called after the data has been loaded.
///
func loadRecords()
///
/// This method must simply return the number of rows in the data set.
///
func numberOfRows() -> Int
///
/// This method must return the text that is to be displayed in the specified
/// cell.
/// - parameters:
/// - row: The row number (as supplied in the call to tableView(tableView:viewFor:row:).
/// - col: The column identifier (from tableColumn.identifier).
/// - returns: String
///
func textForCell(row: Int, col: String) -> String
} // ListViewControllerDelegate protocol
Now the actual base class:
class BaseListViewController: NSViewController,
NSTableViewDataSource,
NSTableViewDelegate
{
//
// The instance of the extending class. Like most delegate variables in Cocoa
// applications, this variable must be set by the delegate (the extending
// class, in this case).
//
weak var delegate: ListViewControllerDelegate?
//
// The extending class' actual table view object.
//
weak var delegateTableView: NSTableView!
//
// Calls super.viewDidLoad()
// Gets a reference to the extending class' table view object.
// Sets the data source and delegate for the table view.
// Calls the delegate's loadRecords() method.
//
override func viewDidLoad()
{
super.viewDidLoad()
delegateTableView = delegate?.tableView
delegateTableView.dataSource = self
delegateTableView.delegate = self
delegate?.loadRecords()
delegateTableView.reloadData()
}
//
// This is called by the extending class' table view object to retreive the
// number of rows in the data set.
//
func numberOfRows(in tableView: NSTableView) -> Int
{
return (delegate?.numberOfRows())!
}
//
// This is called by the extending class' table view to retrieve a view cell
// for each column/row in the table. We call the delegate's textForCell(row:col:)
// method to retrieve the text and then create a view cell with that as its
// contents.
//
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{
if let col = tableColumn?.identifier, let text = delegate?.textForCell(row: row, col: col)
{
if let cell = delegate?.tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: nil) as? NSTableCellView
{
cell.textField?.stringValue = text
return cell
}
}
return nil
}
} // BaseListViewController{}
And, finally, an extending class:
///
/// A concrete example class that extends BaseListViewController{}.
/// It loadRecords() method simply uses a hard-coded list.
/// This is the class that is specified in the IB.
///
class ViewOne: BaseListViewController, ListViewControllerDelegate
{
var records: [ModelOne] = []
//
// The actual table view in our view.
//
#IBOutlet weak var tableView: NSTableView!
override func viewDidLoad()
{
super.delegate = self
super.viewDidLoad()
}
func loadRecords()
{
records =
[
ModelOne(1, "AAA"),
ModelOne(2, "BBB"),
ModelOne(3, "CCC"),
ModelOne(4, "DDD"),
]
}
func numberOfRows() -> Int
{
return records.count
}
func textForCell(row: Int, col: String) -> String
{
switch col
{
case "id":
return "\(records[row].id)"
case "name":
return records[row].name
default:
return ""
}
}
} // ViewOne{}
This is, of course, a simplified prototype. In a real-world implementation, loading the records and updating the table would happen in closures after asynchronously loading the data from a database, web service, or some such.
My full prototype defines two models and two view controllers that extend BaseListViewClass. It works as desired. The production version of the base class will contain numerous other methods (which is why a wanted it to be a base class in the first place :-)
Im more familiar with ActionScript3 and see many similarities in Swift2, kind of why i am trying out basic coding in Swift2 and Xcode.
Here's my example:
#IBOutlet weak var b1CurrSpeed: NSTextField!
I want to store b1CurrSpeed as a string so i could access the actual textfield component to set its default value when application is loaded.
I'm aiming for Swift2 for osx apps.
Here is a fictional example, not related to any actual code:
var tf:NSTextField = this.getItem("b1CurrSpeed");
tf.stringValue = "Hello world";
Reason to this approach is following...
I would like to store textfield value in NSUserDefaults, the key for defaults would be name of that textfield. So when looping thru the defaults, i would like to get key as string and when ive got that i'd have access to actual component to set its stringvalue property.
Tho, is that good approach in Swift / xCode ?
If you want to create a function for it, do someting like this:
func getStringForKey(key: String) -> String {
guard let result = NSUserDefaults.standardUserDefaults().objectForKey(key) as! String else { return "" }
return result
}
You can set the TextFields value with myTextField.text
Swift's Mirror type can get you close to it but it is limited to NSObject subclasses, can only access stored properties and is read-only.
Yet, there are ways around these limitations if your requirements will allow.
For example, here's an extension that will save and restore defaults values for all UITextfields on a view controller using the name you gave to each IBOutlet.
extension UIViewController
{
func loadDefaults(userDefaults: NSUserDefaults)
{
for prop in Mirror(reflecting:self).children
{
// add variants for each type/property you want to support
if let field = prop.value as? UITextField,
let name = prop.label
{
if let defaultValue = userDefaults.objectForKey(name) as? String
{ field.text = defaultValue }
}
}
}
func saveDefaults(userDefaults: NSUserDefaults)
{
for prop in Mirror(reflecting:self).children
{
if let field = prop.value as? UITextField,
let name = prop.label
{
if let currentValue = field.text
{ userDefaults.setObject(currentValue, forKey: name) }
}
}
}
}
I'm exploring tvOS and I found that Apple offers nice set of templates written using TVML. I'd like to know if a tvOS app that utilises TVML templates can also use UIKit.
Can I mix UIKit and TVMLKit within one app?
I found a thread on Apple Developer Forum but it does not fully answer this question and I am going through documentation to find an answer.
Yes, you can. Displaying TVML templates requires you to use an object that controls the JavaScript Context: TVApplicationController.
var appController: TVApplicationController?
This object has a UINavigationController property associated with it. So whenever you see fit, you can call:
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
This allows you to push a Custom UIKit viewcontroller onto the navigation stack. If you want to go back to TVML Templates, just pop the viewController off of the navigation stack.
If what you would like to know is how to communicate between JavaScript and Swift, here is a method that creates a javascript function called pushMyView()
func createPushMyView(){
//allows us to access the javascript context
appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
//this is the block that will be called when javascript calls pushMyView()
let pushMyViewBlock : #convention(block) () -> Void = {
() -> Void in
//pushes a UIKit view controller onto the navigation stack
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
}
//this creates a function in the javascript context called "pushMyView".
//calling pushMyView() in javascript will call the block we created above.
evaluation.setObject(unsafeBitCast(pushMyViewBlock, AnyObject.self), forKeyedSubscript: "pushMyView")
}, completion: {(Bool) -> Void in
//done running the script
})
}
Once you call createPushMyView() in Swift, you are free to call pushMyView() in your javascript code and it will push a view controller onto the stack.
SWIFT 4.1 UPDATE
Just a few simple changes to method names and casting:
appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in
and
evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)
As mentioned in the accepted answer, you can call pretty much any Swift function from within the JavaScript context. Note that, as the name implies, setObject:forKeyedSubscript: will also accept objects (if they conform to a protocol that inherits from JSExport) in addition to blocks, allowing you to access methods and properties on that object. Here's an example
import Foundation
import TVMLKit
// Just an example, use sessionStorage/localStorage JS object to actually accomplish something like this
#objc protocol JSBridgeProtocol : JSExport {
func setValue(value: AnyObject?, forKey key: String)
func valueForKey(key: String) -> AnyObject?
}
class JSBridge: NSObject, JSBridgeProtocol {
var storage: Dictionary<String, String> = [:]
override func setValue(value: AnyObject?, forKey key: String) {
storage[key] = String(value)
}
override func valueForKey(key: String) -> AnyObject? {
return storage[key]
}
}
Then in your app controller:
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
let bridge:JSBridge = JSBridge();
jsContext.setObject(bridge, forKeyedSubscript:"bridge");
}
Then you can do this in your JS: bridge.setValue(['foo', 'bar'], "baz")
Not only that, but you can override views for existing elements, or define custom elements to use in your markup, and back them with native views:
// Call lines like these before you instantiate your TVApplicationController
TVInterfaceFactory.sharedInterfaceFactory().extendedInterfaceCreator = CustomInterfaceFactory()
// optionally register a custom element. You could use this in your markup as <loadingIndicator></loadingIndicator> or <loadingIndicator /> with optional attributes. LoadingIndicatorElement needs to be a TVViewElement subclass, and there are three functions you can optionally override to trigger JS events or DOM updates
TVElementFactory.registerViewElementClass(LoadingIndicatorElement.self, forElementName: "loadingIndicator")
Quick custom element example:
import Foundation
import TVMLKit
class LoadingIndicatorElement: TVViewElement {
override var elementName: String {
return "loadingIndicator"
}
internal override func resetProperty(resettableProperty: TVElementResettableProperty) {
super.resetProperty(resettableProperty)
}
// API's to dispatch events to JavaScript
internal override func dispatchEventOfType(type: TVElementEventType, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//super.dispatchEventOfType(type, canBubble: canBubble, cancellable: isCancellable, extraInfo: extraInfo, completion: completion)
}
internal override func dispatchEventWithName(eventName: String, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//...
}
}
And here's how to set up a custom interface factory:
class CustomInterfaceFactory: TVInterfaceFactory {
let kCustomViewTag = 97142 // unlikely to collide
override func viewForElement(element: TVViewElement, existingView: UIView?) -> UIView? {
if (element.elementName == "title") {
if (existingView != nil) {
return existingView
}
let textElement = (element as! TVTextElement)
if (textElement.attributedText!.length > 0) {
let label = UILabel()
// Configure your label here (this is a good way to set a custom font, for example)...
// You can examine textElement.style or textElement.textStyle to get the element's style properties
label.backgroundColor = UIColor.redColor()
let existingText = NSMutableAttributedString(attributedString: textElement.attributedText!)
label.text = existingText.string
return label
}
} else if element.elementName == "loadingIndicator" {
if (existingView != nil && existingView!.tag == kCustomViewTag) {
return existingView
}
let view = UIImageView(image: UIImage(named: "loading.png"))
return view // Simple example. You could easily use your own UIView subclass
}
return nil // Don't call super, return nil when you don't want to override anything...
}
// Use either this or viewForElement for a given element, not both
override func viewControllerForElement(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {
if (element.elementName == "whatever") {
let whateverStoryboard = UIStoryboard(name: "Whatever", bundle: nil)
let viewController = whateverStoryboard.instantiateInitialViewController()
return viewController
}
return nil
}
// Use this to return a valid asset URL for resource:// links for badge/img src (not necessary if the referenced file is included in your bundle)
// I believe you could use this to cache online resources (by replacing resource:// with http(s):// if a corresponding file doesn't exist (then starting an async download/save of the resource before returning the modified URL). Just return a file url for the version on disk if you've already cached it.
override func URLForResource(resourceName: String) -> NSURL? {
return nil
}
}
Unfortunately, view/viewControllerForElement: will not be called for all elements. Some of the existing elements (like collection views) will handle the rendering of their child elements themselves, without involving your interface factory, which means you'll have to override a higher level element, or maybe use a category/swizzling or UIAppearance to get the effect you want.
Finally, as I just implied, you can use UIAppearance to change the way certain built-in views look. Here's the easiest way to change the appearance of your TVML app's tab bar, for example:
// in didFinishLaunching...
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)
If you already have a native UIKit app for tvOS, but would like to extend it by using TVMLKit for some part of it, You can.
Use the TVMLKit as a sub app in your native tvOS app. The following app shows how to do this, by retaining the TVApplicationController and present the navigationController from the TVApplicationController. The TVApplicationControllerContext is used to transfer data to the JavaScript app, as the url is transferred here :
class ViewController: UIViewController, TVApplicationControllerDelegate {
// Retain the applicationController
var appController:TVApplicationController?
static let tvBaseURL = "http://localhost:9001/"
static let tvBootURL = "\(ViewController.tvBaseURL)/application.js"
#IBAction func buttonPressed(_ sender: UIButton) {
print("button")
// Use TVMLKit to handle interface
// Get the JS context and send it the url to use in the JS app
let hostedContContext = TVApplicationControllerContext()
if let url = URL(string: ViewController.tvBootURL) {
hostedContContext.javaScriptApplicationURL = url
}
// Save an instance to a new Sub application, the controller already knows what window we are running so pass nil
appController = TVApplicationController(context: hostedContContext, window: nil, delegate: self)
// Get the navigationController of the Sub App and present it
let navc = appController!.navigationController
present(navc, animated: true, completion: nil)
}
Yes. See the TVMLKit Framework, whose docs start with:
The TVMLKit framework enables you to incorporate JavaScript and TVML files in your binary apps to create client-server apps.
From a quick skim of those docs, it looks like you use the various TVWhateverFactory classes to create UIKit views or view controllers from TVML, after which you can insert them into a UIKit app.