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

Revisit iOS interop API #1494

Merged
merged 68 commits into from
Aug 28, 2024
Merged

Revisit iOS interop API #1494

merged 68 commits into from
Aug 28, 2024

Conversation

elijah-semyonov
Copy link

@elijah-semyonov elijah-semyonov commented Aug 13, 2024

Changes

Revisit iOS interop API to build upon new implementation of InteropView.
Main functional additions are:
Support of onReset, fine-grain touches strategy control (cooperative with explicit time delay, eager, none)ю

Fixes 5719, 5876

Testing

TBD, new/updated demo pages.

This should be tested by QA

Release Notes

Breaking changes - iOS

  • UIKitView and UIKitViewController in package androidx.compose.ui.interop are deprecated. New API are mentioned in deprecation message. Deprecated invocations should work fine unless custom onResize is used, it is disallowed now and will print a warning.

Features - iOS

  • New UIKitView and UIKitViewController API in package androidx.compose.ui.viewinterop. Support of onReset to reuse the interop composable emitted node and avoid excessive native views reallocations, fine-grain touches strategy control (cooperative with explicit time delay, non-cooperative where no touches are received by Compose, ignoring touches).

* @see Modifier.semantics
*/
@Deprecated(
message = "Use androidx.compose.ui.viewinterop.UIKitView instead"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore the doc and add replaceWith as separate parameter instead of writing it in the message

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Despite the name, onResize actually contains the logic for default resizing strategy.
// Since this strategy is already implied, changes to this argument can't be processed in a
// sane manner
require(onResize == DefaultViewResize) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a breaking change if it was used previously

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it didn't make sense because it basically required user to set a proper frame of interop view, which was already laid out by compose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter. It can be used in libraries that the user doesn't control, so it will become a blocker for Compose update

Copy link
Author

@elijah-semyonov elijah-semyonov Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When did we stabilise this API in the first place?
I mean, I just don't want to carry a trail of compatibility code on API, that was experimental due to target being experimental.

Comment on lines 175 to 176
callbacks?.run {
newUserComponentSize?.let {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: just regular if? double lambda wrapping seems weird

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a side effect of my experimentation with reassignable callbacks/properties and callbacks being var. Changed it.

/**
* Check that [group] doesn't entirely clip a child view with a [cgRect]
*/
private fun isCgRectEntirelyClippedByGroup(cgRect: CValue<CGRect>): Boolean =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems semantic of this is simpler than name isVisible probably?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, refactored it.

@Immutable
data class UIKitInteropProperties @ExperimentalComposeUiApi constructor(
internal val interactionMode: UIKitInteropInteractionMode? = UIKitInteropInteractionMode.Cooperative(),
internal val isNativeAccessibilityEnabled: Boolean = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are getters internal?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's only needed for us.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure. This is kind of an object where you just wrap a few booleans/enums. The expectation here is full creating + reading for the ability to create a new instance based on the some values from the existing one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's make it public

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved

* All the changes are executed synchronously right after the state change on a main thread
* in sync with a CATransaction batching UIKit objects changes to sync it with Compose rendering.
*/
interface UIKitInteropCallbacks<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIKitInteropListener?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds better

* - View receives touches with 150ms delay, allowing compose to intercept them.
* - Native accessibility resolution is disabled
*/
val Default = UIKitInteropProperties()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that we want this as part of a public API

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you like to do here?
I think we'd still expand this constructor/argument types, so want to keep at least a stable part without having a constructor overloads combinatory explosion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean this particular default value. Let's make it internal for now (still can be used as default arg)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it internal

Copy link
Member

@MatkovIvan MatkovIvan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, such new APIs require a second reviewer

* @property delay Indicates how much time in milliseconds is given for Compose to intercept
* the touches before delivering them to the interop view. The default value is [DEFAULT_DELAY].
*/
class Cooperative(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's explicitly mark it as @ExperimentalComposeUiApi

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* main thread in sync with a CATransaction batching UIKit objects changes to sync it with Compose
* rendering.
*/
interface UIKitInteropListener<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the final shape of this API? public-stable is intended here?

Copy link
Author

@elijah-semyonov elijah-semyonov Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I've requested some insight from our dev advocates, so it could be not

@Immutable
data class UIKitInteropProperties @ExperimentalComposeUiApi constructor(
internal val interactionMode: UIKitInteropInteractionMode? = UIKitInteropInteractionMode.Cooperative(),
internal val isNativeAccessibilityEnabled: Boolean = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved

@elijah-semyonov elijah-semyonov marked this pull request as draft August 19, 2024 07:48
@elijah-semyonov elijah-semyonov marked this pull request as ready for review August 20, 2024 07:20
@elijah-semyonov
Copy link
Author

I rolled back additional APIs, only onReset is included.

@Gaubee
Copy link

Gaubee commented Aug 21, 2024

Hope you can review and merge this PR as soon as possible. The change in touch strategy is having a big impact on our project, and we really need to push out a version that behaves correctly for our clients.

Thanks! 😊

@elijah-semyonov
Copy link
Author

Hope you can review and merge this PR as soon as possible. The change in touch strategy is having a big impact on our project, and we really need to push out a version that behaves correctly for our clients.

Thanks! 😊

You mean new touches strategy brought in recent alpha?

@kingsword09
Copy link

Hope you can review and merge this PR as soon as possible. The change in touch strategy is having a big impact on our project, and we really need to push out a version that behaves correctly for our clients.
Thanks! 😊

You mean new touches strategy brought in recent alpha?

Yes, in youtrack is this.

@elijah-semyonov
Copy link
Author

No worries, it's in its presumably final state, so it will be merged after reviewed.
But please, don't use alpha and dev builds for production.

@Gaubee
Copy link

Gaubee commented Aug 21, 2024

No worries, it's in its presumably final state, so it will be merged after reviewed.
But please, don't use alpha and dev builds for production.

Understand, but because we are happy to conduct active exploration, we will provide customers with some experimental versions.

.trackInteropPlacement(this)
.onGloballyPositioned { layoutCoordinates ->
layoutAccordingTo(layoutCoordinates)
// TODO: Should be the same as [Owner.onInteropViewLayoutChange]?
// container.onInteropViewLayoutChange(this)
// container.onInteropViewLayoutChange(this)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a reminder for investigation after upstream merge, ask @MatkovIvan for more details.

@JetBrains JetBrains deleted a comment from ASalavei Aug 21, 2024
Copy link
Member

@MatkovIvan MatkovIvan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the current shape of it, thanks!
(aside of InteropPlatformDetails in shared code 🙃 )

Also, could you please update PR description/release notes (no breaking changes here)

* chain needs to be changed within ComposeNode Updater
*/
internal interface InteropPlatformDetails {
fun platformModifier(holder: InteropViewHolder): Modifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't it a part of InteropContainer? Those 2 booleans don't seem as platform-specific and can be mentioned in the common interface as parameters in this function.

or. it might be converted to something like

internal fun interface InteropViewModifierFactory {
    fun create(holder: InteropViewHolder): Modifier
}

and probably make some default implementation(s) in shared code (outside of this PR).

Copy link
Author

@elijah-semyonov elijah-semyonov Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Native platform accessibility resolution is absolutely platform specific, touch interaction mode is also platform specific.
The point of InteropPlatformDetails is an ability to change something in the implementation of specific InteropViewHolder when it's reused. It can cover basically anything what happens here (in case of iOS - not only change of a modifier, but also - reconfiguration of InteropWrappingView which serves as a group. In future it could be used for expanding gesture recogniser failure requirements behavior and forwarding it to the user.

interopWrappingView.interactionMode = null
} else {
val uiKitInteropPlatformDetails =
checkNotNull(platformDetails as? UIKitInteropPlatformDetails) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving the previous comment should remove this cast, I guess

@elijah-semyonov elijah-semyonov merged commit 538f5ce into jb-main Aug 28, 2024
6 checks passed
@elijah-semyonov elijah-semyonov deleted the es/revisit-ios-interop-api branch August 28, 2024 08:13
vickyleu added a commit to vickyleu/compose-webview-multiplatform that referenced this pull request Aug 31, 2024
@vickyleu
Copy link

1.7.0-dev1783
In a full-screen WebView, to ensure smooth scrolling, you must set

properties = UIKitInteropProperties(
                    interactionMode= UIKitInteropInteractionMode.Cooperative(
                        delayMillis = 1,
                    ),
                    isNativeAccessibilityEnabled = false
)

I still don't quite understand the purpose of delaying touch events. Why is delay necessary? In most cases, delayMillis doesn't make much sense, as in scrollable scenarios, no one would hold down and wait for the page to become scrollable.

vickyleu added a commit to vickyleu/compose-webview-multiplatform that referenced this pull request Aug 31, 2024
@Gaubee
Copy link

Gaubee commented Sep 1, 2024

1.7.0-dev1783 In a full-screen WebView, to ensure smooth scrolling, you must set

properties = UIKitInteropProperties(
                    interactionMode= UIKitInteropInteractionMode.Cooperative(
                        delayMillis = 1,
                    ),
                    isNativeAccessibilityEnabled = false
)

I still don't quite understand the purpose of delaying touch events. Why is delay necessary? In most cases, delayMillis doesn't make much sense, as in scrollable scenarios, no one would hold down and wait for the page to become scrollable.

  1. Why no use interactionMode = UIKitInteropInteractionMode.NonCooperative?
  2. In most cases, a lightweight UIView just needs a tap event, and WKWebView is a heavyweight UIView, so it has its own scroll event. Scroll delay is the problem that scrolling gestures are not consumed by UIView in a long list of mixed rendered UIViews.

@elijah-semyonov
Copy link
Author

elijah-semyonov commented Sep 2, 2024

@vickyleu
No, you need to set NonCooperative. @Gaubee explained the rationale well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants