Skip to content

Commit 93c5f75

Browse files
committed
feat: add swift haptics native module
1 parent 8c98b12 commit 93c5f75

File tree

9 files changed

+213
-59
lines changed

9 files changed

+213
-59
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
enum NotificationFeedbackType: String {
2+
case success
3+
case warning
4+
case error
5+
6+
var hapticType: UINotificationFeedbackGenerator.FeedbackType {
7+
switch self {
8+
case .success: return .success
9+
case .warning: return .warning
10+
case .error: return .error
11+
}
12+
}
13+
}
14+
15+
enum ImpactFeedbackType: String {
16+
case light
17+
case medium
18+
case heavy
19+
case soft
20+
case rigid
21+
22+
var hapticType: UIImpactFeedbackGenerator.FeedbackStyle {
23+
switch self {
24+
case .light: return .light
25+
case .medium: return .medium
26+
case .heavy: return .heavy
27+
case .soft: return .soft
28+
case .rigid: return .rigid
29+
}
30+
}
31+
}
32+
33+
@objc(StreamChatHapticsModule)
34+
public class StreamChatHapticsModule: NSObject {
35+
@objc
36+
func notificationFeedback(_ typeString: String) {
37+
guard let type = NotificationFeedbackType(rawValue: typeString) else {
38+
return
39+
}
40+
41+
DispatchQueue.main.async {
42+
let generator = UINotificationFeedbackGenerator()
43+
generator.prepare()
44+
generator.notificationOccurred(type.hapticType)
45+
}
46+
}
47+
48+
@objc
49+
func impactFeedback(_ typeString: String) {
50+
guard let type = ImpactFeedbackType(rawValue: typeString) else {
51+
return
52+
}
53+
54+
DispatchQueue.main.async {
55+
let generator = UIImpactFeedbackGenerator(style: type.hapticType)
56+
generator.prepare()
57+
generator.impactOccurred()
58+
}
59+
}
60+
61+
@objc
62+
func selectionFeedback() {
63+
DispatchQueue.main.async {
64+
let generator = UISelectionFeedbackGenerator()
65+
generator.prepare()
66+
generator.selectionChanged()
67+
}
68+
}
69+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// StreamChatHapticsModuleBridge.m
2+
3+
#import <React/RCTBridgeModule.h>
4+
5+
@interface RCT_EXTERN_MODULE(StreamChatHapticsModule, NSObject)
6+
7+
RCT_EXTERN_METHOD(notificationFeedback: (NSString *)type)
8+
RCT_EXTERN_METHOD(impactFeedback: (NSString *)type)
9+
RCT_EXTERN_METHOD(selectionFeedback)
10+
11+
@end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { TurboModule } from 'react-native';
2+
3+
import { TurboModuleRegistry } from 'react-native';
4+
5+
export interface Spec extends TurboModule {
6+
notificationFeedback(type: 'success' | 'warning' | 'error'): void;
7+
impactFeedback(type: 'light' | 'medium' | 'heavy' | 'soft' | 'rigid'): void;
8+
selectionFeedback(): void;
9+
}
10+
11+
export default TurboModuleRegistry.getEnforcing<Spec>('StreamChatHapticsModule');

package/native-package/src/native/index.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import { NativeModules } from 'react-native';
22

3-
import type { Options, ResizeFormat, Response } from './types';
3+
import type {
4+
ImpactFeedbackType,
5+
NotificationFeedbackType,
6+
Options,
7+
ResizeFormat,
8+
Response,
9+
} from './types';
410
export type { ResizeFormat, ResizeMode, Response } from './types';
11+
import NativeStreamChatHapticsModule from './NativeStreamChatHapticsModule';
12+
import NativeStreamChatReactNative from './NativeStreamChatReactNative';
513

614
// @ts-ignore
715
// eslint-disable-next-line no-underscore-dangle
816
const isTurboModuleEnabled = global.__turboModuleProxy != null;
917

1018
const ImageResizer = isTurboModuleEnabled
11-
? require('./NativeStreamChatReactNative').default
19+
? NativeStreamChatReactNative
1220
: NativeModules.StreamChatReactNative;
1321

22+
const Haptics = isTurboModuleEnabled
23+
? NativeStreamChatHapticsModule
24+
: NativeModules.StreamChatHapticsModule;
25+
1426
const defaultOptions: Options = {
1527
mode: 'contain',
1628
onlyScaleDown: false,
@@ -43,6 +55,18 @@ async function createResizedImage(
4355
);
4456
}
4557

58+
export async function notificationFeedback(type: NotificationFeedbackType) {
59+
await Haptics.notificationFeedback(type);
60+
}
61+
62+
export async function impactFeedback(type: ImpactFeedbackType) {
63+
await Haptics.impactFeedback(type);
64+
}
65+
66+
export async function selectionFeedback() {
67+
await Haptics.selectionFeedback();
68+
}
69+
4670
export default {
4771
createResizedImage,
4872
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* The type of notification feedback generated by a `UINotificationFeedbackGenerator` object.
3+
* [`UINotificationFeedbackType`](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/feedbacktype).
4+
*/
5+
export enum NotificationFeedbackType {
6+
/**
7+
* A notification feedback type indicating that a task has completed successfully.
8+
*/
9+
Success = 'success',
10+
/**
11+
* A notification feedback type indicating that a task has produced a warning.
12+
*/
13+
Warning = 'warning',
14+
/**
15+
* A notification feedback type indicating that a task has failed.
16+
*/
17+
Error = 'error',
18+
}
19+
20+
/**
21+
* The mass of the objects in the collision simulated by a `UIImpactFeedbackGenerator` object
22+
* [`UINotificationFeedbackStyle`](https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/feedbackstyle).
23+
*/
24+
export enum ImpactFeedbackType {
25+
/**
26+
* A collision between small, light user interface elements.
27+
*/
28+
Light = 'light',
29+
/**
30+
* A collision between moderately sized user interface elements.
31+
*/
32+
Medium = 'medium',
33+
/**
34+
* A collision between large, heavy user interface elements.
35+
*/
36+
Heavy = 'heavy',
37+
/**
38+
* A collision between user interface elements that are soft, exhibiting a large amount of compression or elasticity.
39+
*/
40+
Soft = 'soft',
41+
/**
42+
* A collision between user interface elements that are rigid, exhibiting a small amount of compression or elasticity.
43+
*/
44+
Rigid = 'rigid',
45+
}
File renamed without changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './haptics';
2+
export * from './imageResizer';
Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,36 @@
1-
let ReactNativeHapticFeedback;
1+
import { impactFeedback, notificationFeedback, selectionFeedback } from '../native';
22

3-
try {
4-
ReactNativeHapticFeedback = require('react-native-haptic-feedback').default;
5-
} catch (e) {
6-
console.warn('react-native-haptic-feedback is not installed.');
7-
}
3+
import { ImpactFeedbackType, NotificationFeedbackType } from '../native/types';
84

9-
/**
10-
* Since react-native-haptic-feedback isn't installed by default, we've
11-
* copied the types from the package here.
12-
*
13-
* @see https://github.com/junina-de/react-native-haptic-feedback/blob/master/index.d.ts
14-
* */
15-
export type HapticFeedbackTypes =
16-
| 'selection'
5+
type HapticFeedbackTypes =
6+
| 'impactHeavy'
177
| 'impactLight'
188
| 'impactMedium'
19-
| 'impactHeavy'
20-
| 'rigid'
21-
| 'soft'
22-
| 'notificationSuccess'
23-
| 'notificationWarning'
249
| 'notificationError'
25-
| 'clockTick'
26-
| 'contextClick'
27-
| 'keyboardPress'
28-
| 'keyboardRelease'
29-
| 'keyboardTap'
30-
| 'longPress'
31-
| 'textHandleMove'
32-
| 'virtualKey'
33-
| 'virtualKeyRelease'
34-
| 'effectClick'
35-
| 'effectDoubleClick'
36-
| 'effectHeavyClick'
37-
| 'effectTick';
10+
| 'notificationSuccess'
11+
| 'notificationWarning';
3812

39-
export const triggerHaptic = ReactNativeHapticFeedback
40-
? (method: HapticFeedbackTypes) => {
41-
ReactNativeHapticFeedback.trigger(method, {
42-
enableVibrateFallback: false,
43-
ignoreAndroidSystemSettings: false,
44-
});
45-
}
46-
: () => {};
13+
export const triggerHaptic = async (method: HapticFeedbackTypes) => {
14+
switch (method) {
15+
case 'impactHeavy':
16+
await impactFeedback(ImpactFeedbackType.Heavy);
17+
break;
18+
case 'impactLight':
19+
await impactFeedback(ImpactFeedbackType.Light);
20+
break;
21+
case 'impactMedium':
22+
await impactFeedback(ImpactFeedbackType.Medium);
23+
break;
24+
case 'notificationError':
25+
await notificationFeedback(NotificationFeedbackType.Error);
26+
break;
27+
case 'notificationSuccess':
28+
await notificationFeedback(NotificationFeedbackType.Success);
29+
break;
30+
case 'notificationWarning':
31+
await notificationFeedback(NotificationFeedbackType.Warning);
32+
break;
33+
default:
34+
await selectionFeedback();
35+
}
36+
};

package/native-package/stream-chat-react-native.podspec

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,30 @@ Pod::Spec.new do |s|
1111
s.license = package["license"]
1212
s.authors = package["author"]
1313

14-
s.platforms = { :ios => "10.0" }
14+
s.platforms = { :ios => "13.0" }
1515
s.source = { :git => "./ios", :tag => "#{s.version}" }
1616

17-
s.source_files = "ios/**/*.{h,m,mm}"
17+
s.source_files = "ios/**/*.{h,m,mm,swift}"
1818

19-
s.dependency "React-Core"
2019
s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
2120

22-
# Don't install the dependencies when we run `pod install` in the old architecture.
23-
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
24-
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
25-
s.pod_target_xcconfig = {
26-
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
27-
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
28-
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
29-
}
30-
s.dependency "React-Codegen"
31-
s.dependency "RCT-Folly"
32-
s.dependency "RCTRequired"
33-
s.dependency "RCTTypeSafety"
34-
s.dependency "ReactCommon/turbomodule/core"
21+
if respond_to?(:install_modules_dependencies, true)
3522
install_modules_dependencies(s)
23+
else
24+
s.dependency "React-Core"
25+
# Don't install the dependencies when we run `pod install` in the old architecture.
26+
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
27+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
28+
s.pod_target_xcconfig = {
29+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
30+
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
31+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
32+
}
33+
s.dependency "React-Codegen"
34+
s.dependency "RCT-Folly"
35+
s.dependency "RCTRequired"
36+
s.dependency "RCTTypeSafety"
37+
s.dependency "ReactCommon/turbomodule/core"
38+
end
3639
end
3740
end
38-

0 commit comments

Comments
 (0)