Skip to content

Commit

Permalink
Sync iOS Changes (#145)
Browse files Browse the repository at this point in the history
* sync extended AnyType and usage in BeaconPlugin and PubSubPlugin

* sync nav state refactor and hook updates

* add SwiftUICheckPathPlugin

* clear external state for viewmodifier if state transitions from another plugin

* handle NaN -> nil when decoding view updates

* sync allow wrapped function to return custom decodable type

* mark some metrics data optional for non VIEW states

* make DefaultBeacon Codable and Hashable

* Expose LocalModel, BindingParser in swift

* bump circle to use xcode 14.3

* try lowering ruby version to satisfy cocoapods

* fix indent

* pick specific ruby version

* only init rbenv through bashrc

* try chruby instead of rbenv

* try circle step

* try using preinstalled rbenv

* rbenv install

* bump bazel to 5.4.1
  • Loading branch information
hborawski authored Aug 30, 2023
1 parent e7681a2 commit 82270dc
Show file tree
Hide file tree
Showing 37 changed files with 1,468 additions and 188 deletions.
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

0 comments on commit 82270dc

Please sign in to comment.