@use-voltra/ios-client 2.1.0 → 2.2.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 +61 -8
- package/build/commonjs/dynamic-widget/api.js +15 -0
- package/build/commonjs/dynamic-widget/api.js.map +1 -0
- package/build/commonjs/index.js +15 -7
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/module/dynamic-widget/api.js +10 -0
- package/build/module/dynamic-widget/api.js.map +1 -0
- package/build/module/index.js +1 -0
- package/build/module/index.js.map +1 -1
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/typescript/commonjs/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -0
- package/build/typescript/commonjs/index.d.ts +1 -0
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +1 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -0
- package/build/typescript/module/index.d.ts +1 -0
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/native/NativeVoltra.d.ts +1 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/expo-plugin/build/cjs/index.js +3 -1
- package/expo-plugin/build/cjs/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/index.js +1 -1
- package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/podfile.js +38 -12
- package/expo-plugin/build/cjs/ios-widget/podfile.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +143 -107
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js +30 -22
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js +50 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +45 -43
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js +77 -91
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js +2 -2
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/target.js +29 -63
- package/expo-plugin/build/cjs/ios-widget/xcode/target.js.map +1 -1
- package/expo-plugin/build/esm/index.js +3 -1
- package/expo-plugin/build/esm/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/index.js +1 -1
- package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/podfile.js +36 -11
- package/expo-plugin/build/esm/ios-widget/podfile.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +143 -106
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js +31 -22
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js +45 -0
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +45 -42
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/index.js +81 -96
- package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js +3 -2
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/target.js +28 -62
- package/expo-plugin/build/esm/ios-widget/xcode/target.js.map +1 -1
- package/expo-plugin/build/types/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/podfile.d.ts +19 -1
- package/expo-plugin/build/types/ios-widget/podfile.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +3 -7
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts +15 -10
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts +16 -0
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +22 -3
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts +16 -4
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/target.d.ts +11 -26
- package/expo-plugin/build/types/ios-widget/xcode/target.d.ts.map +1 -1
- package/expo-plugin/src/index.ts +5 -1
- package/expo-plugin/src/ios-widget/index.ts +1 -1
- package/expo-plugin/src/ios-widget/podfile.ts +46 -13
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/fresh.pbxproj +197 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-existing-extension.pbxproj +321 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-per-config-signing.pbxproj +203 -0
- package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +357 -0
- package/expo-plugin/src/ios-widget/xcode/__tests__/pbxConsistency.ts +130 -0
- package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +151 -152
- package/expo-plugin/src/ios-widget/xcode/configurationList.ts +52 -35
- package/expo-plugin/src/ios-widget/xcode/fileReferences.ts +51 -0
- package/expo-plugin/src/ios-widget/xcode/groups.ts +52 -54
- package/expo-plugin/src/ios-widget/xcode/index.ts +107 -119
- package/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts +59 -54
- package/expo-plugin/src/ios-widget/xcode/productFile.ts +3 -2
- package/expo-plugin/src/ios-widget/xcode/target.ts +31 -83
- package/ios/Package.swift +7 -1
- package/ios/app/NativeVoltra.mm +19 -0
- package/ios/app/VoltraModule.swift +18 -0
- package/ios/app/VoltraModuleImpl.swift +16 -0
- package/ios/shared/DynamicWidgetPropsCodec.swift +51 -0
- package/ios/shared/DynamicWidgetPropsStorage.swift +53 -0
- package/ios/shared/DynamicWidgetPropsStore.swift +50 -0
- package/ios/shared/DynamicWidgetRenderCoordinator.swift +31 -0
- package/ios/shared/DynamicWidgetUpdater.swift +30 -0
- package/ios/shared/VoltraConstants.swift +4 -0
- package/ios/shared/VoltraJSRenderer.swift +3 -1
- package/ios/shared/VoltraNode.swift +6 -2
- package/ios/shared/VoltraWidgetDefaults.swift +9 -1
- package/ios/target/VoltraClientWidgetRuntime.swift +17 -5
- package/ios/target/VoltraHomeWidget.swift +6 -2
- package/ios/target/VoltraWidget.swift +3 -1
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +3 -1
- package/ios/ui/Helpers/VoltraProgressDriver.swift +6 -2
- package/ios/ui/Layout/VoltraFlexStackLayout.swift +9 -3
- package/ios/ui/Style/JSColorParser.swift +24 -8
- package/ios/ui/Style/JSGradientParser.swift +36 -12
- package/ios/ui/Style/JSStyleParser.swift +36 -12
- package/ios/ui/Style/LayoutStyle.swift +12 -4
- package/ios/ui/Views/VoltraChart.swift +52 -16
- package/ios/ui/Views/VoltraLinearGradient.swift +6 -2
- package/package.json +5 -5
- package/src/dynamic-widget/api.ts +25 -0
- package/src/index.ts +1 -0
- package/src/native/NativeVoltra.ts +1 -0
|
@@ -6,18 +6,9 @@ import { IOS } from '../../constants'
|
|
|
6
6
|
// Types
|
|
7
7
|
// ============================================================================
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface CreateTargetSkeletonOptions {
|
|
10
10
|
targetName: string
|
|
11
11
|
targetUuid: string
|
|
12
|
-
productFile: { fileRef: string }
|
|
13
|
-
xCConfigurationList: { uuid: string }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface AddNativeTargetOptions {
|
|
17
|
-
targetName: string
|
|
18
|
-
targetUuid: string
|
|
19
|
-
productFile: { fileRef: string }
|
|
20
|
-
xCConfigurationList: { uuid: string }
|
|
21
12
|
}
|
|
22
13
|
|
|
23
14
|
// ============================================================================
|
|
@@ -25,19 +16,39 @@ interface AddNativeTargetOptions {
|
|
|
25
16
|
// ============================================================================
|
|
26
17
|
|
|
27
18
|
/**
|
|
28
|
-
*
|
|
19
|
+
* Creates the minimal widget-extension native target and registers it with the project.
|
|
20
|
+
*
|
|
21
|
+
* A target cannot be "ensured" into existence, so this is the one create-only step in
|
|
22
|
+
* {@link applyXcodeChanges}: it adds an empty `PBXNativeTarget` (no product reference, configuration
|
|
23
|
+
* list or build phases yet) to the `PBXNativeTarget` and `PBXProject` sections. The shared ensure
|
|
24
|
+
* pipeline then fills in the configuration list, product file, group, build phases, attributes and
|
|
25
|
+
* dependency — the same code that reconciles an already-existing target.
|
|
29
26
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* - Adds the target to PBXProject section
|
|
33
|
-
* - Adds a dependency from the main app to the widget extension
|
|
27
|
+
* @returns The freshly created native-target object (the value stored in the `PBXNativeTarget`
|
|
28
|
+
* section under `targetUuid`).
|
|
34
29
|
*/
|
|
35
|
-
export function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
export function createTargetSkeleton(xcodeProject: XcodeProject, options: CreateTargetSkeletonOptions) {
|
|
31
|
+
const { targetName, targetUuid } = options
|
|
32
|
+
|
|
33
|
+
const target = {
|
|
34
|
+
uuid: targetUuid,
|
|
35
|
+
pbxNativeTarget: {
|
|
36
|
+
isa: 'PBXNativeTarget',
|
|
37
|
+
name: targetName,
|
|
38
|
+
productName: targetName,
|
|
39
|
+
productType: `"com.apple.product-type.app-extension"`,
|
|
40
|
+
buildConfigurationList: '',
|
|
41
|
+
productReference: '',
|
|
42
|
+
buildPhases: [],
|
|
43
|
+
buildRules: [],
|
|
44
|
+
dependencies: [],
|
|
45
|
+
},
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
xcodeProject.addToPbxNativeTargetSection(target)
|
|
49
|
+
xcodeProject.addToPbxProjectSection(target)
|
|
50
|
+
|
|
51
|
+
return xcodeProject.pbxNativeTargetSection()[targetUuid]
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
/**
|
|
@@ -83,66 +94,3 @@ export function ensureTargetDependency(xcodeProject: XcodeProject, targetUuid: s
|
|
|
83
94
|
xcodeProject.addTargetDependency(mainTargetUuid, [targetUuid])
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
|
-
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// Helper Functions
|
|
89
|
-
// ============================================================================
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Adds the widget extension target to the PBXNativeTarget section.
|
|
93
|
-
*/
|
|
94
|
-
function addToPbxNativeTargetSection(xcodeProject: XcodeProject, options: AddNativeTargetOptions) {
|
|
95
|
-
const { targetName, targetUuid, productFile, xCConfigurationList } = options
|
|
96
|
-
|
|
97
|
-
const target = {
|
|
98
|
-
uuid: targetUuid,
|
|
99
|
-
pbxNativeTarget: {
|
|
100
|
-
isa: 'PBXNativeTarget',
|
|
101
|
-
name: targetName,
|
|
102
|
-
productName: targetName,
|
|
103
|
-
productReference: productFile.fileRef,
|
|
104
|
-
productType: `"com.apple.product-type.app-extension"`,
|
|
105
|
-
buildConfigurationList: xCConfigurationList.uuid,
|
|
106
|
-
buildPhases: [],
|
|
107
|
-
buildRules: [],
|
|
108
|
-
dependencies: [],
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
xcodeProject.addToPbxNativeTargetSection(target)
|
|
113
|
-
|
|
114
|
-
return target
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Adds the target to the PBXProject section.
|
|
119
|
-
*/
|
|
120
|
-
function addToPbxProjectSection(xcodeProject: XcodeProject, target: { uuid: string }): void {
|
|
121
|
-
xcodeProject.addToPbxProjectSection(target)
|
|
122
|
-
|
|
123
|
-
// Add target attributes to project section
|
|
124
|
-
const projectSection = xcodeProject.pbxProjectSection()
|
|
125
|
-
const firstProject = xcodeProject.getFirstProject()
|
|
126
|
-
|
|
127
|
-
if (!projectSection[firstProject.uuid].attributes.TargetAttributes) {
|
|
128
|
-
projectSection[firstProject.uuid].attributes.TargetAttributes = {}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
projectSection[firstProject.uuid].attributes.TargetAttributes[target.uuid] = {
|
|
132
|
-
LastSwiftMigration: IOS.LAST_SWIFT_MIGRATION,
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Adds a target dependency so the main app depends on the widget extension.
|
|
138
|
-
*/
|
|
139
|
-
function addTargetDependency(xcodeProject: XcodeProject, target: { uuid: string }): void {
|
|
140
|
-
if (!xcodeProject.hash.project.objects['PBXTargetDependency']) {
|
|
141
|
-
xcodeProject.hash.project.objects['PBXTargetDependency'] = {}
|
|
142
|
-
}
|
|
143
|
-
if (!xcodeProject.hash.project.objects['PBXContainerItemProxy']) {
|
|
144
|
-
xcodeProject.hash.project.objects['PBXContainerItemProxy'] = {}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
xcodeProject.addTargetDependency(xcodeProject.getFirstTarget().uuid, [target.uuid])
|
|
148
|
-
}
|
package/ios/Package.swift
CHANGED
|
@@ -27,7 +27,6 @@ let package = Package(
|
|
|
27
27
|
"Date+toTimerInterval.swift",
|
|
28
28
|
"ShortNames.swift",
|
|
29
29
|
"VoltraAttributes.swift",
|
|
30
|
-
"VoltraConfig.swift",
|
|
31
30
|
"VoltraElement.swift",
|
|
32
31
|
"VoltraEvent.swift",
|
|
33
32
|
"VoltraEventBus.swift",
|
|
@@ -38,7 +37,14 @@ let package = Package(
|
|
|
38
37
|
"VoltraPersistentEventQueue.swift",
|
|
39
38
|
],
|
|
40
39
|
sources: [
|
|
40
|
+
"DynamicWidgetPropsCodec.swift",
|
|
41
|
+
"DynamicWidgetPropsStorage.swift",
|
|
42
|
+
"DynamicWidgetPropsStore.swift",
|
|
43
|
+
"DynamicWidgetRenderCoordinator.swift",
|
|
44
|
+
"DynamicWidgetUpdater.swift",
|
|
41
45
|
"JSONValue.swift",
|
|
46
|
+
"VoltraConfig.swift",
|
|
47
|
+
"VoltraConstants.swift",
|
|
42
48
|
"VoltraPayloadMigrator.swift",
|
|
43
49
|
"VoltraRegion.swift",
|
|
44
50
|
"ComponentTypeID.swift",
|
package/ios/app/NativeVoltra.mm
CHANGED
|
@@ -289,6 +289,25 @@
|
|
|
289
289
|
}];
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
- (void)updateDynamicWidget:(NSString *)dynamicWidgetId
|
|
293
|
+
dynamicWidgetPropsJson:(NSString *)dynamicWidgetPropsJson
|
|
294
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
295
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
296
|
+
{
|
|
297
|
+
[self.module updateDynamicWidget:dynamicWidgetId
|
|
298
|
+
dynamicWidgetPropsJson:dynamicWidgetPropsJson
|
|
299
|
+
completion:^(NSError *error) {
|
|
300
|
+
if (error) {
|
|
301
|
+
NSString *message = [NSString stringWithFormat:@"Failed to update Dynamic Widget '%@': %@",
|
|
302
|
+
dynamicWidgetId,
|
|
303
|
+
error.localizedDescription];
|
|
304
|
+
reject(@"updateDynamicWidget", message, error);
|
|
305
|
+
} else {
|
|
306
|
+
resolve(nil);
|
|
307
|
+
}
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
- (void)scheduleWidget:(NSString *)widgetId
|
|
293
312
|
timelineJson:(NSString *)timelineJson
|
|
294
313
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
@@ -145,6 +145,24 @@ public enum VoltraErrors: Error {
|
|
|
145
145
|
|
|
146
146
|
// MARK: - Home Screen Widgets
|
|
147
147
|
|
|
148
|
+
@objc public func updateDynamicWidget(
|
|
149
|
+
_ dynamicWidgetId: String,
|
|
150
|
+
dynamicWidgetPropsJson: String,
|
|
151
|
+
completion: @escaping (Error?) -> Void
|
|
152
|
+
) {
|
|
153
|
+
Task {
|
|
154
|
+
do {
|
|
155
|
+
try await impl.updateDynamicWidget(
|
|
156
|
+
dynamicWidgetId: dynamicWidgetId,
|
|
157
|
+
dynamicWidgetPropsJson: dynamicWidgetPropsJson
|
|
158
|
+
)
|
|
159
|
+
completion(nil)
|
|
160
|
+
} catch {
|
|
161
|
+
completion(error)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
@objc public func updateWidget(
|
|
149
167
|
_ widgetId: String,
|
|
150
168
|
jsonString: String,
|
|
@@ -257,6 +257,22 @@ public class VoltraModuleImpl {
|
|
|
257
257
|
|
|
258
258
|
// MARK: - Widgets
|
|
259
259
|
|
|
260
|
+
func updateDynamicWidget(
|
|
261
|
+
dynamicWidgetId: String,
|
|
262
|
+
dynamicWidgetPropsJson: String
|
|
263
|
+
) async throws {
|
|
264
|
+
let dynamicWidgetUpdater = DynamicWidgetUpdater(
|
|
265
|
+
dynamicWidgetPropsPersistence: DynamicWidgetPropsStore(),
|
|
266
|
+
dynamicWidgetTimelineReload: { dynamicWidgetId in
|
|
267
|
+
VoltraWidgetService.reloadTimeline(for: dynamicWidgetId)
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
try await dynamicWidgetUpdater.updateDynamicWidget(
|
|
271
|
+
dynamicWidgetID: dynamicWidgetId,
|
|
272
|
+
dynamicWidgetPropsJSON: dynamicWidgetPropsJson
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
260
276
|
func updateWidget(widgetId: String, jsonString: String, options: UpdateWidgetOptions?) async throws {
|
|
261
277
|
try VoltraWidgetService.setWidgetData(widgetId: widgetId, jsonString: jsonString, deepLinkUrl: options?.deepLinkUrl)
|
|
262
278
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum DynamicWidgetPropsCodec {
|
|
4
|
+
static let emptyDynamicWidgetPropsJSON = "{}"
|
|
5
|
+
|
|
6
|
+
private static let currentStorageVersion = 1
|
|
7
|
+
|
|
8
|
+
static func encodeStorageEntry(dynamicWidgetPropsJSON: String) throws -> String {
|
|
9
|
+
let dynamicWidgetProps: JSONValue
|
|
10
|
+
do {
|
|
11
|
+
dynamicWidgetProps = try JSONValue.parse(from: dynamicWidgetPropsJSON)
|
|
12
|
+
} catch {
|
|
13
|
+
throw DynamicWidgetPropsStoreError.invalidJSON
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
guard case .object = dynamicWidgetProps else {
|
|
17
|
+
throw DynamicWidgetPropsStoreError.topLevelValueMustBeObject
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let entry = DynamicWidgetPropsStorageEntry(
|
|
21
|
+
dynamicWidgetPropsStorageVersion: currentStorageVersion,
|
|
22
|
+
dynamicWidgetProps: dynamicWidgetProps
|
|
23
|
+
)
|
|
24
|
+
let data = try JSONEncoder().encode(entry)
|
|
25
|
+
guard let json = String(data: data, encoding: .utf8) else {
|
|
26
|
+
throw DynamicWidgetPropsStoreError.invalidJSON
|
|
27
|
+
}
|
|
28
|
+
return json
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static func decodeDynamicWidgetProps(storageEntryJSON: String?) -> String {
|
|
32
|
+
guard
|
|
33
|
+
let storageEntryJSON,
|
|
34
|
+
let data = storageEntryJSON.data(using: .utf8),
|
|
35
|
+
let entry = try? JSONDecoder().decode(DynamicWidgetPropsStorageEntry.self, from: data),
|
|
36
|
+
entry.dynamicWidgetPropsStorageVersion == currentStorageVersion,
|
|
37
|
+
case .object = entry.dynamicWidgetProps,
|
|
38
|
+
let dynamicWidgetPropsData = try? JSONEncoder().encode(entry.dynamicWidgetProps),
|
|
39
|
+
let dynamicWidgetPropsJSON = String(data: dynamicWidgetPropsData, encoding: .utf8)
|
|
40
|
+
else {
|
|
41
|
+
return emptyDynamicWidgetPropsJSON
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return dynamicWidgetPropsJSON
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private struct DynamicWidgetPropsStorageEntry: Codable {
|
|
49
|
+
let dynamicWidgetPropsStorageVersion: Int
|
|
50
|
+
let dynamicWidgetProps: JSONValue
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum DynamicWidgetPropsStoreError: Error, Equatable, LocalizedError {
|
|
4
|
+
case appGroupNotConfigured
|
|
5
|
+
case userDefaultsUnavailable
|
|
6
|
+
case persistenceFailed
|
|
7
|
+
case invalidJSON
|
|
8
|
+
case topLevelValueMustBeObject
|
|
9
|
+
|
|
10
|
+
var errorDescription: String? {
|
|
11
|
+
switch self {
|
|
12
|
+
case .appGroupNotConfigured:
|
|
13
|
+
return "App Group not configured. Set 'groupIdentifier' in the Voltra config plugin to use Dynamic Widgets."
|
|
14
|
+
case .userDefaultsUnavailable:
|
|
15
|
+
return "Unable to access UserDefaults for the Dynamic Widget App Group."
|
|
16
|
+
case .persistenceFailed:
|
|
17
|
+
return "Failed to persist Dynamic Widget props to the App Group."
|
|
18
|
+
case .invalidJSON:
|
|
19
|
+
return "Dynamic Widget props must be valid JSON."
|
|
20
|
+
case .topLevelValueMustBeObject:
|
|
21
|
+
return "Dynamic Widget props must be a top-level JSON object."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protocol DynamicWidgetPropsStorage {
|
|
27
|
+
func string(forKey key: String) throws -> String?
|
|
28
|
+
func set(_ value: String, forKey key: String) throws
|
|
29
|
+
func removeObject(forKey key: String) throws
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct DynamicWidgetPropsUserDefaultsStorage: DynamicWidgetPropsStorage {
|
|
33
|
+
private let userDefaults: UserDefaults
|
|
34
|
+
|
|
35
|
+
init(userDefaults: UserDefaults) {
|
|
36
|
+
self.userDefaults = userDefaults
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func string(forKey key: String) throws -> String? {
|
|
40
|
+
userDefaults.string(forKey: key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func set(_ value: String, forKey key: String) throws {
|
|
44
|
+
userDefaults.set(value, forKey: key)
|
|
45
|
+
guard userDefaults.synchronize() else {
|
|
46
|
+
throw DynamicWidgetPropsStoreError.persistenceFailed
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func removeObject(forKey key: String) throws {
|
|
51
|
+
userDefaults.removeObject(forKey: key)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Versioned, per-ID Dynamic Widget props shared by the app and widget extension processes.
|
|
4
|
+
struct DynamicWidgetPropsStore {
|
|
5
|
+
private let storage: Result<any DynamicWidgetPropsStorage, Error>
|
|
6
|
+
|
|
7
|
+
init() {
|
|
8
|
+
guard let groupIdentifier = VoltraConfig.groupIdentifier() else {
|
|
9
|
+
storage = .failure(DynamicWidgetPropsStoreError.appGroupNotConfigured)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
guard let userDefaults = UserDefaults(suiteName: groupIdentifier) else {
|
|
13
|
+
storage = .failure(DynamicWidgetPropsStoreError.userDefaultsUnavailable)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
storage = .success(DynamicWidgetPropsUserDefaultsStorage(userDefaults: userDefaults))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
init(storage: any DynamicWidgetPropsStorage) {
|
|
20
|
+
self.storage = .success(storage)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func persistDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for dynamicWidgetID: String) throws {
|
|
24
|
+
let storageEntryJSON = try DynamicWidgetPropsCodec.encodeStorageEntry(
|
|
25
|
+
dynamicWidgetPropsJSON: dynamicWidgetPropsJSON
|
|
26
|
+
)
|
|
27
|
+
let resolvedStorage = try storage.get()
|
|
28
|
+
try resolvedStorage.set(storageEntryJSON, forKey: Self.storageKey(for: dynamicWidgetID))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func dynamicWidgetProps(for dynamicWidgetID: String) -> String {
|
|
32
|
+
guard
|
|
33
|
+
case let .success(resolvedStorage) = storage,
|
|
34
|
+
let storageEntryJSON = try? resolvedStorage.string(forKey: Self.storageKey(for: dynamicWidgetID))
|
|
35
|
+
else {
|
|
36
|
+
return DynamicWidgetPropsCodec.emptyDynamicWidgetPropsJSON
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return DynamicWidgetPropsCodec.decodeDynamicWidgetProps(storageEntryJSON: storageEntryJSON)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func clearDynamicWidgetProps(for dynamicWidgetID: String) throws {
|
|
43
|
+
let resolvedStorage = try storage.get()
|
|
44
|
+
try resolvedStorage.removeObject(forKey: Self.storageKey(for: dynamicWidgetID))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static func storageKey(for dynamicWidgetID: String) -> String {
|
|
48
|
+
VoltraStorageKeys.dynamicWidgetPropsV1(dynamicWidgetID)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// Routes a Dynamic Widget's current persisted props and environment to its runtime boundary.
|
|
2
|
+
struct DynamicWidgetRenderCoordinator {
|
|
3
|
+
typealias DynamicWidgetPropsProvider = (_ dynamicWidgetID: String) -> String
|
|
4
|
+
typealias DynamicWidgetRuntimeBoundary = (
|
|
5
|
+
_ dynamicWidgetID: String,
|
|
6
|
+
_ dynamicWidgetPropsJSON: String,
|
|
7
|
+
_ dynamicWidgetEnvironmentJSON: String
|
|
8
|
+
) -> String?
|
|
9
|
+
|
|
10
|
+
private let dynamicWidgetPropsProvider: DynamicWidgetPropsProvider
|
|
11
|
+
private let dynamicWidgetRuntimeBoundary: DynamicWidgetRuntimeBoundary
|
|
12
|
+
|
|
13
|
+
init(
|
|
14
|
+
dynamicWidgetPropsProvider: @escaping DynamicWidgetPropsProvider,
|
|
15
|
+
dynamicWidgetRuntimeBoundary: @escaping DynamicWidgetRuntimeBoundary
|
|
16
|
+
) {
|
|
17
|
+
self.dynamicWidgetPropsProvider = dynamicWidgetPropsProvider
|
|
18
|
+
self.dynamicWidgetRuntimeBoundary = dynamicWidgetRuntimeBoundary
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func renderDynamicWidget(
|
|
22
|
+
dynamicWidgetID: String,
|
|
23
|
+
dynamicWidgetEnvironmentJSON: String
|
|
24
|
+
) -> String? {
|
|
25
|
+
dynamicWidgetRuntimeBoundary(
|
|
26
|
+
dynamicWidgetID,
|
|
27
|
+
dynamicWidgetPropsProvider(dynamicWidgetID),
|
|
28
|
+
dynamicWidgetEnvironmentJSON
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
protocol DynamicWidgetPropsPersistence {
|
|
2
|
+
func persistDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for dynamicWidgetID: String) throws
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
extension DynamicWidgetPropsStore: DynamicWidgetPropsPersistence {}
|
|
6
|
+
|
|
7
|
+
/// Persists runtime props before requesting a targeted Dynamic Widget timeline reload.
|
|
8
|
+
struct DynamicWidgetUpdater {
|
|
9
|
+
private let dynamicWidgetPropsPersistence: any DynamicWidgetPropsPersistence
|
|
10
|
+
private let dynamicWidgetTimelineReload: (String) async throws -> Void
|
|
11
|
+
|
|
12
|
+
init(
|
|
13
|
+
dynamicWidgetPropsPersistence: any DynamicWidgetPropsPersistence,
|
|
14
|
+
dynamicWidgetTimelineReload: @escaping (String) async throws -> Void
|
|
15
|
+
) {
|
|
16
|
+
self.dynamicWidgetPropsPersistence = dynamicWidgetPropsPersistence
|
|
17
|
+
self.dynamicWidgetTimelineReload = dynamicWidgetTimelineReload
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func updateDynamicWidget(
|
|
21
|
+
dynamicWidgetID: String,
|
|
22
|
+
dynamicWidgetPropsJSON: String
|
|
23
|
+
) async throws {
|
|
24
|
+
try dynamicWidgetPropsPersistence.persistDynamicWidgetProps(
|
|
25
|
+
dynamicWidgetPropsJSON,
|
|
26
|
+
for: dynamicWidgetID
|
|
27
|
+
)
|
|
28
|
+
try await dynamicWidgetTimelineReload(dynamicWidgetID)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -28,6 +28,10 @@ public enum VoltraStorageKeys {
|
|
|
28
28
|
"Voltra_Widget_ServerUrl_\(widgetId)"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
public static func dynamicWidgetPropsV1(_ dynamicWidgetId: String) -> String {
|
|
32
|
+
"Voltra_DynamicWidget_Props_v1_\(dynamicWidgetId)"
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
// MARK: - Prefixes / kind identifiers
|
|
32
36
|
|
|
33
37
|
public static let widgetKindPrefix = "Voltra_Widget_"
|
|
@@ -155,7 +155,9 @@ public enum VoltraJSRenderer {
|
|
|
155
155
|
// MARK: - Lifecycle
|
|
156
156
|
|
|
157
157
|
private static func context() -> JSContext? {
|
|
158
|
-
if let existing = _context {
|
|
158
|
+
if let existing = _context {
|
|
159
|
+
return existing
|
|
160
|
+
}
|
|
159
161
|
guard let ctx = JSContext() else {
|
|
160
162
|
VoltraLogger.widget.error("[\(TAG)] Failed to create JSContext")
|
|
161
163
|
return nil
|
|
@@ -55,7 +55,9 @@ public indirect enum VoltraNode: Hashable, View {
|
|
|
55
55
|
|
|
56
56
|
/// Check if node represents nothing
|
|
57
57
|
public var isEmpty: Bool {
|
|
58
|
-
if case .empty = self {
|
|
58
|
+
if case .empty = self {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
59
61
|
return false
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -72,7 +74,9 @@ public indirect enum VoltraNode: Hashable, View {
|
|
|
72
74
|
// Extract stylesheet (key "s")
|
|
73
75
|
if case let .array(stylesheetArray) = rootObject["s"] {
|
|
74
76
|
stylesheet = stylesheetArray.compactMap { item in
|
|
75
|
-
if case let .object(dict) = item {
|
|
77
|
+
if case let .object(dict) = item {
|
|
78
|
+
return dict
|
|
79
|
+
}
|
|
76
80
|
return nil
|
|
77
81
|
}
|
|
78
82
|
}
|
|
@@ -86,13 +86,17 @@ public enum VoltraWidgetDefaults {
|
|
|
86
86
|
|
|
87
87
|
// MARK: - Remove
|
|
88
88
|
|
|
89
|
-
/// Removes all persisted data (json, deepLinkUrl, timeline) for a single widget.
|
|
89
|
+
/// Removes all persisted data (json, deepLinkUrl, timeline, Dynamic Widget props) for a single widget.
|
|
90
90
|
/// This is the single source of truth for which keys exist per widget.
|
|
91
91
|
public static func removeAllData(for widgetId: String) {
|
|
92
92
|
guard let defaults = try? resolvedDefaults() else { return }
|
|
93
93
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
|
|
94
94
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
|
|
95
95
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
|
|
96
|
+
let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
|
|
97
|
+
storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
|
|
98
|
+
)
|
|
99
|
+
try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
|
|
96
100
|
defaults.synchronize()
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -105,11 +109,15 @@ public enum VoltraWidgetDefaults {
|
|
|
105
109
|
/// Removes all persisted data for every widget listed in the app's Info.plist.
|
|
106
110
|
public static func removeAllWidgets() {
|
|
107
111
|
guard let defaults = try? resolvedDefaults() else { return }
|
|
112
|
+
let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
|
|
113
|
+
storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
|
|
114
|
+
)
|
|
108
115
|
let widgetIds = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetIds) as? [String] ?? []
|
|
109
116
|
for widgetId in widgetIds {
|
|
110
117
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
|
|
111
118
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
|
|
112
119
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
|
|
120
|
+
try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
|
|
113
121
|
}
|
|
114
122
|
defaults.synchronize()
|
|
115
123
|
}
|
|
@@ -351,10 +351,20 @@ public struct VoltraClientWidgetContentView: View {
|
|
|
351
351
|
locale: locale,
|
|
352
352
|
configuration: entry.configuration
|
|
353
353
|
)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
let dynamicWidgetPropsStore = DynamicWidgetPropsStore()
|
|
355
|
+
let dynamicWidgetRenderCoordinator = DynamicWidgetRenderCoordinator(
|
|
356
|
+
dynamicWidgetPropsProvider: dynamicWidgetPropsStore.dynamicWidgetProps(for:),
|
|
357
|
+
dynamicWidgetRuntimeBoundary: { dynamicWidgetID, dynamicWidgetPropsJSON, dynamicWidgetEnvironmentJSON in
|
|
358
|
+
VoltraJSRenderer.render(
|
|
359
|
+
widgetId: dynamicWidgetID,
|
|
360
|
+
propsJSON: dynamicWidgetPropsJSON,
|
|
361
|
+
envJSON: dynamicWidgetEnvironmentJSON
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
if let resolved = dynamicWidgetRenderCoordinator.renderDynamicWidget(
|
|
366
|
+
dynamicWidgetID: entry.widgetId,
|
|
367
|
+
dynamicWidgetEnvironmentJSON: envJSON
|
|
358
368
|
), let node = parseResolvedNode(jsonString: resolved) {
|
|
359
369
|
return VoltraHomeWidgetEntry(date: entry.date, rootNode: node, widgetId: entry.widgetId)
|
|
360
370
|
}
|
|
@@ -368,7 +378,9 @@ public struct VoltraClientWidgetContentView: View {
|
|
|
368
378
|
private func parseResolvedNode(jsonString: String) -> VoltraNode? {
|
|
369
379
|
guard let json = try? JSONValue.parse(from: jsonString) else { return nil }
|
|
370
380
|
let node = VoltraNode.parse(from: json)
|
|
371
|
-
if case .empty = node {
|
|
381
|
+
if case .empty = node {
|
|
382
|
+
return nil
|
|
383
|
+
}
|
|
372
384
|
return node
|
|
373
385
|
}
|
|
374
386
|
|
|
@@ -461,7 +461,9 @@ private extension View {
|
|
|
461
461
|
private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
462
462
|
// Prefer the timeline entry's deep link URL if available
|
|
463
463
|
if let entryUrl = entry.deepLinkUrl, !entryUrl.isEmpty {
|
|
464
|
-
if entryUrl.contains("://"), let url = URL(string: entryUrl) {
|
|
464
|
+
if entryUrl.contains("://"), let url = URL(string: entryUrl) {
|
|
465
|
+
return url
|
|
466
|
+
}
|
|
465
467
|
if let scheme = VoltraDeepLinkResolver.deepLinkScheme() {
|
|
466
468
|
let path = entryUrl.hasPrefix("/") ? entryUrl : "/\(entryUrl)"
|
|
467
469
|
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
@@ -470,7 +472,9 @@ private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
|
470
472
|
|
|
471
473
|
// Fallback to static deep link URL from UserDefaults
|
|
472
474
|
if let raw = VoltraHomeWidgetStore.readDeepLinkUrl(widgetId: entry.widgetId), !raw.isEmpty {
|
|
473
|
-
if raw.contains("://"), let url = URL(string: raw) {
|
|
475
|
+
if raw.contains("://"), let url = URL(string: raw) {
|
|
476
|
+
return url
|
|
477
|
+
}
|
|
474
478
|
if let scheme = VoltraDeepLinkResolver.deepLinkScheme() {
|
|
475
479
|
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
|
476
480
|
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
@@ -9,7 +9,9 @@ public struct VoltraWidget: Widget {
|
|
|
9
9
|
/// Convert an array of nodes to a single root node for rendering
|
|
10
10
|
private func rootNode(for region: VoltraRegion, from state: VoltraAttributes.ContentState) -> VoltraNode {
|
|
11
11
|
let nodes = state.regions[region] ?? []
|
|
12
|
-
if nodes.isEmpty {
|
|
12
|
+
if nodes.isEmpty {
|
|
13
|
+
return .empty
|
|
14
|
+
}
|
|
13
15
|
return nodes.count == 1 ? nodes[0] : .array(nodes)
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -30,7 +30,9 @@ enum VoltraDeepLinkResolver {
|
|
|
30
30
|
guard !raw.isEmpty else { return nil }
|
|
31
31
|
|
|
32
32
|
// If it's already an absolute URL, use it as-is
|
|
33
|
-
if raw.contains("://"), let url = URL(string: raw) {
|
|
33
|
+
if raw.contains("://"), let url = URL(string: raw) {
|
|
34
|
+
return url
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
// Otherwise, prepend the app's URL scheme
|
|
36
38
|
if let scheme = deepLinkScheme() {
|
|
@@ -43,10 +43,14 @@ enum VoltraProgressDriver {
|
|
|
43
43
|
/// The progress value is clamped to `[0, 1]` unless `clamp` is `false`.
|
|
44
44
|
static func progress(for range: ClosedRange<Date>, at date: Date = Date(), clamp: Bool = true) -> Double {
|
|
45
45
|
let total = range.upperBound.timeIntervalSince(range.lowerBound)
|
|
46
|
-
if total <= 0 {
|
|
46
|
+
if total <= 0 {
|
|
47
|
+
return clamp ? 1 : 0
|
|
48
|
+
}
|
|
47
49
|
let elapsed = date.timeIntervalSince(range.lowerBound)
|
|
48
50
|
let ratio = elapsed / total
|
|
49
|
-
if !clamp {
|
|
51
|
+
if !clamp {
|
|
52
|
+
return ratio
|
|
53
|
+
}
|
|
50
54
|
return min(max(ratio, 0), 1)
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -250,7 +250,9 @@ struct VoltraFlexStackLayout: Layout {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
// If stretch and no explicit cross size, use full cross axis (only finite values)
|
|
253
|
-
if let proposal = proposal, proposal.isFinite {
|
|
253
|
+
if let proposal = proposal, proposal.isFinite {
|
|
254
|
+
return proposal
|
|
255
|
+
}
|
|
254
256
|
// Otherwise, measure intrinsic
|
|
255
257
|
let size = subview.sizeThatFits(ProposedViewSize(width: nil, height: nil))
|
|
256
258
|
return crossAxis(size)
|
|
@@ -266,8 +268,12 @@ struct VoltraFlexStackLayout: Layout {
|
|
|
266
268
|
|
|
267
269
|
private func clamp(_ value: CGFloat, min: CGFloat?, max: CGFloat?) -> CGFloat {
|
|
268
270
|
var result = value
|
|
269
|
-
if let min = min {
|
|
270
|
-
|
|
271
|
+
if let min = min {
|
|
272
|
+
result = Swift.max(result, min)
|
|
273
|
+
}
|
|
274
|
+
if let max = max {
|
|
275
|
+
result = Swift.min(result, max)
|
|
276
|
+
}
|
|
271
277
|
return result
|
|
272
278
|
}
|
|
273
279
|
|