Skip to content
Open
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
16 changes: 14 additions & 2 deletions Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.companionObject
#endif

public protocol EnvironmentKey {
Expand All @@ -41,6 +41,18 @@ public protocol EnvironmentKeyCompanion {
associatedtype Value
var defaultValue: Value { get }
}

/// Companion object instance for `type`, via Java reflection with `isAccessible = true`.
/// Kotlin's `companionObjectInstance` omits that and throws `IllegalAccessException` for
/// non-`public` key types, whose generated class is package-private (issue #243).
func companionInstance(ofType type: Any.Type) -> Any? {
var instance: Any? = nil
// SKIP INSERT: val name = type.companionObject?.simpleName ?: "Companion"
// SKIP INSERT: val field = type.java.getDeclaredField(name)
// SKIP INSERT: field.isAccessible = true
// SKIP INSERT: instance = field.get(null)
return instance
}
#endif

// Model as a class because our implementation only holds the global environment keys, and so does not need to copy.
Expand Down Expand Up @@ -145,7 +157,7 @@ public final class EnvironmentValues {

/// The Compose `CompositionLocal` for the given environment value key type.
public func valueCompositionLocal(key: Any.Type) -> ProvidableCompositionLocal<Any> {
// SKIP INSERT: val defaultValue = { (key.companionObjectInstance as EnvironmentKeyCompanion<*>).defaultValue }
// SKIP INSERT: val defaultValue = { (companionInstance(key) as EnvironmentKeyCompanion<*>).defaultValue }
return compositionLocal(key: key, defaultValue: defaultValue)
}

Expand Down
7 changes: 3 additions & 4 deletions Sources/SkipUI/SkipUI/Environment/PreferenceKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import kotlin.reflect.full.companionObjectInstance
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
#endif
Expand Down Expand Up @@ -137,7 +136,7 @@ struct PreferenceCollector<Value> {

init(key: Any.Type, initialValue: Value? = nil) {
self.key = key
let companion = key.companionObjectInstance as! PreferenceKeyCompanion<Value>
let companion = companionInstance(ofType: key) as! PreferenceKeyCompanion<Value>
self.initialValue = initialValue ?? companion.defaultValue
self.reducer = { value, nextValue in
var updatedValue = value
Expand Down Expand Up @@ -200,8 +199,8 @@ extension View {
public func onPreferenceChange<K>(_ key: K.Type /* = K.self */, perform action: @escaping (K.Value) -> Void) -> any View where K : PreferenceKey {
#if SKIP
// Work around transpiler bug
// SKIP REPLACE: val companion = key.companionObjectInstance as PreferenceKeyCompanion<V>
let companion = key.companionObjectInstance as! PreferenceKeyCompanion<V>
// SKIP REPLACE: val companion = companionInstance(key) as PreferenceKeyCompanion<V>
let companion = companionInstance(ofType: key) as! PreferenceKeyCompanion<V>
return onPreferenceChange(key: key, defaultValue: companion.defaultValue, reducer: { value, nextValue in
var updatedValue = value as! V
companion.reduce(value: &updatedValue, nextValue: { nextValue as! V })
Expand Down
36 changes: 33 additions & 3 deletions Tests/SkipUITests/SkipUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,28 @@ final class SkipUITests: SkipUITestCase {
}
}

// Reading a non-public `PreferenceKey` reflects its companion the same way a
// non-public `EnvironmentKey` does. Composing `.onPreferenceChange` for a private
// key must not throw `IllegalAccessException` (issue #243); the label renders only
// if composition completes without that crash.
func testCustomPreferenceKey() throws {
try testUI(view: {
PreferenceValueView()
.accessibilityIdentifier("test-view")
}, eval: { rule in
try check(rule, id: "preference-label", hasText: "content")
})
}
struct PreferenceValueView: View {
var body: some View {
VStack {
Text("content")
.accessibilityIdentifier("preference-label")
}
.onPreferenceChange(PreferenceTestKey.self) { _ in }
}
}

// func testObservability() throws {
// try testUI(view: {
// ObservablesOuterView()
Expand Down Expand Up @@ -1005,13 +1027,21 @@ extension SemanticsNode {
#endif

// Used in `testCustomEnvironmentValue`
public struct EnvironmentValueTestKey: EnvironmentKey {
public static let defaultValue = "default"
private struct EnvironmentValueTestKey: EnvironmentKey {
static let defaultValue = "default"
}

extension EnvironmentValues {
public var testValue: String {
fileprivate var testValue: String {
get { self[EnvironmentValueTestKey.self] }
set { self[EnvironmentValueTestKey.self] = newValue }
}
}

// Used in `testCustomPreferenceKey`; non-public to reproduce issue #243.
private struct PreferenceTestKey: PreferenceKey {
static let defaultValue = "preferred"
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
Loading