react-native-tinyui 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +468 -28
- package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
- package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
- package/ios/Core/Tinyui.mm +54 -0
- package/ios/Core/TinyuiComponentClasses.mm +44 -0
- package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
- package/ios/Menu/TinyuiMenuProvider.swift +141 -75
- package/ios/Menu/TinyuiMenuView.mm +24 -35
- package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
- package/ios/Popover/TinyuiPopoverView.mm +26 -1
- package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
- package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
- package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
- package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
- package/ios/Stepper/TinyuiStepperView.h +12 -0
- package/ios/Stepper/TinyuiStepperView.mm +128 -0
- package/ios/TipKit/TinyuiTip.swift +31 -0
- package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
- package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
- package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
- package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
- package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/lib/module/ConcentricView/index.js +29 -0
- package/lib/module/ConcentricView/index.js.map +1 -0
- package/lib/module/ConcentricView/types.js +4 -0
- package/lib/module/ConcentricView/types.js.map +1 -0
- package/lib/module/Menu/MenuNativeComponent.ts +12 -3
- package/lib/module/Menu/index.js +17 -12
- package/lib/module/Menu/index.js.map +1 -1
- package/lib/module/Menu/options.js +4 -12
- package/lib/module/Menu/options.js.map +1 -1
- package/lib/module/NativeTinyui.js.map +1 -1
- package/lib/module/Popover/index.js +4 -2
- package/lib/module/Popover/index.js.map +1 -1
- package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/lib/module/SFSymbol/index.js +73 -0
- package/lib/module/SFSymbol/index.js.map +1 -0
- package/lib/module/SFSymbol/options.js +79 -0
- package/lib/module/SFSymbol/options.js.map +1 -0
- package/lib/module/SFSymbol/types.js +4 -0
- package/lib/module/SFSymbol/types.js.map +1 -0
- package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
- package/lib/module/Stepper/index.js +75 -0
- package/lib/module/Stepper/index.js.map +1 -0
- package/lib/module/Stepper/types.js +4 -0
- package/lib/module/Stepper/types.js.map +1 -0
- package/lib/module/TipKit/Popover.js +89 -0
- package/lib/module/TipKit/Popover.js.map +1 -0
- package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/lib/module/TipKit/index.js +28 -0
- package/lib/module/TipKit/index.js.map +1 -0
- package/lib/module/TipKit/types.js +4 -0
- package/lib/module/TipKit/types.js.map +1 -0
- package/lib/module/TipKit/validation.js +24 -0
- package/lib/module/TipKit/validation.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/getNativeTinyui.js.map +1 -1
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
- package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
- package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +8 -4
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/Menu/index.d.ts +1 -1
- package/lib/typescript/src/Menu/index.d.ts.map +1 -1
- package/lib/typescript/src/Menu/options.d.ts +1 -5
- package/lib/typescript/src/Menu/options.d.ts.map +1 -1
- package/lib/typescript/src/Menu/types.d.ts +26 -7
- package/lib/typescript/src/Menu/types.d.ts.map +1 -1
- package/lib/typescript/src/NativeTinyui.d.ts +2 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
- package/lib/typescript/src/Popover/index.d.ts.map +1 -1
- package/lib/typescript/src/Popover/types.d.ts +2 -0
- package/lib/typescript/src/Popover/types.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
- package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
- package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
- package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/index.d.ts +4 -0
- package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/types.d.ts +24 -0
- package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
- package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/index.d.ts +10 -0
- package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/types.d.ts +46 -0
- package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/validation.d.ts +4 -0
- package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
- package/package.json +37 -2
- package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
- package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/src/ConcentricView/index.tsx +34 -0
- package/src/ConcentricView/types.ts +6 -0
- package/src/Menu/MenuNativeComponent.ts +12 -3
- package/src/Menu/index.tsx +35 -12
- package/src/Menu/options.ts +2 -20
- package/src/Menu/types.ts +28 -7
- package/src/NativeTinyui.ts +2 -0
- package/src/Popover/index.tsx +8 -3
- package/src/Popover/types.ts +2 -0
- package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/src/SFSymbol/index.tsx +89 -0
- package/src/SFSymbol/options.ts +116 -0
- package/src/SFSymbol/types.ts +158 -0
- package/src/Stepper/StepperNativeComponent.ts +29 -0
- package/src/Stepper/index.tsx +91 -0
- package/src/Stepper/types.ts +24 -0
- package/src/TipKit/Popover.tsx +87 -0
- package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/src/TipKit/index.ts +47 -0
- package/src/TipKit/types.ts +57 -0
- package/src/TipKit/validation.ts +39 -0
- package/src/index.tsx +31 -0
- package/src/utils/getNativeTinyui.ts +8 -1
- package/ios/Core/TinyuiHostingHelpers.swift +0 -74
- package/ios/Menu/TinyuiMenuViewSwift.swift +0 -326
- package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
package/README.md
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="./assets/banner.png" alt="TinyUI — Native iOS components for React Native" width="100%" />
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
New Architecture. The public API follows normal React composition patterns;
|
|
5
|
-
the native UI is implemented with SwiftUI behind Fabric components.
|
|
3
|
+
# react-native-tinyui
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
A dependency-free collection of native iOS components for React Native's New
|
|
6
|
+
Architecture. Built on Fabric, TinyUI offers familiar React composition while
|
|
7
|
+
staying close to native APIs. It uses UIKit by default and SwiftUI where needed.
|
|
8
|
+
|
|
9
|
+
## Components
|
|
10
|
+
|
|
11
|
+
| Component | What it does |
|
|
12
|
+
| ---------------------------------- | -------------------------------------------------------------- |
|
|
13
|
+
| [Menu](#menu) | Present native menus with actions, submenus, and sections |
|
|
14
|
+
| [Popover](#popover) | Show interactive React Native content in a native popover |
|
|
15
|
+
| [TipKit](#tipkit) | Present contextual tips using Apple's TipKit framework |
|
|
16
|
+
| [LiquidGlassText](#liquidglasstext) | Render text with native glass effects inside the glyphs |
|
|
17
|
+
| [Stepper](#stepper) | Adjust numeric values with the native iOS stepper |
|
|
18
|
+
| [ConcentricView](#concentricview) | Follow the containing view's corners with concentric rounding |
|
|
19
|
+
| [SFSymbol](#sfsymbol) | Display SF Symbols with native rendering and animation effects |
|
|
20
|
+
|
|
21
|
+
Screenshots below show the example app on an iPhone 17 Pro simulator running
|
|
22
|
+
iOS 26.5.
|
|
23
|
+
|
|
24
|
+
## Philosophy and comparison with `@expo/ui`
|
|
25
|
+
|
|
26
|
+
TinyUI prioritizes performance, stability, a small app footprint, and familiar
|
|
27
|
+
React composition. These priorities guide which components we add and how we
|
|
28
|
+
bridge them.
|
|
29
|
+
|
|
30
|
+
| Design choice | `react-native-tinyui` | `@expo/ui` |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Runtime dependencies | Zero additional runtime dependencies beyond `react` and `react-native`. No `expo`, Expo Modules, or `react-native-nitro-modules` required. | Uses Expo Modules; existing React Native apps must [install `expo`](https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/#installation). |
|
|
33
|
+
| Native implementation | Performance and stability first. Prefers direct UIKit integration through Fabric; uses SwiftUI when a component needs it. | Uses [SwiftUI on iOS](https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/), with its layout and hosting model. |
|
|
34
|
+
| Selective inclusion and app size | Component-specific JS imports avoid loading unrelated component modules. An explicit native component list excludes unselected UI implementations and their framework requirements from the pod. | Provides platform-specific JS entry points. Its [iOS podspec](https://github.com/expo/expo/blob/main/packages/expo-ui/ios/ExpoUI.podspec) includes native sources together, without a per-component selection list. JS imports alone do not select native sources. |
|
|
35
|
+
| Component scope | Welcomes both system UI and third-party UI, such as [GlassText](#liquidglasstext). Adding components to the catalog does not require apps to include their UI implementations when they remain unselected. | Centers on SwiftUI and Jetpack Compose. Supports [custom SwiftUI components and modifiers, including third-party SwiftUI libraries](https://docs.expo.dev/guides/expo-ui-swift-ui/extending/). |
|
|
36
|
+
| React API | Familiar props, children, and controlled/uncontrolled state, composed with ordinary React Native views. Names and behavior stay close to the underlying native APIs. | Exposes native toolkit concepts through React, including SwiftUI views, modifiers, and a [`Host` container](https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/#usage). |
|
|
37
|
+
| Supported targets and maintenance | iOS 17+ and the New Architecture only. Excluding older iOS versions and the legacy bridge reduces compatibility code and keeps maintenance focused. | Covers a broader platform scope: SwiftUI on iOS, Jetpack Compose on Android, and [universal components for iOS, Android, and web](https://docs.expo.dev/versions/latest/sdk/ui/universal/). Requirements depend on the Expo SDK and component. |
|
|
38
|
+
|
|
39
|
+
For the smallest builds, combine component-specific JS imports with
|
|
40
|
+
[`react-native-tinyui.components`](#optional-component-selection). All native
|
|
41
|
+
components are enabled by default, so configure that list explicitly. Shared
|
|
42
|
+
core code, Fabric Codegen, and lightweight registration stubs remain in the
|
|
43
|
+
native build; the size savings come from excluding unused UI implementations.
|
|
44
|
+
Final bundle size also depends on the app's bundler and linker settings.
|
|
13
45
|
|
|
14
46
|
## Requirements
|
|
15
47
|
|
|
@@ -45,6 +77,10 @@ Available components:
|
|
|
45
77
|
- `Menu`
|
|
46
78
|
- `Popover`
|
|
47
79
|
- `LiquidGlassText`
|
|
80
|
+
- `Stepper`
|
|
81
|
+
- `ConcentricView`
|
|
82
|
+
- `SFSymbol`
|
|
83
|
+
- `TipKit` (enables `TipKit.Popover`, `TipKit.configure` and `TipKit.invalidate`)
|
|
48
84
|
|
|
49
85
|
All components are enabled by default. Use an empty `components` array to
|
|
50
86
|
compile only the TurboModule core. Run `pod install` again whenever this list
|
|
@@ -57,6 +93,10 @@ Component-specific JavaScript entry points are also available:
|
|
|
57
93
|
import { Menu } from 'react-native-tinyui/menu';
|
|
58
94
|
import { Popover } from 'react-native-tinyui/popover';
|
|
59
95
|
import { LiquidGlassText } from 'react-native-tinyui/liquid-glass-text';
|
|
96
|
+
import { Stepper } from 'react-native-tinyui/stepper';
|
|
97
|
+
import { ConcentricView } from 'react-native-tinyui/concentric-view';
|
|
98
|
+
import { SFSymbol } from 'react-native-tinyui/sf-symbol';
|
|
99
|
+
import { TipKit } from 'react-native-tinyui/tip-kit';
|
|
60
100
|
```
|
|
61
101
|
|
|
62
102
|
The root `react-native-tinyui` imports remain supported for backwards
|
|
@@ -65,10 +105,11 @@ compatibility. JavaScript entry points provide cleaner dependency boundaries;
|
|
|
65
105
|
|
|
66
106
|
## Menu
|
|
67
107
|
|
|
108
|
+
<img src="docs/screenshots/menu.png" alt="Menu with native document actions expanded" width="320" />
|
|
109
|
+
|
|
68
110
|
`Menu` uses `children` as its trigger and receives its native menu entries
|
|
69
|
-
through the `options` prop.
|
|
70
|
-
|
|
71
|
-
menu.
|
|
111
|
+
through the `options` prop. A tap opens the native menu; `onActionPress`
|
|
112
|
+
receives every selected action's stable id and displayed title.
|
|
72
113
|
|
|
73
114
|
```tsx
|
|
74
115
|
import { Menu } from 'react-native-tinyui';
|
|
@@ -76,34 +117,40 @@ import { Menu } from 'react-native-tinyui';
|
|
|
76
117
|
<Menu
|
|
77
118
|
accessibilityLabel="Document actions"
|
|
78
119
|
title="Document actions"
|
|
79
|
-
|
|
120
|
+
onActionPress={({ nativeEvent }) => {
|
|
121
|
+
if (nativeEvent.id === 'rename') rename();
|
|
122
|
+
if (nativeEvent.id === 'toggle-pinned') togglePinned();
|
|
123
|
+
if (nativeEvent.id === 'copy-link') copyLink();
|
|
124
|
+
if (nativeEvent.id === 'invite-people') invitePeople();
|
|
125
|
+
if (nativeEvent.id === 'remove') remove();
|
|
126
|
+
}}
|
|
80
127
|
options={[
|
|
81
128
|
{
|
|
129
|
+
id: 'rename',
|
|
82
130
|
title: 'Rename',
|
|
83
131
|
systemImage: 'square.and.pencil',
|
|
84
|
-
onSelect: rename,
|
|
85
132
|
},
|
|
86
133
|
{
|
|
134
|
+
id: 'toggle-pinned',
|
|
87
135
|
title: 'Pinned',
|
|
88
136
|
state: 'on',
|
|
89
137
|
systemImage: 'pin',
|
|
90
|
-
onSelect: togglePinned,
|
|
91
138
|
},
|
|
92
139
|
{
|
|
93
140
|
type: 'submenu',
|
|
94
141
|
title: 'Share',
|
|
95
142
|
systemImage: 'square.and.arrow.up',
|
|
96
143
|
options: [
|
|
97
|
-
{
|
|
98
|
-
{
|
|
144
|
+
{ id: 'copy-link', title: 'Copy link' },
|
|
145
|
+
{ id: 'invite-people', title: 'Invite people' },
|
|
99
146
|
],
|
|
100
147
|
},
|
|
101
148
|
{ type: 'divider' },
|
|
102
149
|
{
|
|
150
|
+
id: 'remove',
|
|
103
151
|
title: 'Delete',
|
|
104
152
|
destructive: true,
|
|
105
153
|
systemImage: 'trash',
|
|
106
|
-
onSelect: remove,
|
|
107
154
|
},
|
|
108
155
|
]}
|
|
109
156
|
>
|
|
@@ -118,15 +165,46 @@ or `type: 'divider'` for structural entries; nested entries use their own
|
|
|
118
165
|
`options` array. SF Symbols are passed with `systemImage`; missing symbols
|
|
119
166
|
simply render no image.
|
|
120
167
|
|
|
168
|
+
To open the menu from another control, call `open()` on its ref:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { useRef } from 'react';
|
|
172
|
+
import { Button, Text } from 'react-native';
|
|
173
|
+
import { Menu, type MenuRef } from 'react-native-tinyui/menu';
|
|
174
|
+
|
|
175
|
+
function DocumentMenu() {
|
|
176
|
+
const menuRef = useRef<MenuRef>(null);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<Menu ref={menuRef} options={[{ id: 'rename', title: 'Rename' }]}>
|
|
181
|
+
<Text>Document actions</Text>
|
|
182
|
+
</Menu>
|
|
183
|
+
<Button title="Open menu" onPress={() => menuRef.current?.open()} />
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`open()` requires iOS 17.4 or newer and anchors the menu to its mounted trigger.
|
|
190
|
+
It does nothing on older iOS versions, while `disabled`, or when the trigger is
|
|
191
|
+
detached. Tapping the trigger still works on all supported iOS versions. The ref
|
|
192
|
+
also retains the outer View's methods, such as `measure` and `measureInWindow`.
|
|
193
|
+
|
|
121
194
|
Action options support `subtitle`, `state`, `destructive`, `disabled`, `hidden`,
|
|
122
|
-
and `keepOpen`.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
195
|
+
and `keepOpen`. Set explicit `id` values when handling `onActionPress`; actions
|
|
196
|
+
without one receive a generated menu-path id. Use `icon` to load an image from
|
|
197
|
+
the containing app's asset catalog; `iconColor` tints either an asset image or
|
|
198
|
+
an SF Symbol and accepts React Native `ColorValue`s, including dynamic and
|
|
199
|
+
semantic iOS colors. UIKit doesn't expose custom menu title colors, so the
|
|
200
|
+
deprecated `titleColor` field is retained only for source compatibility; use
|
|
201
|
+
`destructive` for system red styling. Submenus support the same label fields
|
|
202
|
+
plus `destructive`, `disabled`, `hidden`, and `displayInline`.
|
|
127
203
|
|
|
128
204
|
## Popover
|
|
129
205
|
|
|
206
|
+
<img src="docs/screenshots/popover.png" alt="Popover displaying resizable content above its trigger" width="320" />
|
|
207
|
+
|
|
130
208
|
`Popover` supports standard controlled and uncontrolled React state. Its content
|
|
131
209
|
remains a normal interactive React Native view tree hosted by a real
|
|
132
210
|
`UIPopoverPresentationController`.
|
|
@@ -138,10 +216,9 @@ import { Popover, PopoverClose } from 'react-native-tinyui';
|
|
|
138
216
|
defaultOpen={false}
|
|
139
217
|
attachmentAnchor="bottom"
|
|
140
218
|
arrowEdge="top"
|
|
141
|
-
contentSize={{ width: 320, height: 220 }}
|
|
142
219
|
onOpenChange={(open) => console.log({ open })}
|
|
143
220
|
content={({ close }) => (
|
|
144
|
-
<View style={
|
|
221
|
+
<View style={{ width: 280, padding: 20 }}>
|
|
145
222
|
<Text>Any React Native content can be rendered here.</Text>
|
|
146
223
|
<PopoverClose close={close} style={styles.doneButton}>
|
|
147
224
|
<Text>Done</Text>
|
|
@@ -161,11 +238,121 @@ the `content` prop, which can be a node or a render function receiving
|
|
|
161
238
|
as `Popover.Close`) renders a `Pressable` that calls `close` when pressed.
|
|
162
239
|
|
|
163
240
|
Use `open` with `onOpenChange` for a controlled popover, or `defaultOpen` for an
|
|
164
|
-
uncontrolled one.
|
|
165
|
-
|
|
241
|
+
uncontrolled one. The layout of `content` determines the popover's preferred
|
|
242
|
+
size: set `width` and optionally `height` on its root view, or let its children
|
|
243
|
+
determine the height. Size changes are applied while the popover is visible.
|
|
244
|
+
UIKit may limit the displayed size to the available space on screen.
|
|
245
|
+
|
|
246
|
+
Keep the content background transparent to show the native popover material:
|
|
247
|
+
Liquid Glass on iOS 26+ when built with Xcode 26+, and the system popover
|
|
248
|
+
appearance on earlier iOS versions. `style` on `Popover` styles the trigger's
|
|
249
|
+
outer container, not the presented content.
|
|
250
|
+
|
|
251
|
+
## TipKit
|
|
252
|
+
|
|
253
|
+
<img src="docs/screenshots/tip-kit.png" alt="TipKit popover with a save action anchored to the favorite button" width="320" />
|
|
254
|
+
|
|
255
|
+
`TipKit.Popover` anchors a native TipKit `TipUIPopoverViewController` to ordinary
|
|
256
|
+
React Native children on iOS 17+. The children retain their own touch handlers;
|
|
257
|
+
the tip appears automatically when both `enabled` and TipKit's eligibility allow
|
|
258
|
+
it. Its text, SF Symbol, action buttons, display history and invalidation use
|
|
259
|
+
Apple's TipKit framework. No additional package is required.
|
|
260
|
+
|
|
261
|
+
Configure TipKit once, **before mounting tips** (for example, during app startup):
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import { TipKit, SFSymbol } from 'react-native-tinyui';
|
|
265
|
+
import { Pressable } from 'react-native';
|
|
266
|
+
|
|
267
|
+
await TipKit.configure({ displayFrequency: 'daily' });
|
|
268
|
+
|
|
269
|
+
// Inside your screen:
|
|
270
|
+
<TipKit.Popover
|
|
271
|
+
tipId="favorite-feature-v1"
|
|
272
|
+
title="Keep your favorites close"
|
|
273
|
+
message="Save an item to find it quickly next time."
|
|
274
|
+
systemImage="star"
|
|
275
|
+
enabled={isScreenFocused && !hasFavorites}
|
|
276
|
+
maxDisplayCount={3}
|
|
277
|
+
actions={[{ id: 'learn-more', title: 'Learn more' }]}
|
|
278
|
+
onActionPress={({ id }) => {
|
|
279
|
+
if (id === 'learn-more') openHelp();
|
|
280
|
+
}}
|
|
281
|
+
onStatusChange={(status) => console.log(status)}
|
|
282
|
+
onVisibleChange={(visible) => console.log({ visible })}
|
|
283
|
+
onError={(error) => console.warn(error.message)}
|
|
284
|
+
>
|
|
285
|
+
<Pressable onPress={async () => {
|
|
286
|
+
await saveFavorite();
|
|
287
|
+
await TipKit.invalidate('favorite-feature-v1', 'actionPerformed');
|
|
288
|
+
}}>
|
|
289
|
+
<SFSymbol name="star" size={24} />
|
|
290
|
+
</Pressable>
|
|
291
|
+
</TipKit.Popover>;
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
| Prop | Description | Default |
|
|
295
|
+
| --- | --- | --- |
|
|
296
|
+
| `tipId` | Required stable identity for persistent history | — |
|
|
297
|
+
| `title` | Required nonempty title, already localized by the app | — |
|
|
298
|
+
| `message` | Supporting plain text | — |
|
|
299
|
+
| `systemImage` | SF Symbol name | — |
|
|
300
|
+
| `actions` | Buttons with unique nonempty `{ id, title }` values | `[]` |
|
|
301
|
+
| `enabled` | Allows presentation; does not override TipKit eligibility | `true` |
|
|
302
|
+
| `maxDisplayCount` | Positive integer; automatically invalidates after this many displays | Unlimited |
|
|
303
|
+
| `ignoresDisplayFrequency` | Exempts this tip from the app-wide display interval | `false` |
|
|
304
|
+
| `arrowEdge` | Preferred bubble edge: `top`, `bottom`, `leading`, `trailing`, `auto` | `auto` |
|
|
305
|
+
| `onStatusChange` | `{ status: 'pending' \| 'available' }` or `{ status: 'invalidated', reason }` | — |
|
|
306
|
+
| `onVisibleChange` | Whether the native bubble is actually visible | — |
|
|
307
|
+
| `onActionPress` | Receives the selected `{ id, title }`; does not automatically invalidate | — |
|
|
308
|
+
| `onError` | Receives `{ code, message }`; otherwise a warning is logged | — |
|
|
309
|
+
|
|
310
|
+
The component accepts standard `ViewProps` and a native view ref. `style` lays
|
|
311
|
+
out the anchor, not the bubble. UIKit chooses the bubble's size and may adapt
|
|
312
|
+
the arrow to available space; leading/trailing follow layout direction. Bubble
|
|
313
|
+
content is native text/images/actions; `children` supplies only the anchor.
|
|
314
|
+
Content edits apply to the next presentation; the currently visible bubble
|
|
315
|
+
retains the content and action labels it was presented with.
|
|
316
|
+
|
|
317
|
+
### Configuration and lifetime
|
|
318
|
+
|
|
319
|
+
`TipKit.configure({ displayFrequency })` returns a promise. Supported frequencies
|
|
320
|
+
are `immediate`, `hourly`, `daily` (default), `weekly` and `monthly`. This is an
|
|
321
|
+
app-wide TipKit setting. Repeating the same configuration is safe, including
|
|
322
|
+
after Fast Refresh; changing it later rejects with `E_TIPS_ALREADY_CONFIGURED`.
|
|
323
|
+
Coordinate this startup configuration with any other native TipKit integration.
|
|
324
|
+
Initialization errors reject the promise; an enabled view mounted too early
|
|
325
|
+
reports `E_TIPS_NOT_CONFIGURED` and can recover after configuration succeeds.
|
|
326
|
+
|
|
327
|
+
`TipKit.invalidate(tipId, reason = 'actionPerformed')` also returns a promise and
|
|
328
|
+
requires configuration first. It persistently invalidates the ID even when no
|
|
329
|
+
view for that tip is mounted. `reason` may be `actionPerformed` or `tipClosed`.
|
|
330
|
+
System invalidations can additionally report `displayCountExceeded`,
|
|
331
|
+
`displayDurationExceeded`, or `unknown` through `onStatusChange`.
|
|
332
|
+
|
|
333
|
+
Keep IDs stable across renders, app launches and translations. All instances
|
|
334
|
+
with the same ID share their TipKit history. Keep `maxDisplayCount` and
|
|
335
|
+
`ignoresDisplayFrequency` consistent for each ID; conflicting values report
|
|
336
|
+
`E_CONFLICTING_TIP_OPTIONS`. Use a deliberate new versioned ID for a new feature
|
|
337
|
+
tip. Hiding a tip, unmounting it or reconfiguring TinyUI never resets history.
|
|
338
|
+
|
|
339
|
+
Use `enabled={isScreenFocused && businessCondition}` with your navigation
|
|
340
|
+
library. TinyUI also waits for a visible anchor and a free presenter, and
|
|
341
|
+
dismisses on detachment or when the anchor leaves the visible area. It presents
|
|
342
|
+
at most one TinyUI tip at a time and waits while another modal occupies the
|
|
343
|
+
presenter. An outside dismissal suppresses immediate reopening for that mount;
|
|
344
|
+
toggle `enabled` off and on to allow another attempt, subject to TipKit's state.
|
|
345
|
+
The system close button can permanently invalidate a tip.
|
|
346
|
+
|
|
347
|
+
This first version exposes popover tips and JS eligibility conditions. Inline
|
|
348
|
+
`TipUIView`, native rule/event builders, TipGroup and datastore reset APIs are
|
|
349
|
+
not exposed. The example's **New demo tip** button deliberately creates another
|
|
350
|
+
ID for trying the behavior again without clearing the app's TipKit datastore.
|
|
166
351
|
|
|
167
352
|
## LiquidGlassText
|
|
168
353
|
|
|
354
|
+
<img src="docs/screenshots/liquid-glass-text.png" alt="LiquidGlassText rendering English, Chinese, and numbers over a photo" width="320" />
|
|
355
|
+
|
|
169
356
|
`LiquidGlassText` renders native glass inside the glyph outlines, based on
|
|
170
357
|
[GlassText](https://github.com/ailtonvivaz/GlassText). It exposes the effect and
|
|
171
358
|
typography through React props.
|
|
@@ -230,6 +417,259 @@ their translated string directly.
|
|
|
230
417
|
The adapted Core Text outline implementation retains the upstream MIT notice in
|
|
231
418
|
[`ios/LiquidGlassText/GlassText-LICENSE`](ios/LiquidGlassText/GlassText-LICENSE).
|
|
232
419
|
|
|
420
|
+
## Stepper
|
|
421
|
+
|
|
422
|
+
<img src="docs/screenshots/stepper.png" alt="Native Stepper with a controlled value and configurable range" width="320" />
|
|
423
|
+
|
|
424
|
+
`Stepper` bridges UIKit's
|
|
425
|
+
[`UIStepper`](https://developer.apple.com/documentation/uikit/uistepper) through
|
|
426
|
+
Fabric. It supports fractional steps, press-and-hold repeat, and wrapping at
|
|
427
|
+
the bounds on iOS 17+.
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
import { useState } from 'react';
|
|
431
|
+
import { Stepper } from 'react-native-tinyui';
|
|
432
|
+
|
|
433
|
+
function Quantity() {
|
|
434
|
+
const [quantity, setQuantity] = useState(1);
|
|
435
|
+
return (
|
|
436
|
+
<Stepper
|
|
437
|
+
accessibilityLabel="Quantity"
|
|
438
|
+
value={quantity}
|
|
439
|
+
minimumValue={1}
|
|
440
|
+
maximumValue={10}
|
|
441
|
+
onValueChange={setQuantity}
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Uncontrolled: UIKit owns the value after initialization.
|
|
447
|
+
<Stepper defaultValue={2.5} stepValue={0.5} maximumValue={5} wraps />;
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
| Prop | Description | Default |
|
|
451
|
+
| --- | --- | --- |
|
|
452
|
+
| `value` | Controlled numeric value | — |
|
|
453
|
+
| `defaultValue` | Initial value when uncontrolled; later changes are ignored | `0` |
|
|
454
|
+
| `minimumValue` | Lower bound | `0` |
|
|
455
|
+
| `maximumValue` | Upper bound, at least `minimumValue` | `100` |
|
|
456
|
+
| `stepValue` | Positive increment/decrement amount | `1` |
|
|
457
|
+
| `isContinuous` | Report changes during interaction; otherwise report on release | `true` |
|
|
458
|
+
| `autorepeat` | Repeatedly step while holding a button | `true` |
|
|
459
|
+
| `wraps` | Continue from the opposite bound when stepping past an end | `false` |
|
|
460
|
+
| `disabled` | Prevent user interaction | `false` |
|
|
461
|
+
| `onValueChange` | Callback receiving the new number after user interaction | — |
|
|
462
|
+
|
|
463
|
+
When `value` is supplied, update it in `onValueChange` to accept changes. Keeping
|
|
464
|
+
the same `value` restores the native control to that value. Omit `value` to let
|
|
465
|
+
UIKit manage the state. Programmatic value or range changes do not call
|
|
466
|
+
`onValueChange`.
|
|
467
|
+
|
|
468
|
+
All numeric props must be finite, `stepValue` must be positive, and
|
|
469
|
+
`maximumValue` must be at least `minimumValue`; invalid inputs throw a descriptive
|
|
470
|
+
error. Values are clamped to the current range. Equal bounds produce a fixed
|
|
471
|
+
value. Changing an uncontrolled range also clamps its current native value.
|
|
472
|
+
|
|
473
|
+
The component accepts standard `ViewProps` (except `children`) and a native view
|
|
474
|
+
ref. Native accessibility behavior is preserved; use `accessibilityLabel` to
|
|
475
|
+
name the value being adjusted. Fabric measures the native control's intrinsic
|
|
476
|
+
size. A larger `style` frame centers the native control without stretching
|
|
477
|
+
its buttons. Put padding on a surrounding `View`. Like UIKit's control, the
|
|
478
|
+
stepper shows the minus and plus buttons; render a separate `Text` for the value.
|
|
479
|
+
|
|
480
|
+
## ConcentricView
|
|
481
|
+
|
|
482
|
+
<img src="docs/screenshots/concentric-view.png" alt="Nested ConcentricView containers following the bottom screen corners" width="320" />
|
|
483
|
+
|
|
484
|
+
`ConcentricView` is a Fabric container that lets UIKit resolve its corner radii
|
|
485
|
+
relative to its containing view. On iOS 26+, it applies
|
|
486
|
+
[`UICornerRadius.containerConcentricRadius`](https://developer.apple.com/documentation/uikit/uicornerradius-c.class/containerconcentricradius)
|
|
487
|
+
through `UIView.cornerConfiguration`. UIKit handles geometry and layout changes.
|
|
488
|
+
Building this component requires **Xcode 26+**; on iOS 17–25 it uses
|
|
489
|
+
`minimumRadius` as a fixed corner radius.
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
import { Text } from 'react-native';
|
|
493
|
+
import { ConcentricView } from 'react-native-tinyui';
|
|
494
|
+
|
|
495
|
+
<ConcentricView
|
|
496
|
+
minimumRadius={32}
|
|
497
|
+
style={{ padding: 12, backgroundColor: '#DCEBFF' }}
|
|
498
|
+
>
|
|
499
|
+
<ConcentricView
|
|
500
|
+
minimumRadius={12}
|
|
501
|
+
style={{ padding: 20, backgroundColor: '#3875D5' }}
|
|
502
|
+
>
|
|
503
|
+
<Text>UIKit resolves each corner.</Text>
|
|
504
|
+
</ConcentricView>
|
|
505
|
+
</ConcentricView>;
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`minimumRadius` defaults to `0` and must be finite and nonnegative. It only
|
|
509
|
+
controls the fixed fallback radius before iOS 26; iOS 26+ resolves corners
|
|
510
|
+
entirely through UIKit, without a minimum radius. The component accepts
|
|
511
|
+
`children`, standard `ViewProps`, and a native view ref. It clips content
|
|
512
|
+
by default; use `style={{ overflow: 'visible' }}` to allow overflow. Avoid
|
|
513
|
+
`style.borderRadius` and individual corner radii on this component.
|
|
514
|
+
React Native's custom border, outline, and shadow drawing does not calculate
|
|
515
|
+
concentric radii; keep those decorations on a surrounding view when needed.
|
|
516
|
+
|
|
517
|
+
## SFSymbol
|
|
518
|
+
|
|
519
|
+
<img src="docs/screenshots/sf-symbol.png" alt="SFSymbol rendering a bell badge with native symbol colors" width="320" />
|
|
520
|
+
|
|
521
|
+
`SFSymbol` uses **UIKit `UIImageView` + Apple's Symbols framework**, exposed
|
|
522
|
+
through Fabric. It does not embed a SwiftUI hosting view. Xcode 26+ is required
|
|
523
|
+
to build; the deployment target remains iOS 17. Yoga measures the configured
|
|
524
|
+
symbol's natural size, so `style.width` and `style.height` are optional.
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { useState } from 'react';
|
|
528
|
+
import { Button, PlatformColor, View } from 'react-native';
|
|
529
|
+
import { SFSymbol } from 'react-native-tinyui';
|
|
530
|
+
|
|
531
|
+
function Favorite() {
|
|
532
|
+
const [selected, setSelected] = useState(false);
|
|
533
|
+
const [trigger, setTrigger] = useState(0);
|
|
534
|
+
return (
|
|
535
|
+
<View>
|
|
536
|
+
<SFSymbol
|
|
537
|
+
name={selected ? 'heart.fill' : 'heart'}
|
|
538
|
+
size={32}
|
|
539
|
+
weight="semibold"
|
|
540
|
+
renderingMode="hierarchical"
|
|
541
|
+
color={PlatformColor('systemPinkColor')}
|
|
542
|
+
effect={{ type: 'bounce', trigger, options: { repeat: false } }}
|
|
543
|
+
contentTransition={{ type: 'magicReplace', direction: 'downUp' }}
|
|
544
|
+
/>
|
|
545
|
+
<Button title="Favorite" onPress={() => {
|
|
546
|
+
setSelected(!selected);
|
|
547
|
+
setTrigger(trigger + 1);
|
|
548
|
+
}} />
|
|
549
|
+
</View>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Image configuration
|
|
555
|
+
|
|
556
|
+
| Prop | Values / behavior |
|
|
557
|
+
| --- | --- |
|
|
558
|
+
| `name` | Required complete symbol name, including variants such as `square.fill`. Missing symbols render empty. |
|
|
559
|
+
| `source` | `system` (default), or `asset` for a custom SF Symbol in the app's asset catalog. Ordinary bitmap assets are not accepted. |
|
|
560
|
+
| `size` | Positive point size; default `17`. |
|
|
561
|
+
| `weight` | `unspecified` (default), `ultraLight`, `thin`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, `black`. |
|
|
562
|
+
| `scale` | `default`, `unspecified`, `small`, `medium`, `large`. |
|
|
563
|
+
| `textStyle` | `extraLargeTitle`, `extraLargeTitle2`, `largeTitle`, `title1`–`title3`, `headline`, `subheadline`, `body`, `callout`, `footnote`, `caption1`, `caption2`. Overrides `size` and supplies the style's weight unless explicitly set. |
|
|
564
|
+
| `fontFamily` | Font/PostScript name used to derive symbol metrics; unknown fonts use system metrics. |
|
|
565
|
+
| `allowFontScaling` | Default `true`. Scales the base point size by React Native's `fontScale`, including when using `textStyle`. |
|
|
566
|
+
| `maxFontSizeMultiplier` | Cap on that scale, >= 1. Omitted or `0` means unlimited. |
|
|
567
|
+
| `renderingMode` | `automatic` (default), `monochrome`, `hierarchical`, `palette`, `multicolor`. |
|
|
568
|
+
| `color` | React Native `ColorValue`, including `PlatformColor` and `DynamicColorIOS`. Defaults to semantic label color. |
|
|
569
|
+
| `paletteColors` | One to three `ColorValue`s in primary, secondary, tertiary order, used in palette mode. If omitted, the palette uses `color`. |
|
|
570
|
+
| `variableValue` | Progress from `0` to `1`; omitted uses the symbol default. Requires a symbol with variable annotations. |
|
|
571
|
+
| `variableValueMode` | `automatic` (default), `color`, `draw` (iOS 26+). |
|
|
572
|
+
| `colorRenderingMode` | `automatic` (default), `flat`, `gradient` (iOS 26+). |
|
|
573
|
+
| `resizeMode` | `center` (default, natural point size), `contain`, `cover`, `stretch`, for explicitly sized frames. Use `style.overflow: 'hidden'` to clip. |
|
|
574
|
+
|
|
575
|
+
Standard `ViewProps`, styles and refs are supported. Symbols are decorative
|
|
576
|
+
(`accessible={false}`) by default; supply `accessible` and a meaningful
|
|
577
|
+
`accessibilityLabel` for standalone images. Use a `Pressable` for interactive icons.
|
|
578
|
+
|
|
579
|
+
### Preset effects
|
|
580
|
+
|
|
581
|
+
Pass one object to `effect`, or an array of **distinct** effect types to combine
|
|
582
|
+
compatible effects. UIKit determines how combined effects interact.
|
|
583
|
+
|
|
584
|
+
| `type` | Configuration | Minimum iOS |
|
|
585
|
+
| --- | --- | --- |
|
|
586
|
+
| `bounce`, `scale`, `appear`, `disappear` | `direction: 'up' \| 'down'`, `scope` | 17 |
|
|
587
|
+
| `pulse` | `scope` | 17 |
|
|
588
|
+
| `variableColor` | `iteration: 'iterative' \| 'cumulative'`, `reversing: boolean`, `inactiveLayers: 'hide' \| 'dim'` | 17 |
|
|
589
|
+
| `wiggle` | `direction: 'up' \| 'down' \| 'left' \| 'right' \| 'forward' \| 'backward' \| 'clockwise' \| 'counterClockwise'`, or `angle` in degrees clockwise from +x; `scope` | 18 |
|
|
590
|
+
| `rotate` | `direction: 'clockwise' \| 'counterClockwise'`, `scope` | 18 |
|
|
591
|
+
| `breathe` | `style: 'plain' \| 'pulse'`, `scope` | 18 |
|
|
592
|
+
| `drawOn` | `scope`, also accepting `individually` | 26 |
|
|
593
|
+
| `drawOff` | `scope`, also accepting `individually`; `reversed: boolean` | 26 |
|
|
594
|
+
|
|
595
|
+
`scope` is `byLayer` or `wholeSymbol`; omit any modifier to keep Apple's default.
|
|
596
|
+
Each preset also accepts:
|
|
597
|
+
|
|
598
|
+
- `active` (default `true`): apply/remove the effect. Scale and visibility
|
|
599
|
+
effects hold their state until removed. Disappear/Draw Off can hide the symbol.
|
|
600
|
+
- `trigger`: a number or string. Change it to replay an effect. Mounting or
|
|
601
|
+
changing effect configuration also applies it; unrelated rerenders do not.
|
|
602
|
+
- `animated` (default `true`): animate application/removal of the effect.
|
|
603
|
+
- `options.speed`: positive speed multiplier, default system speed.
|
|
604
|
+
- `options.repeat`: a positive integer play count, `'forever'`, or `false` for
|
|
605
|
+
one play. Omit to use the preset's native repetition behavior.
|
|
606
|
+
- `options.repeatBehavior`: `periodic` or `continuous` (iOS 18+).
|
|
607
|
+
Continuous repeats forever and cannot be combined with a count or delay.
|
|
608
|
+
- `options.repeatDelay`: nonnegative delay in seconds between periodic plays
|
|
609
|
+
(iOS 18+). Without a count, periodic/delayed repetition continues indefinitely.
|
|
610
|
+
|
|
611
|
+
Repetition options only affect presets that support repeating. For example,
|
|
612
|
+
`scale` is a held state, not a looping animation. Removing `effect` clears all
|
|
613
|
+
effects. Detached/recycled views stop animations; reattaching applies them again.
|
|
614
|
+
|
|
615
|
+
### Replacement transitions and compatibility
|
|
616
|
+
|
|
617
|
+
`contentTransition` animates changes to the name or image configuration. It
|
|
618
|
+
accepts `type: 'automatic' | 'replace' | 'magicReplace'`, optional
|
|
619
|
+
`direction: 'downUp' | 'upUp' | 'offUp'`, `scope: 'byLayer' | 'wholeSymbol'`, and
|
|
620
|
+
a positive `speed`. Direction and scope configure Replace and the Magic Replace
|
|
621
|
+
fallback; Automatic uses the system transition. The initial image is not transitioned.
|
|
622
|
+
|
|
623
|
+
Unsupported presets are ignored on earlier iOS versions. Magic Replace falls
|
|
624
|
+
back to the configured Replace before iOS 18. Continuous repetition falls back
|
|
625
|
+
to periodic repetition on iOS 17; custom repeat delay is ignored. Before iOS 26,
|
|
626
|
+
variable mode and gradient configuration are ignored. Actual animation, palette,
|
|
627
|
+
multicolor and variable/draw support depend on each symbol's native annotations.
|
|
628
|
+
|
|
629
|
+
`respectReduceMotion` defaults to `true`: repeating/discrete effects and content
|
|
630
|
+
transitions are suppressed while Reduce Motion is enabled, while scale and
|
|
631
|
+
visibility state changes apply instantly. Set it to `false` to opt out.
|
|
632
|
+
|
|
633
|
+
## Example App
|
|
634
|
+
|
|
635
|
+
The [example app](example/src/App.tsx) demonstrates Menu, Popover, TipKit,
|
|
636
|
+
LiquidGlassText, Stepper, ConcentricView, and SFSymbol. It includes all native
|
|
637
|
+
components, so building it requires Xcode 26+. Use iOS 26+ to try the glass effects
|
|
638
|
+
and the latest SF Symbol features.
|
|
639
|
+
|
|
640
|
+
From the repository root, install the dependencies:
|
|
641
|
+
|
|
642
|
+
```sh
|
|
643
|
+
yarn
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Install the example's Pods:
|
|
647
|
+
|
|
648
|
+
```sh
|
|
649
|
+
cd example/ios && pod install
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Then run Metro and the iOS app from the repository root in separate terminals:
|
|
653
|
+
|
|
654
|
+
```sh
|
|
655
|
+
yarn example start
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
```sh
|
|
659
|
+
yarn example ios
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The example uses Metro port `8082`, which is already configured in these scripts.
|
|
663
|
+
See the [development workflow](CONTRIBUTING.md#development-workflow) for more
|
|
664
|
+
details.
|
|
665
|
+
|
|
666
|
+
## Apps Using This Library
|
|
667
|
+
|
|
668
|
+
- [Night Vision - LiDAR Camera](https://apps.apple.com/app/id1668629667)
|
|
669
|
+
- [Laser Measure - LiDAR Powered](https://apps.apple.com/app/id6466744678)
|
|
670
|
+
- [PhoneAway - Digital Detox](https://apps.apple.com/app/id6744548607)
|
|
671
|
+
- [Fatigue Alert - Stay Awake](https://apps.apple.com/app/id6479893638)
|
|
672
|
+
|
|
233
673
|
## Contributing
|
|
234
674
|
|
|
235
675
|
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_CONCENTRIC_VIEW
|
|
2
|
+
|
|
3
|
+
#import "TinyuiConcentricView.h"
|
|
4
|
+
|
|
5
|
+
#import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
|
|
6
|
+
#import <react/renderer/components/TinyuiSpec/Props.h>
|
|
7
|
+
#import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
|
|
8
|
+
|
|
9
|
+
using namespace facebook::react;
|
|
10
|
+
|
|
11
|
+
@interface TinyuiConcentricView () <RCTTinyuiConcentricViewViewProtocol>
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@implementation TinyuiConcentricView
|
|
15
|
+
|
|
16
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
17
|
+
{
|
|
18
|
+
return concreteComponentDescriptorProvider<TinyuiConcentricViewComponentDescriptor>();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
22
|
+
{
|
|
23
|
+
if (self = [super initWithFrame:frame]) {
|
|
24
|
+
static const auto defaultProps = std::make_shared<const TinyuiConcentricViewProps>();
|
|
25
|
+
_props = defaultProps;
|
|
26
|
+
}
|
|
27
|
+
return self;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
|
|
31
|
+
{
|
|
32
|
+
[super finalizeUpdates:updateMask];
|
|
33
|
+
[self applyCornerConfiguration];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
|
|
37
|
+
{
|
|
38
|
+
[super traitCollectionDidChange:previousTraitCollection];
|
|
39
|
+
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
|
|
40
|
+
[self applyCornerConfiguration];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)applyCornerConfiguration
|
|
45
|
+
{
|
|
46
|
+
// Fabric updates layer.cornerRadius while applying view styles. Configure
|
|
47
|
+
// UIKit afterwards so it owns the resolved radii, including on layout changes.
|
|
48
|
+
if (@available(iOS 26.0, *)) {
|
|
49
|
+
self.cornerConfiguration = [UICornerConfiguration configurationWithRadius:
|
|
50
|
+
[UICornerRadius containerConcentricRadius]];
|
|
51
|
+
} else {
|
|
52
|
+
const auto &props = *std::static_pointer_cast<const TinyuiConcentricViewProps>(_props);
|
|
53
|
+
self.layer.cornerRadius = props.minimumRadius;
|
|
54
|
+
self.layer.cornerCurve = kCACornerCurveContinuous;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@end
|
|
59
|
+
|
|
60
|
+
#endif
|