react-native-platform-components 0.0.2
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/LICENSE +20 -0
- package/PlatformComponents.podspec +20 -0
- package/README.md +233 -0
- package/android/build.gradle +78 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/platformcomponents/DateConstraints.kt +83 -0
- package/android/src/main/java/com/platformcomponents/Helper.kt +27 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +684 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +149 -0
- package/android/src/main/java/com/platformcomponents/PCMaterialMode.kt +16 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +410 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +114 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +22 -0
- package/ios/PCDatePicker.h +11 -0
- package/ios/PCDatePicker.mm +248 -0
- package/ios/PCDatePickerView.swift +405 -0
- package/ios/PCSelectionMenu.h +10 -0
- package/ios/PCSelectionMenu.mm +182 -0
- package/ios/PCSelectionMenu.swift +434 -0
- package/lib/module/DatePicker.js +74 -0
- package/lib/module/DatePicker.js.map +1 -0
- package/lib/module/DatePickerNativeComponent.ts +68 -0
- package/lib/module/SelectionMenu.js +79 -0
- package/lib/module/SelectionMenu.js.map +1 -0
- package/lib/module/SelectionMenu.web.js +57 -0
- package/lib/module/SelectionMenu.web.js.map +1 -0
- package/lib/module/SelectionMenuNativeComponent.ts +106 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/sharedTypes.js +4 -0
- package/lib/module/sharedTypes.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DatePicker.d.ts +38 -0
- package/lib/typescript/src/DatePicker.d.ts.map +1 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts +53 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.d.ts +50 -0
- package/lib/typescript/src/SelectionMenu.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts +19 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +85 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/sharedTypes.d.ts +10 -0
- package/lib/typescript/src/sharedTypes.d.ts.map +1 -0
- package/package.json +178 -0
- package/shared/PCDatePickerComponentDescriptors-custom.h +52 -0
- package/shared/PCDatePickerShadowNode-custom.cpp +1 -0
- package/shared/PCDatePickerShadowNode-custom.h +27 -0
- package/shared/PCDatePickerState-custom.h +13 -0
- package/shared/PCSelectionMenuComponentDescriptors-custom.h +25 -0
- package/shared/PCSelectionMenuShadowNode-custom.cpp +36 -0
- package/shared/PCSelectionMenuShadowNode-custom.h +46 -0
- package/src/DatePicker.tsx +146 -0
- package/src/DatePickerNativeComponent.ts +68 -0
- package/src/SelectionMenu.tsx +170 -0
- package/src/SelectionMenu.web.tsx +93 -0
- package/src/SelectionMenuNativeComponent.ts +106 -0
- package/src/index.tsx +3 -0
- package/src/sharedTypes.ts +14 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import java.util.ArrayList
|
|
8
|
+
|
|
9
|
+
class PlatformComponentsViewPackage : ReactPackage {
|
|
10
|
+
override fun createViewManagers(
|
|
11
|
+
reactContext: ReactApplicationContext
|
|
12
|
+
): List<ViewManager<*, *>> {
|
|
13
|
+
return listOf(
|
|
14
|
+
PCSelectionMenuViewManager(),
|
|
15
|
+
PCDatePickerViewManager(),
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
20
|
+
return emptyList()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// ios/PCDatePicker.mm
|
|
2
|
+
|
|
3
|
+
#import "PCDatePicker.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTConversions.h>
|
|
6
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
|
|
7
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
|
|
8
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
|
|
9
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/RCTComponentViewHelpers.h>
|
|
10
|
+
#import <react/renderer/core/LayoutPrimitives.h>
|
|
11
|
+
|
|
12
|
+
#if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
|
|
13
|
+
#import <PlatformComponents/PlatformComponents-Swift.h>
|
|
14
|
+
#else
|
|
15
|
+
#import "PlatformComponents-Swift.h"
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
#import "PCDatePickerComponentDescriptors-custom.h"
|
|
19
|
+
#import "PCDatePickerShadowNode-custom.h"
|
|
20
|
+
#import "PCDatePickerState-custom.h"
|
|
21
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
22
|
+
|
|
23
|
+
using namespace facebook::react;
|
|
24
|
+
|
|
25
|
+
@interface PCDatePicker () <RCTPCDatePickerViewProtocol>
|
|
26
|
+
|
|
27
|
+
- (void)updateMeasurements;
|
|
28
|
+
- (const PCDatePickerEventEmitter &)eventEmitterTyped;
|
|
29
|
+
|
|
30
|
+
@end
|
|
31
|
+
|
|
32
|
+
@implementation PCDatePicker {
|
|
33
|
+
PCDatePickerView *_datePickerView;
|
|
34
|
+
MeasuringPCDatePickerShadowNode::ConcreteState::Shared _state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
38
|
+
return concreteComponentDescriptorProvider<
|
|
39
|
+
MeasuringPCDatePickerComponentDescriptor>();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
43
|
+
if (self = [super initWithFrame:frame]) {
|
|
44
|
+
_datePickerView = [PCDatePickerView new];
|
|
45
|
+
_datePickerView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
46
|
+
|
|
47
|
+
__weak __typeof(self) weakSelf = self;
|
|
48
|
+
|
|
49
|
+
_datePickerView.onChangeHandler = ^(NSNumber *ms) {
|
|
50
|
+
__typeof(self) strongSelf = weakSelf;
|
|
51
|
+
if (!strongSelf)
|
|
52
|
+
return;
|
|
53
|
+
|
|
54
|
+
PCDatePickerEventEmitter::OnConfirm event{};
|
|
55
|
+
event.timestampMs = ms.doubleValue;
|
|
56
|
+
|
|
57
|
+
strongSelf.eventEmitterTyped.onConfirm(event);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
_datePickerView.onCancelHandler = ^{
|
|
61
|
+
__typeof(self) strongSelf = weakSelf;
|
|
62
|
+
if (!strongSelf)
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
PCDatePickerEventEmitter::OnClosed event{};
|
|
66
|
+
strongSelf.eventEmitterTyped.onClosed(event);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
self.contentView = _datePickerView;
|
|
70
|
+
|
|
71
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
72
|
+
[_datePickerView.topAnchor
|
|
73
|
+
constraintEqualToAnchor:self.contentView.topAnchor],
|
|
74
|
+
[_datePickerView.bottomAnchor
|
|
75
|
+
constraintEqualToAnchor:self.contentView.bottomAnchor],
|
|
76
|
+
[_datePickerView.leadingAnchor
|
|
77
|
+
constraintEqualToAnchor:self.contentView.leadingAnchor],
|
|
78
|
+
[_datePickerView.trailingAnchor
|
|
79
|
+
constraintEqualToAnchor:self.contentView.trailingAnchor],
|
|
80
|
+
]];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return self;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- (void)layoutSubviews {
|
|
87
|
+
[super layoutSubviews];
|
|
88
|
+
_datePickerView.frame = self.bounds;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#pragma mark - Props
|
|
92
|
+
|
|
93
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
94
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
95
|
+
const auto &oldViewProps =
|
|
96
|
+
*std::static_pointer_cast<const PCDatePickerProps>(_props);
|
|
97
|
+
const auto &newViewProps =
|
|
98
|
+
*std::static_pointer_cast<const PCDatePickerProps>(props);
|
|
99
|
+
|
|
100
|
+
BOOL needsToUpdateMeasurements = NO;
|
|
101
|
+
|
|
102
|
+
// presentation (default "modal")
|
|
103
|
+
NSString *newPresentation =
|
|
104
|
+
newViewProps.presentation.empty()
|
|
105
|
+
? @"modal"
|
|
106
|
+
: [NSString stringWithUTF8String:newViewProps.presentation.c_str()];
|
|
107
|
+
|
|
108
|
+
if (![_datePickerView.presentation isEqualToString:newPresentation]) {
|
|
109
|
+
_datePickerView.presentation = newPresentation;
|
|
110
|
+
needsToUpdateMeasurements = YES;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// visible: treat only "open" as open; everything else as closed
|
|
114
|
+
BOOL shouldOpen = (newViewProps.visible == "open");
|
|
115
|
+
NSNumber *newOpen = @(shouldOpen);
|
|
116
|
+
if (![_datePickerView.open isEqual:newOpen]) {
|
|
117
|
+
_datePickerView.open = newOpen;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// dateMs (sentinel -1)
|
|
121
|
+
if (oldViewProps.dateMs != newViewProps.dateMs) {
|
|
122
|
+
_datePickerView.dateMs =
|
|
123
|
+
(newViewProps.dateMs >= 0) ? @(newViewProps.dateMs) : nil;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// min/max (sentinel -1)
|
|
127
|
+
if (oldViewProps.minDateMs != newViewProps.minDateMs) {
|
|
128
|
+
_datePickerView.minDateMs =
|
|
129
|
+
(newViewProps.minDateMs >= 0) ? @(newViewProps.minDateMs) : nil;
|
|
130
|
+
}
|
|
131
|
+
if (oldViewProps.maxDateMs != newViewProps.maxDateMs) {
|
|
132
|
+
_datePickerView.maxDateMs =
|
|
133
|
+
(newViewProps.maxDateMs >= 0) ? @(newViewProps.maxDateMs) : nil;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// locale
|
|
137
|
+
if (oldViewProps.locale != newViewProps.locale) {
|
|
138
|
+
_datePickerView.localeIdentifier =
|
|
139
|
+
(!newViewProps.locale.empty())
|
|
140
|
+
? [NSString stringWithUTF8String:newViewProps.locale.c_str()]
|
|
141
|
+
: nil;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// time zone
|
|
145
|
+
if (oldViewProps.timeZoneName != newViewProps.timeZoneName) {
|
|
146
|
+
_datePickerView.timeZoneName =
|
|
147
|
+
(!newViewProps.timeZoneName.empty())
|
|
148
|
+
? [NSString stringWithUTF8String:newViewProps.timeZoneName.c_str()]
|
|
149
|
+
: nil;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// mode
|
|
153
|
+
if (oldViewProps.mode != newViewProps.mode) {
|
|
154
|
+
_datePickerView.mode =
|
|
155
|
+
(!newViewProps.mode.empty())
|
|
156
|
+
? [NSString stringWithUTF8String:newViewProps.mode.c_str()]
|
|
157
|
+
: @"date";
|
|
158
|
+
needsToUpdateMeasurements = YES;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ----- iOS nested props -----
|
|
162
|
+
const auto &oldIos = oldViewProps.ios;
|
|
163
|
+
const auto &newIos = newViewProps.ios;
|
|
164
|
+
|
|
165
|
+
if (oldIos.preferredStyle != newIos.preferredStyle) {
|
|
166
|
+
_datePickerView.preferredStyle =
|
|
167
|
+
(!newIos.preferredStyle.empty())
|
|
168
|
+
? [NSString stringWithUTF8String:newIos.preferredStyle.c_str()]
|
|
169
|
+
: nil;
|
|
170
|
+
needsToUpdateMeasurements = YES;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (oldIos.countDownDurationSeconds != newIos.countDownDurationSeconds) {
|
|
174
|
+
_datePickerView.countDownDurationSeconds =
|
|
175
|
+
@(newIos.countDownDurationSeconds);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (oldIos.minuteInterval != newIos.minuteInterval) {
|
|
179
|
+
_datePickerView.minuteIntervalValue = @(newIos.minuteInterval);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Expecting: "inherit" | "round" | "noRound"
|
|
183
|
+
if (oldIos.roundsToMinuteInterval != newIos.roundsToMinuteInterval) {
|
|
184
|
+
if (!newIos.roundsToMinuteInterval.empty()) {
|
|
185
|
+
_datePickerView.roundsToMinuteIntervalMode =
|
|
186
|
+
[NSString stringWithUTF8String:newIos.roundsToMinuteInterval.c_str()];
|
|
187
|
+
} else {
|
|
188
|
+
_datePickerView.roundsToMinuteIntervalMode = @"inherit";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (needsToUpdateMeasurements) {
|
|
193
|
+
[self updateMeasurements];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
[super updateProps:props oldProps:oldProps];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
200
|
+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
|
|
201
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
202
|
+
|
|
203
|
+
// Fill whatever Yoga decided the content frame is.
|
|
204
|
+
_datePickerView.frame = self.contentView.bounds;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#pragma mark - State (Measuring)
|
|
208
|
+
|
|
209
|
+
- (void)updateState:(const State::Shared &)state
|
|
210
|
+
oldState:(const State::Shared &)oldState {
|
|
211
|
+
_state = std::static_pointer_cast<
|
|
212
|
+
const MeasuringPCDatePickerShadowNode::ConcreteState>(state);
|
|
213
|
+
|
|
214
|
+
if (oldState == nullptr) {
|
|
215
|
+
// First time: compute initial size.
|
|
216
|
+
[self updateMeasurements];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[super updateState:state oldState:oldState];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
- (void)updateMeasurements {
|
|
223
|
+
if (_state == nullptr)
|
|
224
|
+
return;
|
|
225
|
+
|
|
226
|
+
// Use the real width Yoga gave us (bounds is correct here after layoutMetrics
|
|
227
|
+
// update)
|
|
228
|
+
const CGFloat w = self.bounds.size.width > 1 ? self.bounds.size.width : 320;
|
|
229
|
+
|
|
230
|
+
CGSize size =
|
|
231
|
+
[_datePickerView sizeForLayoutWithConstrainedTo:CGSizeMake(w, 0)];
|
|
232
|
+
|
|
233
|
+
PCDatePickerStateFrameSize next;
|
|
234
|
+
next.frameSize = {(Float)size.width, (Float)size.height};
|
|
235
|
+
_state->updateState(std::move(next));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#pragma mark - Strongly typed emitter
|
|
239
|
+
|
|
240
|
+
- (const PCDatePickerEventEmitter &)eventEmitterTyped {
|
|
241
|
+
return static_cast<const PCDatePickerEventEmitter &>(*_eventEmitter);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@end
|
|
245
|
+
|
|
246
|
+
Class<RCTComponentViewProtocol> RCTPCDatePickerCls(void) {
|
|
247
|
+
return PCDatePicker.class;
|
|
248
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@objcMembers
|
|
4
|
+
public final class PCDatePickerView: UIControl,
|
|
5
|
+
UIPopoverPresentationControllerDelegate,
|
|
6
|
+
UIAdaptivePresentationControllerDelegate
|
|
7
|
+
{
|
|
8
|
+
// MARK: - UI
|
|
9
|
+
private let picker = UIDatePicker()
|
|
10
|
+
private var modalVC: UIViewController?
|
|
11
|
+
|
|
12
|
+
// Suppress “programmatic” valueChanged events (apply props / initial present settle).
|
|
13
|
+
private var suppressChangeEvents = false
|
|
14
|
+
private func suppressNextChangesBriefly() {
|
|
15
|
+
suppressChangeEvents = true
|
|
16
|
+
// Clear on next runloop tick (usually enough to skip the “settle” event).
|
|
17
|
+
DispatchQueue.main.async { [weak self] in
|
|
18
|
+
self?.suppressChangeEvents = false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - Events (wired from ObjC++)
|
|
23
|
+
public var onChangeHandler: ((NSNumber) -> Void)?
|
|
24
|
+
public var onCancelHandler: (() -> Void)?
|
|
25
|
+
|
|
26
|
+
// MARK: - Props
|
|
27
|
+
|
|
28
|
+
/// "date" | "time" | "dateAndTime" | "countDownTimer"
|
|
29
|
+
public var mode: String = "date" {
|
|
30
|
+
didSet {
|
|
31
|
+
if oldValue != mode {
|
|
32
|
+
applyMode()
|
|
33
|
+
invalidateSize()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// "modal" | "inline"
|
|
39
|
+
public var presentation: String = "modal" {
|
|
40
|
+
didSet {
|
|
41
|
+
if oldValue != presentation {
|
|
42
|
+
applyPresentation()
|
|
43
|
+
invalidateSize()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// modal only: NSNumber(0/1)
|
|
49
|
+
public var open: NSNumber? {
|
|
50
|
+
didSet {
|
|
51
|
+
guard oldValue != open else { return }
|
|
52
|
+
applyOpen()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public var dateMs: NSNumber? {
|
|
57
|
+
didSet { applyDateMs(animated: false) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public var minDateMs: NSNumber? { didSet { applyMinMax() } }
|
|
61
|
+
public var maxDateMs: NSNumber? { didSet { applyMinMax() } }
|
|
62
|
+
|
|
63
|
+
public var localeIdentifier: String? { didSet { applyLocale() } }
|
|
64
|
+
public var timeZoneName: String? { didSet { applyTimeZone() } }
|
|
65
|
+
|
|
66
|
+
/// "wheels" | "compact" | "inline" | "automatic"
|
|
67
|
+
public var preferredStyle: String? {
|
|
68
|
+
didSet {
|
|
69
|
+
if oldValue != preferredStyle {
|
|
70
|
+
applyPreferredStyle()
|
|
71
|
+
invalidateSize()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public var countDownDurationSeconds: NSNumber? { didSet { applyCountDownDuration() } }
|
|
77
|
+
public var minuteIntervalValue: NSNumber? { didSet { applyMinuteInterval() } }
|
|
78
|
+
|
|
79
|
+
/// "inherit" | "round" | "noRound"
|
|
80
|
+
public var roundsToMinuteIntervalMode: String = "inherit" {
|
|
81
|
+
didSet { if oldValue != roundsToMinuteIntervalMode { applyRoundsMode() } }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - Init
|
|
85
|
+
|
|
86
|
+
public override init(frame: CGRect) {
|
|
87
|
+
super.init(frame: frame)
|
|
88
|
+
commonInit()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public required init?(coder: NSCoder) {
|
|
92
|
+
super.init(coder: coder)
|
|
93
|
+
commonInit()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func commonInit() {
|
|
97
|
+
backgroundColor = .clear
|
|
98
|
+
isOpaque = false
|
|
99
|
+
|
|
100
|
+
isUserInteractionEnabled = true
|
|
101
|
+
picker.isUserInteractionEnabled = true
|
|
102
|
+
|
|
103
|
+
picker.translatesAutoresizingMaskIntoConstraints = false
|
|
104
|
+
picker.addTarget(self, action: #selector(handleValueChanged), for: .valueChanged)
|
|
105
|
+
|
|
106
|
+
applyMode()
|
|
107
|
+
applyPreferredStyle()
|
|
108
|
+
applyLocale()
|
|
109
|
+
applyTimeZone()
|
|
110
|
+
applyMinMax()
|
|
111
|
+
applyMinuteInterval()
|
|
112
|
+
applyCountDownDuration()
|
|
113
|
+
applyRoundsMode()
|
|
114
|
+
applyPresentation()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MARK: - Layout / Sizing
|
|
118
|
+
|
|
119
|
+
private func invalidateSize() {
|
|
120
|
+
invalidateIntrinsicContentSize()
|
|
121
|
+
setNeedsLayout()
|
|
122
|
+
superview?.setNeedsLayout()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// ✅ Inline only takes space; modal/headless must be 0 height.
|
|
126
|
+
public override var intrinsicContentSize: CGSize {
|
|
127
|
+
guard presentation == "embedded" else {
|
|
128
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
picker.setNeedsLayout()
|
|
132
|
+
picker.layoutIfNeeded()
|
|
133
|
+
let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
|
134
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: max(44, fitted.height))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// ✅ Called by your measuring pipeline.
|
|
138
|
+
/// Modal/headless should be zero so Yoga reserves nothing.
|
|
139
|
+
@objc public func sizeForLayout(withConstrainedTo constrainedSize: CGSize) -> CGSize {
|
|
140
|
+
guard presentation == "embedded" else { return .zero }
|
|
141
|
+
|
|
142
|
+
picker.setNeedsLayout()
|
|
143
|
+
picker.layoutIfNeeded()
|
|
144
|
+
|
|
145
|
+
let width =
|
|
146
|
+
(constrainedSize.width.isFinite && constrainedSize.width > 1)
|
|
147
|
+
? constrainedSize.width : 320
|
|
148
|
+
let fitted = picker.systemLayoutSizeFitting(
|
|
149
|
+
CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
|
|
150
|
+
withHorizontalFittingPriority: .required,
|
|
151
|
+
verticalFittingPriority: .fittingSizeLevel
|
|
152
|
+
)
|
|
153
|
+
return CGSize(width: constrainedSize.width, height: max(44, fitted.height))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Separate sizing for popover content.
|
|
157
|
+
private func popoverContentSize() -> CGSize {
|
|
158
|
+
picker.setNeedsLayout()
|
|
159
|
+
picker.layoutIfNeeded()
|
|
160
|
+
|
|
161
|
+
let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
|
162
|
+
let minH: CGFloat = (preferredStyle == "wheels") ? 216 : 160
|
|
163
|
+
let minW: CGFloat = 280
|
|
164
|
+
return CGSize(width: max(minW, fitted.width), height: max(minH, fitted.height))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MARK: - Presentation
|
|
168
|
+
|
|
169
|
+
private func applyPresentation() {
|
|
170
|
+
if presentation == "embedded" {
|
|
171
|
+
dismissIfNeeded(emitCancel: false)
|
|
172
|
+
attachInlinePickerIfNeeded()
|
|
173
|
+
} else {
|
|
174
|
+
detachInlinePickerIfNeeded()
|
|
175
|
+
applyOpen()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func attachInlinePickerIfNeeded() {
|
|
180
|
+
guard picker.superview !== self else { return }
|
|
181
|
+
|
|
182
|
+
picker.removeFromSuperview()
|
|
183
|
+
addSubview(picker)
|
|
184
|
+
|
|
185
|
+
NSLayoutConstraint.activate([
|
|
186
|
+
picker.topAnchor.constraint(equalTo: topAnchor),
|
|
187
|
+
picker.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
188
|
+
picker.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
189
|
+
picker.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
190
|
+
])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func detachInlinePickerIfNeeded() {
|
|
194
|
+
if picker.superview === self {
|
|
195
|
+
picker.removeFromSuperview()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private func applyOpen() {
|
|
200
|
+
guard presentation == "modal" else { return }
|
|
201
|
+
let shouldOpen = open?.boolValue ?? false
|
|
202
|
+
if shouldOpen { presentIfNeeded() } else { dismissIfNeeded(emitCancel: false) }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Modal Popover
|
|
206
|
+
|
|
207
|
+
private func presentIfNeeded() {
|
|
208
|
+
guard modalVC == nil else { return }
|
|
209
|
+
guard let top = topViewController() else { return }
|
|
210
|
+
|
|
211
|
+
// Prevent “settle” events right as we present.
|
|
212
|
+
suppressNextChangesBriefly()
|
|
213
|
+
|
|
214
|
+
// Ensure picker is not inline.
|
|
215
|
+
detachInlinePickerIfNeeded()
|
|
216
|
+
|
|
217
|
+
let vc = UIViewController()
|
|
218
|
+
vc.view.backgroundColor = .clear
|
|
219
|
+
vc.view.isOpaque = false
|
|
220
|
+
|
|
221
|
+
picker.translatesAutoresizingMaskIntoConstraints = false
|
|
222
|
+
vc.view.addSubview(picker)
|
|
223
|
+
|
|
224
|
+
NSLayoutConstraint.activate([
|
|
225
|
+
picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
|
|
226
|
+
picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor),
|
|
227
|
+
picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
|
|
228
|
+
picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
let size = popoverContentSize()
|
|
232
|
+
vc.preferredContentSize = size
|
|
233
|
+
|
|
234
|
+
// ✅ Anchored popover-style (not a full sheet)
|
|
235
|
+
vc.modalPresentationStyle = .popover
|
|
236
|
+
vc.presentationController?.delegate = self
|
|
237
|
+
|
|
238
|
+
if let pop = vc.popoverPresentationController {
|
|
239
|
+
pop.delegate = self
|
|
240
|
+
pop.sourceView = self
|
|
241
|
+
pop.sourceRect = bounds
|
|
242
|
+
pop.permittedArrowDirections = [.up, .down]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
modalVC = vc
|
|
246
|
+
top.present(vc, animated: true)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private func dismissIfNeeded(emitCancel: Bool) {
|
|
250
|
+
guard let vc = modalVC else { return }
|
|
251
|
+
modalVC = nil
|
|
252
|
+
vc.dismiss(animated: true) { [weak self] in
|
|
253
|
+
guard let self else { return }
|
|
254
|
+
if emitCancel { self.onCancelHandler?() }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ✅ Critical: prevent popover → sheet adaptation in compact environments
|
|
259
|
+
public func adaptivePresentationStyle(for controller: UIPresentationController)
|
|
260
|
+
-> UIModalPresentationStyle
|
|
261
|
+
{
|
|
262
|
+
return .none
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Tap outside / dismiss
|
|
266
|
+
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController)
|
|
267
|
+
{
|
|
268
|
+
modalVC = nil
|
|
269
|
+
onCancelHandler?()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public func popoverPresentationControllerDidDismissPopover(
|
|
273
|
+
_ popoverPresentationController: UIPopoverPresentationController
|
|
274
|
+
) {
|
|
275
|
+
modalVC = nil
|
|
276
|
+
onCancelHandler?()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: - Value changes
|
|
280
|
+
|
|
281
|
+
@objc private func handleValueChanged() {
|
|
282
|
+
// Skip “programmatic/settle” changes
|
|
283
|
+
if suppressChangeEvents { return }
|
|
284
|
+
|
|
285
|
+
let ms = picker.date.timeIntervalSince1970 * 1000.0
|
|
286
|
+
onChangeHandler?(NSNumber(value: ms))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: - Apply props (avoid firing valueChanged)
|
|
290
|
+
|
|
291
|
+
private func applyDateMs(animated: Bool) {
|
|
292
|
+
guard let ms = dateMs?.doubleValue else { return }
|
|
293
|
+
suppressNextChangesBriefly()
|
|
294
|
+
picker.setDate(Date(timeIntervalSince1970: ms / 1000.0), animated: animated)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func applyMinMax() {
|
|
298
|
+
suppressNextChangesBriefly()
|
|
299
|
+
|
|
300
|
+
if let ms = minDateMs?.doubleValue {
|
|
301
|
+
picker.minimumDate = Date(timeIntervalSince1970: ms / 1000.0)
|
|
302
|
+
} else {
|
|
303
|
+
picker.minimumDate = nil
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if let ms = maxDateMs?.doubleValue {
|
|
307
|
+
picker.maximumDate = Date(timeIntervalSince1970: ms / 1000.0)
|
|
308
|
+
} else {
|
|
309
|
+
picker.maximumDate = nil
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private func applyLocale() {
|
|
314
|
+
suppressNextChangesBriefly()
|
|
315
|
+
if let id = localeIdentifier, !id.isEmpty {
|
|
316
|
+
picker.locale = Locale(identifier: id)
|
|
317
|
+
} else {
|
|
318
|
+
picker.locale = nil
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private func applyTimeZone() {
|
|
323
|
+
suppressNextChangesBriefly()
|
|
324
|
+
if let name = timeZoneName, let tz = TimeZone(identifier: name) {
|
|
325
|
+
picker.timeZone = tz
|
|
326
|
+
} else {
|
|
327
|
+
picker.timeZone = nil
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private func applyMode() {
|
|
332
|
+
suppressNextChangesBriefly()
|
|
333
|
+
switch mode {
|
|
334
|
+
case "date": picker.datePickerMode = .date
|
|
335
|
+
case "time": picker.datePickerMode = .time
|
|
336
|
+
case "dateAndTime": picker.datePickerMode = .dateAndTime
|
|
337
|
+
case "countDownTimer": picker.datePickerMode = .countDownTimer
|
|
338
|
+
default: picker.datePickerMode = .date
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private func applyPreferredStyle() {
|
|
343
|
+
guard #available(iOS 13.4, *) else { return }
|
|
344
|
+
suppressNextChangesBriefly()
|
|
345
|
+
let s = preferredStyle ?? "automatic"
|
|
346
|
+
switch s {
|
|
347
|
+
case "wheels": picker.preferredDatePickerStyle = .wheels
|
|
348
|
+
case "compact": picker.preferredDatePickerStyle = .compact
|
|
349
|
+
case "inline": picker.preferredDatePickerStyle = .inline
|
|
350
|
+
default: picker.preferredDatePickerStyle = .automatic
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private func applyCountDownDuration() {
|
|
355
|
+
guard picker.datePickerMode == .countDownTimer else { return }
|
|
356
|
+
suppressNextChangesBriefly()
|
|
357
|
+
if let secs = countDownDurationSeconds?.doubleValue {
|
|
358
|
+
picker.countDownDuration = secs
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private func applyMinuteInterval() {
|
|
363
|
+
guard let v = minuteIntervalValue?.intValue else { return }
|
|
364
|
+
suppressNextChangesBriefly()
|
|
365
|
+
picker.minuteInterval = max(1, min(30, v))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private func applyRoundsMode() {
|
|
369
|
+
guard #available(iOS 14.0, *) else { return }
|
|
370
|
+
suppressNextChangesBriefly()
|
|
371
|
+
switch roundsToMinuteIntervalMode {
|
|
372
|
+
case "round": picker.roundsToMinuteInterval = true
|
|
373
|
+
case "noRound": picker.roundsToMinuteInterval = false
|
|
374
|
+
default: break // inherit
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// MARK: - Top VC (same approach you were using)
|
|
379
|
+
|
|
380
|
+
private func topViewController() -> UIViewController? {
|
|
381
|
+
guard
|
|
382
|
+
let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene })
|
|
383
|
+
.first,
|
|
384
|
+
let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController
|
|
385
|
+
else { return nil }
|
|
386
|
+
|
|
387
|
+
var top = root
|
|
388
|
+
while true {
|
|
389
|
+
if let presented = top.presentedViewController {
|
|
390
|
+
top = presented
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
if let nav = top as? UINavigationController, let visible = nav.visibleViewController {
|
|
394
|
+
top = visible
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
if let tab = top as? UITabBarController, let selected = tab.selectedViewController {
|
|
398
|
+
top = selected
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
break
|
|
402
|
+
}
|
|
403
|
+
return top
|
|
404
|
+
}
|
|
405
|
+
}
|