Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync iOS Changes #145

Merged
merged 19 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.4.0
5.4.1
14 changes: 12 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ executors:
working_directory: ~/player
resource_class: macos.x86.medium.gen2
macos:
xcode: 13.4.1
xcode: 14.3
environment:
TZ: "/usr/share/zoneinfo/America/Los_Angeles"
android:
Expand Down Expand Up @@ -112,7 +112,17 @@ jobs:
- attach_workspace:
at: ~/player

- run: HOMEBREW_NO_AUTO_UPDATE=1 brew install bazelisk maven openjdk@8
- run:
name: Homebrew Dependencies
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install bazelisk maven openjdk@8

- run:
name: Set Ruby Version
command: |
rbenv install 2.6.10
rbenv global 2.6.10
rbenv rehash

- restore_cache:
keys:
Expand Down
8 changes: 8 additions & 0 deletions PlayerUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@ and display it as a SwiftUI view comprised of registered assets.
}
end

s.subspec 'SwiftUICheckPathPlugin' do |plugin|
plugin.ios.deployment_target = '13.0'
plugin.dependency 'PlayerUI/Core'
plugin.dependency 'PlayerUI/SwiftUI'
plugin.dependency 'PlayerUI/CheckPathPlugin'
plugin.source_files = 'ios/plugins/SwiftUICheckPathPlugin/Sources/**/*'
end

