Skip to content

Commit

Permalink
Support HapticFeedback on iOS (#1255)
Browse files Browse the repository at this point in the history
## Proposed Changes

Bridge `HapticFeedback` interface with `UIFeedbackGenerator`s, provide
the constructed bridge as static composition local.

## Testing

Test: APIs that request a haptic feedback should trigger one on iOS
(text selection, on-demand, etc)

## Issues Fixed

Fixes: JetBrains/compose-multiplatform#4598
  • Loading branch information
elijah-semyonov committed Apr 10, 2024
1 parent 7f4929c commit fb98098
Show file tree
Hide file tree
Showing 17 changed files with 147 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.mpp.demo

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp

val HapticFeedbackExample = Screen.Example("Haptic feedback") {
val feedback = LocalHapticFeedback.current

Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = {
feedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}) {
Text("TextHandleMove")
}

Spacer(Modifier.height(16.dp))

Button(onClick = {
feedback.performHapticFeedback(HapticFeedbackType.LongPress)
}) {
Text("LongPress")
}
}
}

val IosSpecificFeatures = Screen.Selection(
"iOS-specific features",
NativeModalWithNaviationExample,
HapticFeedbackExample,
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.mpp.demo

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -11,10 +29,8 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.mpp.demo.Screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
Expand All @@ -31,8 +47,6 @@ import androidx.compose.ui.window.ComposeUIViewController
import androidx.lifecycle.LifecycleEventObserver
import platform.UIKit.*
import platform.Foundation.*
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue

/*
* Copyright 2023 The Android Open Source Project
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.mpp.demo

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.mpp.demo.Screen
import androidx.compose.mpp.demo.bug.BackspaceIssue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.background
import androidx.compose.foundation.border
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package bugs
package androidx.compose.mpp.demo.bugs

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Use `xcodegen` first, then `open ./SkikoSample.xcodeproj` and then Run button in XCode.
package androidx.compose.mpp.demo

import NativeModalWithNaviationExample
import SwiftUIInteropExample
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.remember
Expand All @@ -11,8 +9,8 @@ import androidx.compose.ui.main.defaultUIKitMain
import androidx.compose.ui.platform.AccessibilityDebugLogger
import androidx.compose.ui.platform.AccessibilitySyncOptions
import androidx.compose.ui.window.ComposeUIViewController
import bugs.IosBugs
import bugs.StartRecompositionCheck
import androidx.compose.mpp.demo.bugs.IosBugs
import androidx.compose.mpp.demo.bugs.StartRecompositionCheck
import platform.UIKit.UIViewController

@OptIn(ExperimentalComposeApi::class, ExperimentalComposeUiApi::class)
Expand Down Expand Up @@ -41,7 +39,7 @@ fun IosDemo(arg: String, makeHostingController: ((Int) -> UIViewController)? = n
App(
extraScreens = listOf(
IosBugs,
NativeModalWithNaviationExample,
IosSpecificFeatures,
) + listOf(makeHostingController).mapNotNull {
it?.let {
SwiftUIInteropExample(it)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.ui.hapticfeedback

import platform.UIKit.UIImpactFeedbackGenerator
import platform.UIKit.UISelectionFeedbackGenerator

// TODO: minor UX improvement, add `prepare()` calls when internal APIs are likely to use HapticFeedback
// (e.g. pan started during the text selection) to reduce haptic feedback latency
// see https://developer.apple.com/documentation/uikit/uifeedbackgenerator
internal class CupertinoHapticFeedback : HapticFeedback {
private val impactGenerator = UIImpactFeedbackGenerator()
private val selectionGenerator = UISelectionFeedbackGenerator()

override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {
when (hapticFeedbackType) {
HapticFeedbackType.LongPress -> impactGenerator.impactOccurred()
HapticFeedbackType.TextHandleMove -> selectionGenerator.selectionChanged()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.hapticfeedback.CupertinoHapticFeedback
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.interop.UIKitInteropContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformWindowContext
Expand Down Expand Up @@ -96,6 +98,7 @@ internal class ComposeContainer(
private val content: @Composable () -> Unit,
) : CMPViewController(nibName = null, bundle = null) {
val lifecycleOwner = ViewControllerBasedLifecycleOwner()
val hapticFeedback = CupertinoHapticFeedback()

private var isInsideSwiftUI = false
private var mediator: ComposeSceneMediator? = null
Expand Down Expand Up @@ -426,6 +429,7 @@ internal fun ProvideContainerCompositionLocals(
content: @Composable () -> Unit,
) = with(composeContainer) {
CompositionLocalProvider(
LocalHapticFeedback provides hapticFeedback,
LocalUIViewController provides this,
LocalInterfaceOrientation provides interfaceOrientationState.value,
LocalSystemTheme provides systemThemeState.value,
Expand Down

0 comments on commit fb98098

Please sign in to comment.