react-native-tinyui 1.1.1 → 3.0.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 +140 -32
- package/ios/Core/Tinyui.mm +18 -0
- package/ios/Core/TinyuiComponentClasses.mm +7 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +0 -1
- package/ios/SFSymbol/TinyuiSFSymbolEffects.swift +184 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.swift +103 -0
- package/ios/SFSymbol/TinyuiSFSymbolProvider.swift +135 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +4 -2
- package/ios/SFSymbol/TinyuiSFSymbolView.mm +14 -122
- package/ios/TipKit/TinyuiTip.swift +16 -0
- package/ios/TipKit/TinyuiTipPopoverProvider.swift +66 -135
- package/ios/TipKit/TinyuiTipPopoverView.mm +0 -18
- package/ios/TipKit/TinyuiTipView.h +8 -0
- package/ios/TipKit/TinyuiTipView.mm +93 -0
- package/ios/TipKit/TinyuiTipViewProvider.swift +140 -0
- package/ios/TipKit/TinyuiTipViewShadowNode.h +48 -0
- package/ios/TipKit/TinyuiTipsRuntime.swift +16 -74
- package/lib/module/NativeTinyui.js.map +1 -1
- package/lib/module/SFSymbol/index.js +0 -4
- package/lib/module/SFSymbol/index.js.map +1 -1
- package/lib/module/SFSymbol/options.js +0 -2
- package/lib/module/SFSymbol/options.js.map +1 -1
- package/lib/module/TipKit/Popover.js +0 -22
- package/lib/module/TipKit/Popover.js.map +1 -1
- package/lib/module/TipKit/TipPopoverNativeComponent.ts +0 -6
- package/lib/module/TipKit/TipViewNativeComponent.ts +28 -0
- package/lib/module/TipKit/View.js +62 -0
- package/lib/module/TipKit/View.js.map +1 -0
- package/lib/module/TipKit/index.js +11 -0
- package/lib/module/TipKit/index.js.map +1 -1
- package/lib/module/TipKit/validation.js +4 -4
- package/lib/module/TipKit/validation.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/LiquidGlassText/types.d.ts +1 -1
- package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
- package/lib/typescript/src/NativeTinyui.d.ts +1 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/index.d.ts +1 -1
- package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/options.d.ts +0 -2
- package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/types.d.ts +1 -5
- package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -1
- package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -1
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +0 -9
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/TipKit/TipViewNativeComponent.d.ts +29 -0
- package/lib/typescript/src/TipKit/TipViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/View.d.ts +3 -0
- package/lib/typescript/src/TipKit/View.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/index.d.ts +8 -1
- package/lib/typescript/src/TipKit/index.d.ts.map +1 -1
- package/lib/typescript/src/TipKit/types.d.ts +17 -15
- package/lib/typescript/src/TipKit/types.d.ts.map +1 -1
- package/lib/typescript/src/TipKit/validation.d.ts +2 -2
- package/lib/typescript/src/TipKit/validation.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/LiquidGlassText/types.ts +1 -1
- package/src/NativeTinyui.ts +1 -0
- package/src/SFSymbol/index.tsx +0 -5
- package/src/SFSymbol/options.ts +0 -2
- package/src/SFSymbol/types.ts +1 -18
- package/src/TipKit/Popover.tsx +1 -21
- package/src/TipKit/TipPopoverNativeComponent.ts +0 -6
- package/src/TipKit/TipViewNativeComponent.ts +28 -0
- package/src/TipKit/View.tsx +64 -0
- package/src/TipKit/index.ts +13 -1
- package/src/TipKit/types.ts +19 -20
- package/src/TipKit/validation.ts +8 -5
- package/src/index.tsx +1 -2
- package/ios/SFSymbol/TinyuiSFSymbolEffects.h +0 -9
- package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +0 -180
- package/ios/SFSymbol/TinyuiSFSymbolImage.h +0 -8
- package/ios/SFSymbol/TinyuiSFSymbolImage.mm +0 -141
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import Symbols
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@objc(TinyuiSFSymbolProvider)
|
|
6
|
+
public final class TinyuiSFSymbolProvider: UIImageView {
|
|
7
|
+
private var configuration: NSDictionary?
|
|
8
|
+
private var effects: [[String: Any]] = []
|
|
9
|
+
private var appliedEffects: [String: [String: Any]] = [:]
|
|
10
|
+
private var respectReduceMotion = true
|
|
11
|
+
|
|
12
|
+
public override init(frame: CGRect) {
|
|
13
|
+
super.init(frame: frame)
|
|
14
|
+
contentMode = .center
|
|
15
|
+
isAccessibilityElement = false
|
|
16
|
+
NotificationCenter.default.addObserver(
|
|
17
|
+
self,
|
|
18
|
+
selector: #selector(reduceMotionChanged),
|
|
19
|
+
name: UIAccessibility.reduceMotionStatusDidChangeNotification,
|
|
20
|
+
object: nil
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@available(*, unavailable)
|
|
25
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
26
|
+
|
|
27
|
+
private var reduceMotion: Bool {
|
|
28
|
+
respectReduceMotion && UIAccessibility.isReduceMotionEnabled
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@objc(configure:effects:contentTransition:respectReduceMotion:resizeMode:)
|
|
32
|
+
public func configure(
|
|
33
|
+
_ value: Any?,
|
|
34
|
+
effects: Any?,
|
|
35
|
+
contentTransition: Any?,
|
|
36
|
+
respectReduceMotion: Bool,
|
|
37
|
+
resizeMode: String
|
|
38
|
+
) {
|
|
39
|
+
let changedMotion = self.respectReduceMotion != respectReduceMotion
|
|
40
|
+
self.respectReduceMotion = respectReduceMotion
|
|
41
|
+
if changedMotion { clearEffects() }
|
|
42
|
+
|
|
43
|
+
let next = value as? [String: Any] ?? [:]
|
|
44
|
+
if configuration?.isEqual(to: next) != true {
|
|
45
|
+
let nextImage = TinyuiSFSymbolImage.image(configuration: next, includeColors: true)
|
|
46
|
+
tintColor = next["color"].flatMap(RCTConvert.uiColor) ?? .label
|
|
47
|
+
let transitionConfiguration = contentTransition as? [String: Any] ?? [:]
|
|
48
|
+
if let nextImage, image != nil, window != nil, !reduceMotion,
|
|
49
|
+
let transition = tinyuiSFSymbolTransition(transitionConfiguration)
|
|
50
|
+
{
|
|
51
|
+
setSymbolImage(
|
|
52
|
+
nextImage,
|
|
53
|
+
contentTransition: transition,
|
|
54
|
+
options: tinyuiSFSymbolOptions(transitionConfiguration)
|
|
55
|
+
)
|
|
56
|
+
} else {
|
|
57
|
+
image = nextImage
|
|
58
|
+
}
|
|
59
|
+
configuration = next as NSDictionary
|
|
60
|
+
if nextImage == nil { clearEffects() }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch resizeMode {
|
|
64
|
+
case "contain": contentMode = .scaleAspectFit
|
|
65
|
+
case "cover": contentMode = .scaleAspectFill
|
|
66
|
+
case "stretch": contentMode = .scaleToFill
|
|
67
|
+
default: contentMode = .center
|
|
68
|
+
}
|
|
69
|
+
self.effects = effects as? [[String: Any]] ?? []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@objc public func clearEffects() {
|
|
73
|
+
removeAllSymbolEffects(options: .default, animated: false)
|
|
74
|
+
appliedEffects.removeAll()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc public func applyEffects() {
|
|
78
|
+
guard window != nil, image != nil else { return }
|
|
79
|
+
var requested: [String: [String: Any]] = [:]
|
|
80
|
+
for configuration in effects {
|
|
81
|
+
guard let type = configuration["type"] as? String else { continue }
|
|
82
|
+
requested[type] = configuration
|
|
83
|
+
}
|
|
84
|
+
for (type, old) in appliedEffects {
|
|
85
|
+
let next = requested[type]
|
|
86
|
+
if next.map({ NSDictionary(dictionary: old).isEqual(to: $0) }) != true
|
|
87
|
+
|| (next?["active"] as? NSNumber)?.boolValue == false
|
|
88
|
+
{
|
|
89
|
+
// Reset immediately when replaying/reconfiguring so removal cannot race
|
|
90
|
+
// the replacement effect. Only animate an actual deactivation.
|
|
91
|
+
let reapplying = next != nil && (next?["active"] as? NSNumber)?.boolValue != false
|
|
92
|
+
let animated = !reapplying && !reduceMotion
|
|
93
|
+
&& ((next ?? old)["animated"] as? NSNumber)?.boolValue != false
|
|
94
|
+
TinyuiSFSymbolEffect.make(old)?.remove(
|
|
95
|
+
from: self,
|
|
96
|
+
options: tinyuiSFSymbolOptions((next ?? old)["options"] as? [String: Any]),
|
|
97
|
+
animated: animated
|
|
98
|
+
)
|
|
99
|
+
appliedEffects.removeValue(forKey: type)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (type, configuration) in requested {
|
|
103
|
+
if (configuration["active"] as? NSNumber)?.boolValue == false
|
|
104
|
+
|| appliedEffects[type].map({ NSDictionary(dictionary: $0).isEqual(to: configuration) }) == true
|
|
105
|
+
{
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
let stateful = ["scale", "appear", "disappear", "drawOn", "drawOff"].contains(type)
|
|
109
|
+
// Keep visibility/scale state under Reduce Motion without animating it.
|
|
110
|
+
if reduceMotion && !stateful { continue }
|
|
111
|
+
guard let effect = TinyuiSFSymbolEffect.make(configuration) else { continue }
|
|
112
|
+
effect.add(
|
|
113
|
+
to: self,
|
|
114
|
+
options: tinyuiSFSymbolOptions(configuration["options"] as? [String: Any]),
|
|
115
|
+
animated: !reduceMotion && (configuration["animated"] as? NSNumber)?.boolValue != false
|
|
116
|
+
)
|
|
117
|
+
appliedEffects[type] = configuration
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public override func didMoveToWindow() {
|
|
122
|
+
super.didMoveToWindow()
|
|
123
|
+
if window == nil { clearEffects() }
|
|
124
|
+
else { applyEffects() }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@objc private func reduceMotionChanged() {
|
|
128
|
+
clearEffects()
|
|
129
|
+
applyEffects()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
deinit {
|
|
133
|
+
NotificationCenter.default.removeObserver(self)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#if TINYUI_FEATURE_SF_SYMBOL
|
|
2
2
|
|
|
3
3
|
#import "TinyuiSFSymbolShadowNode.h"
|
|
4
|
-
#import "
|
|
4
|
+
#import "Tinyui-Swift.h"
|
|
5
5
|
#import <react/utils/FollyConvert.h>
|
|
6
6
|
#include <react/renderer/core/LayoutConstraints.h>
|
|
7
7
|
#include <react/renderer/core/LayoutContext.h>
|
|
@@ -16,7 +16,9 @@ Size TinyuiSFSymbolShadowNode::measureContent(
|
|
|
16
16
|
@autoreleasepool {
|
|
17
17
|
// UIImage and UIFont creation are safe off the main thread. No UIView or
|
|
18
18
|
// main-queue sync is needed during concurrent Fabric layout.
|
|
19
|
-
|
|
19
|
+
id configuration = convertFollyDynamicToId(getConcreteProps().configuration);
|
|
20
|
+
if (![configuration isKindOfClass:NSDictionary.class]) configuration = @{};
|
|
21
|
+
UIImage *image = [TinyuiSFSymbolImage imageWithConfiguration:configuration includeColors:NO];
|
|
20
22
|
CGSize size = image == nil ? CGSizeZero : image.size;
|
|
21
23
|
auto scale = layoutContext.pointScaleFactor > 0 ? layoutContext.pointScaleFactor : 1;
|
|
22
24
|
return layoutConstraints.clamp({
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#if TINYUI_FEATURE_SF_SYMBOL
|
|
2
2
|
|
|
3
3
|
#import "TinyuiSFSymbolView.h"
|
|
4
|
-
#import "TinyuiSFSymbolEffects.h"
|
|
5
|
-
#import "TinyuiSFSymbolImage.h"
|
|
6
4
|
#import "TinyuiSFSymbolShadowNode.h"
|
|
7
|
-
#import
|
|
5
|
+
#import "Tinyui-Swift.h"
|
|
8
6
|
#import <react/utils/FollyConvert.h>
|
|
9
7
|
#import <react/renderer/components/TinyuiSpec/Props.h>
|
|
10
8
|
#import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
|
|
@@ -15,11 +13,7 @@ using namespace facebook::react;
|
|
|
15
13
|
@end
|
|
16
14
|
|
|
17
15
|
@implementation TinyuiSFSymbolView {
|
|
18
|
-
|
|
19
|
-
NSDictionary *_configuration;
|
|
20
|
-
NSArray<NSDictionary *> *_effects;
|
|
21
|
-
NSMutableDictionary<NSString *, NSDictionary *> *_appliedEffects;
|
|
22
|
-
BOOL _respectReduceMotion;
|
|
16
|
+
TinyuiSFSymbolProvider *_provider;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -31,141 +25,39 @@ using namespace facebook::react;
|
|
|
31
25
|
{
|
|
32
26
|
if (self = [super initWithFrame:frame]) {
|
|
33
27
|
_props = std::make_shared<const TinyuiSFSymbolViewProps>();
|
|
34
|
-
[self
|
|
35
|
-
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(reduceMotionChanged:)
|
|
36
|
-
name:UIAccessibilityReduceMotionStatusDidChangeNotification object:nil];
|
|
28
|
+
[self createProvider];
|
|
37
29
|
}
|
|
38
30
|
return self;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
- (void)
|
|
33
|
+
- (void)createProvider
|
|
42
34
|
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
_imageView.isAccessibilityElement = NO;
|
|
46
|
-
self.contentView = _imageView;
|
|
47
|
-
_configuration = nil;
|
|
48
|
-
_effects = @[];
|
|
49
|
-
_appliedEffects = [NSMutableDictionary dictionary];
|
|
50
|
-
_respectReduceMotion = YES;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
- (BOOL)reduceMotion
|
|
54
|
-
{
|
|
55
|
-
return _respectReduceMotion && UIAccessibilityIsReduceMotionEnabled();
|
|
35
|
+
_provider = [[TinyuiSFSymbolProvider alloc] initWithFrame:CGRectZero];
|
|
36
|
+
self.contentView = _provider;
|
|
56
37
|
}
|
|
57
38
|
|
|
58
39
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
59
40
|
{
|
|
60
41
|
const auto &next = *std::static_pointer_cast<const TinyuiSFSymbolViewProps>(props);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (![configuration isKindOfClass:NSDictionary.class]) configuration = @{};
|
|
67
|
-
if (![_configuration isEqual:configuration]) {
|
|
68
|
-
UIImage *image = TinyuiSFSymbolImage(configuration, YES);
|
|
69
|
-
_imageView.tintColor = [RCTConvert UIColor:configuration[@"color"]] ?: UIColor.labelColor;
|
|
70
|
-
NSDictionary *transitionConfig = convertFollyDynamicToId(next.contentTransition);
|
|
71
|
-
if (![transitionConfig isKindOfClass:NSDictionary.class]) transitionConfig = @{};
|
|
72
|
-
NSSymbolContentTransition *transition = TinyuiSFSymbolTransition(transitionConfig);
|
|
73
|
-
if (image != nil && _imageView.image != nil && self.window != nil && transition != nil && !self.reduceMotion) {
|
|
74
|
-
[_imageView setSymbolImage:image withContentTransition:transition
|
|
75
|
-
options:TinyuiSFSymbolOptions(transitionConfig)];
|
|
76
|
-
} else {
|
|
77
|
-
_imageView.image = image;
|
|
78
|
-
}
|
|
79
|
-
_configuration = configuration;
|
|
80
|
-
if (image == nil) [self clearEffects];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (next.resizeMode == "contain") _imageView.contentMode = UIViewContentModeScaleAspectFit;
|
|
84
|
-
else if (next.resizeMode == "cover") _imageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
85
|
-
else if (next.resizeMode == "stretch") _imageView.contentMode = UIViewContentModeScaleToFill;
|
|
86
|
-
else _imageView.contentMode = UIViewContentModeCenter;
|
|
87
|
-
|
|
88
|
-
id effects = convertFollyDynamicToId(next.effects);
|
|
89
|
-
_effects = [effects isKindOfClass:NSArray.class] ? effects : @[];
|
|
42
|
+
[_provider configure:convertFollyDynamicToId(next.configuration)
|
|
43
|
+
effects:convertFollyDynamicToId(next.effects)
|
|
44
|
+
contentTransition:convertFollyDynamicToId(next.contentTransition)
|
|
45
|
+
respectReduceMotion:next.respectReduceMotion
|
|
46
|
+
resizeMode:[NSString stringWithUTF8String:next.resizeMode.c_str()]];
|
|
90
47
|
[super updateProps:props oldProps:oldProps];
|
|
91
48
|
}
|
|
92
49
|
|
|
93
50
|
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
|
|
94
51
|
{
|
|
95
52
|
[super finalizeUpdates:updateMask];
|
|
96
|
-
[
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
- (void)clearEffects
|
|
100
|
-
{
|
|
101
|
-
[_imageView removeAllSymbolEffectsWithOptions:NSSymbolEffectOptions.options animated:NO];
|
|
102
|
-
[_appliedEffects removeAllObjects];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
- (void)applyEffects
|
|
106
|
-
{
|
|
107
|
-
if (self.window == nil || _imageView.image == nil) return;
|
|
108
|
-
NSMutableDictionary<NSString *, NSDictionary *> *requested = [NSMutableDictionary dictionary];
|
|
109
|
-
for (NSDictionary *configuration in _effects) {
|
|
110
|
-
if (![configuration isKindOfClass:NSDictionary.class]) continue;
|
|
111
|
-
NSString *type = configuration[@"type"];
|
|
112
|
-
if (![type isKindOfClass:NSString.class]) continue;
|
|
113
|
-
requested[type] = configuration;
|
|
114
|
-
}
|
|
115
|
-
for (NSString *type in _appliedEffects.allKeys) {
|
|
116
|
-
NSDictionary *old = _appliedEffects[type];
|
|
117
|
-
NSDictionary *next = requested[type];
|
|
118
|
-
if (![old isEqual:next] || [next[@"active"] isEqual:@NO]) {
|
|
119
|
-
NSSymbolEffect *effect = TinyuiSFSymbolEffect(old);
|
|
120
|
-
// Reset immediately when replaying/reconfiguring, so an animated removal
|
|
121
|
-
// cannot race the replacement effect. Animate only actual deactivation.
|
|
122
|
-
BOOL reapplying = next != nil && ![next[@"active"] isEqual:@NO];
|
|
123
|
-
BOOL animated = !reapplying && !self.reduceMotion && ![(next ?: old)[@"animated"] isEqual:@NO];
|
|
124
|
-
if (effect != nil) {
|
|
125
|
-
[_imageView removeSymbolEffectOfType:effect
|
|
126
|
-
options:TinyuiSFSymbolOptions((next ?: old)[@"options"]) animated:animated];
|
|
127
|
-
}
|
|
128
|
-
[_appliedEffects removeObjectForKey:type];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (NSString *type in requested) {
|
|
132
|
-
NSDictionary *configuration = requested[type];
|
|
133
|
-
if ([configuration[@"active"] isEqual:@NO] || [_appliedEffects[type] isEqual:configuration]) continue;
|
|
134
|
-
BOOL stateful = [@[@"scale", @"appear", @"disappear", @"drawOn", @"drawOff"] containsObject:type];
|
|
135
|
-
// Keep visibility/scale state under Reduce Motion, without continuous or
|
|
136
|
-
// discrete animation. Clearing those effects must still restore the state.
|
|
137
|
-
if (self.reduceMotion && !stateful) continue;
|
|
138
|
-
NSSymbolEffect *effect = TinyuiSFSymbolEffect(configuration);
|
|
139
|
-
if (effect == nil) continue;
|
|
140
|
-
[_imageView addSymbolEffect:effect options:TinyuiSFSymbolOptions(configuration[@"options"])
|
|
141
|
-
animated:!self.reduceMotion && ![configuration[@"animated"] isEqual:@NO]];
|
|
142
|
-
_appliedEffects[type] = configuration;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
- (void)didMoveToWindow
|
|
147
|
-
{
|
|
148
|
-
[super didMoveToWindow];
|
|
149
|
-
if (self.window == nil) [self clearEffects];
|
|
150
|
-
else [self applyEffects];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
- (void)reduceMotionChanged:(NSNotification *)notification
|
|
154
|
-
{
|
|
155
|
-
[self clearEffects];
|
|
156
|
-
[self applyEffects];
|
|
53
|
+
[_provider applyEffects];
|
|
157
54
|
}
|
|
158
55
|
|
|
159
56
|
- (void)prepareForRecycle
|
|
160
57
|
{
|
|
161
|
-
[
|
|
58
|
+
[_provider clearEffects];
|
|
162
59
|
[super prepareForRecycle];
|
|
163
|
-
[self
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
- (void)dealloc
|
|
167
|
-
{
|
|
168
|
-
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
60
|
+
[self createProvider];
|
|
169
61
|
}
|
|
170
62
|
|
|
171
63
|
@end
|
|
@@ -29,3 +29,19 @@ struct TinyuiTip: Tip, Sendable {
|
|
|
29
29
|
return result
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
extension TinyuiTip {
|
|
34
|
+
init(configuration: [String: Any]) {
|
|
35
|
+
self.init(
|
|
36
|
+
id: configuration["tipId"] as? String ?? "",
|
|
37
|
+
titleString: configuration["title"] as? String ?? "",
|
|
38
|
+
messageString: configuration["message"] as? String ?? "",
|
|
39
|
+
symbolName: configuration["systemImage"] as? String ?? "",
|
|
40
|
+
actionContents: (configuration["actions"] as? [[String: String]] ?? []).map {
|
|
41
|
+
ActionContent(id: $0["id"] ?? "", title: $0["title"] ?? "")
|
|
42
|
+
},
|
|
43
|
+
maximumDisplayCount: configuration["maxDisplayCount"] as? Int ?? 0,
|
|
44
|
+
ignoresFrequency: configuration["ignoresDisplayFrequency"] as? Bool ?? false
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -2,8 +2,6 @@ import TipKit
|
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
4
|
@objc public protocol TinyuiTipPopoverDelegate: AnyObject {
|
|
5
|
-
func onTipStatus(tipId: String, status: String, reason: String)
|
|
6
|
-
func onTipVisible(tipId: String, visible: Bool)
|
|
7
5
|
func onTipAction(tipId: String, id: String, title: String)
|
|
8
6
|
func onTipError(tipId: String, code: String, message: String)
|
|
9
7
|
}
|
|
@@ -12,21 +10,22 @@ import UIKit
|
|
|
12
10
|
@objc public final class TinyuiTipPopoverProvider: UIView, UIPopoverPresentationControllerDelegate {
|
|
13
11
|
private weak var delegate: TinyuiTipPopoverDelegate?
|
|
14
12
|
@objc public weak var parentViewController: UIViewController? {
|
|
15
|
-
didSet {
|
|
13
|
+
didSet {
|
|
14
|
+
guard oldValue !== parentViewController else { return }
|
|
15
|
+
stop()
|
|
16
|
+
start()
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
private var tip: TinyuiTip?
|
|
18
20
|
private var enabled = false
|
|
19
21
|
private var arrowEdge = "auto"
|
|
20
22
|
private var observation: Task<Void, Never>?
|
|
21
|
-
private var
|
|
22
|
-
private var status: Tips.Status?
|
|
23
|
+
private var shouldDisplay = false
|
|
23
24
|
private var lastError: String?
|
|
24
|
-
private var suppressed = false
|
|
25
25
|
private var controller: TipUIPopoverViewController?
|
|
26
26
|
private var presentedTipId: String?
|
|
27
27
|
private var isPresenting = false
|
|
28
28
|
private var isDismissing = false
|
|
29
|
-
private var reportedVisible = false
|
|
30
29
|
|
|
31
30
|
@objc public convenience init(delegate: TinyuiTipPopoverDelegate) {
|
|
32
31
|
self.init(frame: .zero)
|
|
@@ -34,60 +33,48 @@ import UIKit
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
@objc public func configure(_ configuration: [String: Any], enabled: Bool, arrowEdge: String) {
|
|
37
|
-
let nextTip = TinyuiTip(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
messageString: configuration["message"] as? String ?? "",
|
|
41
|
-
symbolName: configuration["systemImage"] as? String ?? "",
|
|
42
|
-
actionContents: (configuration["actions"] as? [[String: String]] ?? []).map {
|
|
43
|
-
TinyuiTip.ActionContent(id: $0["id"] ?? "", title: $0["title"] ?? "")
|
|
44
|
-
},
|
|
45
|
-
maximumDisplayCount: configuration["maxDisplayCount"] as? Int ?? 0,
|
|
46
|
-
ignoresFrequency: configuration["ignoresDisplayFrequency"] as? Bool ?? false
|
|
47
|
-
)
|
|
48
|
-
if tip?.id != nextTip.id {
|
|
49
|
-
stopObservation()
|
|
50
|
-
status = nil
|
|
36
|
+
let nextTip = TinyuiTip(configuration: configuration)
|
|
37
|
+
if tip?.id != nextTip.id || self.enabled != enabled {
|
|
38
|
+
stop()
|
|
51
39
|
lastError = nil
|
|
52
|
-
suppressed = false
|
|
53
40
|
}
|
|
54
|
-
if !self.enabled && enabled { suppressed = false }
|
|
55
41
|
tip = nextTip
|
|
56
42
|
self.enabled = enabled
|
|
57
43
|
self.arrowEdge = arrowEdge
|
|
58
|
-
|
|
44
|
+
// Validate changed options even when an observation is already running.
|
|
45
|
+
do {
|
|
46
|
+
try TinyuiTipsRuntime.shared.remember(nextTip)
|
|
47
|
+
} catch {
|
|
48
|
+
stop()
|
|
49
|
+
let error = error as NSError
|
|
50
|
+
reportError(error.userInfo["code"] as? String ?? "E_TIP", error.localizedDescription)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
lastError = nil
|
|
54
|
+
configureArrow()
|
|
55
|
+
start()
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
override public func didMoveToWindow() {
|
|
62
59
|
super.didMoveToWindow()
|
|
63
|
-
if window == nil {
|
|
64
|
-
stopObservation()
|
|
65
|
-
dismissTip()
|
|
66
|
-
TinyuiTipsRuntime.shared.unregister(self)
|
|
67
|
-
} else {
|
|
68
|
-
TinyuiTipsRuntime.shared.register(self)
|
|
69
|
-
}
|
|
60
|
+
if window == nil { stop() } else { start() }
|
|
70
61
|
}
|
|
71
62
|
|
|
72
63
|
override public func layoutSubviews() {
|
|
73
64
|
super.layoutSubviews()
|
|
74
65
|
configureArrow()
|
|
75
|
-
|
|
66
|
+
// Start once Fabric has given the anchor a size. Layout changes alone do
|
|
67
|
+
// not request another presentation after a tip has been dismissed.
|
|
68
|
+
start()
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
func updatePresentation() {
|
|
71
|
+
private func start() {
|
|
72
|
+
guard observation == nil, enabled, let tip, let window,
|
|
73
|
+
!bounds.isEmpty, let parent = parentViewController,
|
|
74
|
+
parent.viewIfLoaded?.window === window else { return }
|
|
84
75
|
let runtime = TinyuiTipsRuntime.shared
|
|
85
|
-
guard
|
|
86
|
-
|
|
87
|
-
if window != nil && enabled && !runtime.isConfigured {
|
|
88
|
-
reportError("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before displaying tips.")
|
|
89
|
-
}
|
|
90
|
-
dismissTip()
|
|
76
|
+
guard runtime.isConfigured else {
|
|
77
|
+
reportError("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before displaying tips.")
|
|
91
78
|
return
|
|
92
79
|
}
|
|
93
80
|
do {
|
|
@@ -96,25 +83,38 @@ import UIKit
|
|
|
96
83
|
} catch {
|
|
97
84
|
let error = error as NSError
|
|
98
85
|
reportError(error.userInfo["code"] as? String ?? "E_TIP", error.localizedDescription)
|
|
99
|
-
dismissTip()
|
|
100
86
|
return
|
|
101
87
|
}
|
|
102
|
-
startObservation(tip)
|
|
103
88
|
|
|
89
|
+
observation = Task { @MainActor [weak self] in
|
|
90
|
+
for await shouldDisplay in tip.shouldDisplayUpdates {
|
|
91
|
+
guard !Task.isCancelled, let self else { break }
|
|
92
|
+
self.shouldDisplay = shouldDisplay
|
|
93
|
+
self.updatePresentation()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func stop() {
|
|
99
|
+
observation?.cancel()
|
|
100
|
+
observation = nil
|
|
101
|
+
shouldDisplay = false
|
|
102
|
+
dismissTip()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func updatePresentation() {
|
|
104
106
|
if let controller, !isPresenting, !isDismissing, controller.presentingViewController == nil {
|
|
105
107
|
// Dismissal may originate inside TipKit rather than through the delegate.
|
|
106
|
-
suppressed = true
|
|
107
108
|
finishDismissal(controller)
|
|
108
109
|
return
|
|
109
110
|
}
|
|
110
|
-
let
|
|
111
|
-
guard enabled, status == .available, !suppressed, presenter != nil,
|
|
111
|
+
guard enabled, window != nil, let tip, shouldDisplay,
|
|
112
112
|
presentedTipId == nil || presentedTipId == tip.id else {
|
|
113
113
|
dismissTip()
|
|
114
114
|
return
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// Like Tipster's TipPresenter, only present when the host is free.
|
|
117
|
+
guard controller == nil, let presenter = availablePresenter() else { return }
|
|
118
118
|
let tipController = TipUIPopoverViewController(tip, sourceItem: self) { [weak self] action in
|
|
119
119
|
guard let self, self.tip?.id == tip.id,
|
|
120
120
|
let content = tip.actionContents.first(where: { $0.id == action.id }) else { return }
|
|
@@ -125,84 +125,22 @@ import UIKit
|
|
|
125
125
|
presentedTipId = tip.id
|
|
126
126
|
configureArrow()
|
|
127
127
|
isPresenting = true
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Retain the provider through the animation so a Fabric unmount can still
|
|
129
|
+
// dismiss this presentation in the completion, without a global owner.
|
|
130
|
+
presenter.present(tipController, animated: true) { [self] in
|
|
131
|
+
guard self.controller === tipController else { return }
|
|
130
132
|
self.isPresenting = false
|
|
131
|
-
|
|
132
|
-
self.reportedVisible = true
|
|
133
|
-
self.delegate?.onTipVisible(tipId: tip.id, visible: true)
|
|
134
|
-
}
|
|
135
|
-
TinyuiTipsRuntime.shared.requestUpdate()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private func startObservation(_ tip: TinyuiTip) {
|
|
140
|
-
guard observation == nil else { return }
|
|
141
|
-
observationGeneration += 1
|
|
142
|
-
let generation = observationGeneration
|
|
143
|
-
observation = Task { @MainActor [weak self] in
|
|
144
|
-
for await status in tip.statusUpdates {
|
|
145
|
-
guard !Task.isCancelled, let self, self.observationGeneration == generation else { break }
|
|
146
|
-
self.status = status
|
|
147
|
-
let state: String
|
|
148
|
-
let reason: String
|
|
149
|
-
switch status {
|
|
150
|
-
case .pending: state = "pending"; reason = ""
|
|
151
|
-
case .available: state = "available"; reason = ""
|
|
152
|
-
case .invalidated(let invalidation):
|
|
153
|
-
state = "invalidated"
|
|
154
|
-
switch invalidation {
|
|
155
|
-
case .actionPerformed: reason = "actionPerformed"
|
|
156
|
-
case .tipClosed: reason = "tipClosed"
|
|
157
|
-
case .displayCountExceeded: reason = "displayCountExceeded"
|
|
158
|
-
case .displayDurationExceeded: reason = "displayDurationExceeded"
|
|
159
|
-
@unknown default: reason = "unknown"
|
|
160
|
-
}
|
|
161
|
-
@unknown default: state = "pending"; reason = ""
|
|
162
|
-
}
|
|
163
|
-
self.delegate?.onTipStatus(tipId: tip.id, status: state, reason: reason)
|
|
164
|
-
TinyuiTipsRuntime.shared.requestUpdate()
|
|
165
|
-
}
|
|
133
|
+
self.updatePresentation()
|
|
166
134
|
}
|
|
167
135
|
}
|
|
168
136
|
|
|
169
|
-
private func
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
observation = nil
|
|
173
|
-
status = nil
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/// Reject obscured/detached anchors and non-current navigation/tab screens.
|
|
177
|
-
private func eligiblePresenter() -> UIViewController? {
|
|
178
|
-
guard let window, window.windowScene?.activationState == .foregroundActive,
|
|
179
|
-
!window.isHidden, let parent = parentViewController,
|
|
180
|
-
parent.viewIfLoaded?.window === window, !bounds.isEmpty else { return nil }
|
|
181
|
-
|
|
182
|
-
var visibleRect = convert(bounds, to: window)
|
|
183
|
-
var ancestor: UIView? = self
|
|
184
|
-
while let view = ancestor {
|
|
185
|
-
guard !view.isHidden, view.alpha > 0.01 else { return nil }
|
|
186
|
-
if view.clipsToBounds {
|
|
187
|
-
visibleRect = visibleRect.intersection(view.convert(view.bounds, to: window))
|
|
188
|
-
}
|
|
189
|
-
ancestor = view.superview
|
|
190
|
-
}
|
|
191
|
-
guard !visibleRect.intersection(window.bounds).isEmpty else { return nil }
|
|
192
|
-
|
|
193
|
-
var child = parent
|
|
137
|
+
private func availablePresenter() -> UIViewController? {
|
|
138
|
+
guard let window, let parent = parentViewController,
|
|
139
|
+
parent.viewIfLoaded?.window === window else { return nil }
|
|
194
140
|
var ancestorController: UIViewController? = parent
|
|
195
141
|
while let current = ancestorController {
|
|
196
|
-
guard !current.isBeingDismissed, !current.isBeingPresented
|
|
197
|
-
|
|
198
|
-
navigation.topViewController !== child { return nil }
|
|
199
|
-
if let tabs = current as? UITabBarController,
|
|
200
|
-
tabs.selectedViewController !== child { return nil }
|
|
201
|
-
if let presented = current.presentedViewController, presented !== controller { return nil }
|
|
202
|
-
// Wait for navigation animations before presenting. Do not treat our own
|
|
203
|
-
// popover's presentation transition as the anchor leaving the screen.
|
|
204
|
-
if controller == nil && current.transitionCoordinator != nil { return nil }
|
|
205
|
-
child = current
|
|
142
|
+
guard !current.isBeingDismissed, !current.isBeingPresented,
|
|
143
|
+
current.presentedViewController == nil else { return nil }
|
|
206
144
|
ancestorController = current.parent
|
|
207
145
|
}
|
|
208
146
|
return parent
|
|
@@ -226,28 +164,24 @@ import UIKit
|
|
|
226
164
|
guard let controller, !isPresenting, !isDismissing else { return }
|
|
227
165
|
guard controller.presentingViewController != nil else { finishDismissal(controller); return }
|
|
228
166
|
isDismissing = true
|
|
229
|
-
controller.dismiss(animated: window != nil) { [
|
|
230
|
-
guard
|
|
167
|
+
controller.dismiss(animated: window != nil) { [self] in
|
|
168
|
+
guard self.controller === controller else { return }
|
|
231
169
|
self.finishDismissal(controller)
|
|
170
|
+
// Props may have changed while the dismissal animation was running.
|
|
171
|
+
self.updatePresentation()
|
|
232
172
|
}
|
|
233
173
|
}
|
|
234
174
|
|
|
235
175
|
private func finishDismissal(_ dismissed: TipUIPopoverViewController) {
|
|
236
176
|
guard controller === dismissed else { return }
|
|
237
|
-
if reportedVisible, let presentedTipId {
|
|
238
|
-
delegate?.onTipVisible(tipId: presentedTipId, visible: false)
|
|
239
|
-
}
|
|
240
177
|
controller = nil
|
|
241
178
|
presentedTipId = nil
|
|
242
179
|
isPresenting = false
|
|
243
180
|
isDismissing = false
|
|
244
|
-
reportedVisible = false
|
|
245
|
-
TinyuiTipsRuntime.shared.release(self)
|
|
246
181
|
}
|
|
247
182
|
|
|
248
183
|
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
249
184
|
guard let controller, presentationController.presentedViewController === controller else { return }
|
|
250
|
-
suppressed = true
|
|
251
185
|
finishDismissal(controller)
|
|
252
186
|
}
|
|
253
187
|
|
|
@@ -264,12 +198,9 @@ import UIKit
|
|
|
264
198
|
@objc public func reset() {
|
|
265
199
|
enabled = false
|
|
266
200
|
tip = nil
|
|
267
|
-
suppressed = false
|
|
268
201
|
lastError = nil
|
|
269
|
-
|
|
270
|
-
dismissTip()
|
|
202
|
+
stop()
|
|
271
203
|
parentViewController = nil
|
|
272
|
-
TinyuiTipsRuntime.shared.unregister(self)
|
|
273
204
|
}
|
|
274
205
|
|
|
275
206
|
deinit { observation?.cancel() }
|