s.subspec 'TypesProviderPlugin' do |plugin|
plugin.dependency 'PlayerUI/Core'
plugin.source_files = 'ios/plugins/TypesProviderPlugin/Sources/**/*'
Expand Down
8 changes: 8 additions & 0 deletions generated.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ def PlayerUI(
"ios/packages/swiftui/Sources/**/*.c",
"ios/packages/swiftui/Sources/**/*.cc",
"ios/packages/swiftui/Sources/**/*.cpp",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.h",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.hh",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.m",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.mm",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.swift",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.c",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.cc",
"ios/plugins/SwiftUICheckPathPlugin/Sources/**/*.cpp",
"ios/packages/test-utils/Sources/**/*.h",
"ios/packages/test-utils/Sources/**/*.hh",
"ios/packages/test-utils/Sources/**/*.m",
Expand Down
20 changes: 10 additions & 10 deletions ios/packages/core/Sources/Player/HeadlessPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,7 @@ public extension HeadlessPlayer {
- context: The context to load the headless player into
*/
private func loadCore(into context: JSContext) {
guard
let url = ResourceUtilities.urlForFile(
name: "player.prod",
ext: "js",
bundle: Bundle(for: ResourceBundleShim.self),
pathComponent: "PlayerUI.bundle"
),
let jsString = try? String(contentsOf: url, encoding: String.Encoding.utf8)
else { return }
context.evaluateScript(jsString)
context.loadCore()
}

/**
Expand Down Expand Up @@ -307,6 +298,15 @@ public protocol WithSymbol {
internal class ResourceBundleShim {}

internal extension JSContext {
/// Loads the core player bundle into the give JSContext
func loadCore() {
guard
let url = ResourceUtilities.urlForFile(name: "player.prod", ext: "js", bundle: Bundle(for: ResourceBundleShim.self), pathComponent: "PlayerUI.bundle"),
let jsString = try? String(contentsOf: url, encoding: String.Encoding.utf8)
else { return }
evaluateScript(jsString)
}

func getSymbol(_ symbolName: String) -> JSValue? {
guard
let ref = getClassReference(symbolName, load: {_ in}),
Expand Down
35 changes: 29 additions & 6 deletions ios/packages/core/Sources/Types/Assets/BaseAssetRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public enum DecodingError: Error {

/// More than one asset is using the attached identifier
case duplicateIdentifier(String)

/// Update from core player was not convertible
case malformedData
}

/**
Expand Down Expand Up @@ -187,19 +190,39 @@ public struct RegistryDecodeShim<Asset>: Decodable {
extension JSValue {
var jsonDisplayString: String {
do {
return try String(data: jsonData(options: [.prettyPrinted]), encoding: .utf8) ?? "notuf8"
return try String(data: jsonData(pretty: true), encoding: .utf8) ?? "notuf8"
} catch {
return error.localizedDescription
}
}

/// Returns the contents of this value encoded into UTF-8 string data. Throws DecodingError.jsonDataNotFound
/// Returns the contents of this value encoded into UTF-8 string data. Throws DecodingError.malformedData
/// if this value can't be transformed.
func jsonData(options: JSONSerialization.WritingOptions = []) throws -> Data {
guard let object = toObject(), object is NSArray || object is NSDictionary else {
return try JSONSerialization.data(withJSONObject: NSDictionary(), options: options)
func jsonData(pretty: Bool = false) throws -> Data {
guard
let json = context?.objectForKeyedSubscript("JSON"),
!json.isUndefined,
!json.isNull,
// replace functions with placeholders, since we retrieve the value in WrappedFunction
// with the coding path
let replacer = context?.evaluateScript("(key, value) => (typeof value === 'function' ? {} : value)"),
let output = json.invokeMethod(
"stringify",
withArguments: [
self,
replacer as Any,
(
pretty ? 2 : nil
) as Any
]
),
!output.isUndefined,
!output.isNull,
let data = output.toString().data(using: .utf8)
else {
throw DecodingError.malformedData
}
return try JSONSerialization.data(withJSONObject: object, options: options)
return data
}
}

Expand Down
120 changes: 120 additions & 0 deletions ios/packages/core/Sources/Types/Core/BindingParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Foundation
import JavaScriptCore

/// A parser for creating bindings from a string
public class BindingParser {
internal var value: JSValue?
internal var options: BindingParserOptions

/// Create a BindingParser
/// - Parameters:
/// - options: Required options to pass to the core BindingParser
/// - context: The JSContext to create in
public init(options: BindingParserOptions, in context: JSContext) {
self.options = options
let getCallback: @convention(block) (JSValue) -> JSValue? = { binding in
let binding = BindingInstance(from: binding)
return options.get(binding)
}
value = context
.getClassReference("Player.BindingParser", load: { $0.loadCore() })?
.construct(withArguments: [[
"get": JSValue(object: getCallback, in: context) as Any
]])
}

/// Parse a string into a binding
/// - Parameter path: The path to parse into a binding
/// - Returns: a BindingInstance fromt the parsed path
public func parse(path: String) -> BindingInstance? {
value?
.invokeMethod("parse", withArguments: [path])
.map { BindingInstance(from: $0) }
}
}

/// A path in the data model
public class BindingInstance {
internal var value: JSValue?

/// Create a BindingInstance, a path to data in the data model
/// - Parameters:
/// - rawBinding: The string representation of the path
/// - context: The JSContext to create in
public init(rawBinding: String, in context: JSContext) {
value = context
.getClassReference("Player.BindingInstance", load: { $0.loadCore() })?
.construct(withArguments: [rawBinding])
}

/// Create a BindingInstance from an existing JSValue reference
/// - Parameter value: The JSValue that represents a JavaScript BindingInstance
public init(from value: JSValue?) {
self.value = value
}

/// Retrieve this Binding as an array
/// - Returns: The array of segments in the binding
public func asArray() -> [String]? {
return value?
.invokeMethod("asArray", withArguments: [])
.toArray()
/// Array indices are automatically treated as ints, so we need to map
.map { $0 as? String ?? String(describing: $0) }
}

/// Retrieve this Binding as its string form
/// - Returns: The string representation of this binding
public func asString() -> String? {
return value?.invokeMethod("asString", withArguments: []).toString()
}
}

/// Options for ``BindingParser``
public struct BindingParserOptions {
/// Get the value for a specific binding
public var get: (BindingInstance) -> JSValue?

/// Create BindingParserOptions
/// - Parameter get: A callback to retrieve nested values
public init(
get: @escaping (BindingInstance) -> JSValue?
) {
self.get = get
}
}

/// A data model that stores things in an in-memory JS object
public class LocalModel {
/// Reference to the JavaScript LocalModel
internal var value: JSValue?

/// Create a new LocalModel
/// - Parameters:
/// - data: The data to start with
/// - context: The JSContext to create it in
public init(data: [String: Any] = [:], in context: JSContext) {
value = context.getClassReference("Player.LocalModel", load: { $0.loadCore() })?.construct(withArguments: [data])
}

/// Get a value in the model for a given binding
/// - Parameter binding: The binding for where the data is
/// - Returns: The data at the binding
public func get(binding: BindingInstance) -> JSValue? {
value?.invokeMethod("get", withArguments: [binding.value as Any])
}

/// Set data in the model
/// - Parameter transaction: An array of transaction objects, tuples of ``BindingInstance`` and the value to set
public func set(transaction: [(BindingInstance, Any)]) {
value?.invokeMethod("set", withArguments: [transaction.map { [$0.0.value as Any, $0.1] as [Any] }])
}

/// Get a value in the model for a given path
/// - Parameter path: The string path to use for creating a binding
/// - Returns: The data at the path
public func get(path: String) -> JSValue? {
let binding = value?.context.map { BindingInstance(rawBinding: path, in: $0) }
return binding.flatMap { get(binding: $0) }
}
}
Loading