Skip to content

Commit 457ea7f

Browse files
authored
Re-add fileImporter modifier (#1547)
1 parent b19bb46 commit 457ea7f

File tree

8 files changed

+263
-5
lines changed

8 files changed

+263
-5
lines changed

Sources/LiveViewNative/Registries/CustomRegistry.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ public protocol RootRegistry: CustomRegistry where Root == Self {
230230
}
231231

232232
public struct _EmptyRegistryModifier: ViewModifier, Decodable {
233+
public nonisolated init(from decoder: any Decoder) throws {
234+
throw BuiltinRegistryModifierError.unknownModifier
235+
}
236+
233237
public func body(content: Content) -> some View {
234238
content
235239
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// BuiltinOverrideModifiers.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson.Katri on 2/27/25.
6+
//
7+
8+
import SwiftUI
9+
import LiveViewNativeStylesheet
10+
import LiveViewNativeCore
11+
12+
enum BuiltinOverrideModifiers<R: RootRegistry>: ViewModifier, @preconcurrency Decodable {
13+
#if os(iOS) || os(macOS) || os(visionOS)
14+
case fileImporter(FileImporterModifier<R>)
15+
#endif
16+
17+
init(from decoder: any Decoder) throws {
18+
let container = try decoder.singleValueContainer()
19+
20+
#if os(iOS) || os(macOS) || os(visionOS)
21+
if let fileImporter = try? container.decode(FileImporterModifier<R>.self) {
22+
self = .fileImporter(fileImporter)
23+
return
24+
}
25+
#endif
26+
27+
throw BuiltinRegistryModifierError.unknownModifier
28+
}
29+
30+
func body(content: Content) -> some View {
31+
switch self {
32+
#if os(iOS) || os(macOS) || os(visionOS)
33+
case let .fileImporter(modifier):
34+
content.modifier(modifier)
35+
#endif
36+
default:
37+
content
38+
}
39+
}
40+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// FileImporter.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson.Katri on 2/27/25.
6+
//
7+
8+
#if os(iOS) || os(macOS) || os(visionOS)
9+
import SwiftUI
10+
import LiveViewNativeCore
11+
import LiveViewNativeStylesheet
12+
import UniformTypeIdentifiers
13+
import OSLog
14+
15+
private let logger = Logger(subsystem: "LiveViewNative", category: "FileImporterModifier")
16+
17+
@ASTDecodable("fileImporter")
18+
@available(iOS 14.0, macOS 11.0, visionOS 1.0, *)
19+
struct FileImporterModifier<R: RootRegistry>: @preconcurrency Decodable, ViewModifier {
20+
@Environment(\.formModel) private var formModel
21+
22+
private let id: AttributeReference<String>
23+
private let name: AttributeReference<String>
24+
@ChangeTracked private var isPresented: Bool
25+
private let allowedContentTypes: AttributeReference<AllowedContentTypes>
26+
private let allowsMultipleSelection: AttributeReference<Bool>
27+
28+
@ObservedElement private var element
29+
@LiveContext<R> private var context
30+
31+
struct AllowedContentTypes: @preconcurrency Decodable, @preconcurrency AttributeDecodable {
32+
let value: [UTType]
33+
34+
init(from attribute: Attribute?, on element: ElementNode) throws {
35+
self.value = attribute?.value?.split(separator: ",")
36+
.flatMap({ UTType(filenameExtension: String($0.drop(while: { $0 == "." }))) })
37+
?? []
38+
}
39+
40+
init(from decoder: any Decoder) throws {
41+
self.value = try decoder.singleValueContainer().decode([UTType].self)
42+
}
43+
}
44+
45+
init(
46+
id: AttributeReference<String>,
47+
name: AttributeReference<String>,
48+
isPresented: ChangeTracked<Bool>,
49+
allowedContentTypes: AttributeReference<AllowedContentTypes>,
50+
allowsMultipleSelection: AttributeReference<Bool>
51+
) {
52+
self.id = id
53+
self.name = name
54+
self._isPresented = isPresented
55+
self.allowedContentTypes = allowedContentTypes
56+
self.allowsMultipleSelection = allowsMultipleSelection
57+
}
58+
59+
func body(content: Content) -> some View {
60+
#if os(iOS) || os(macOS) || os(visionOS)
61+
content.fileImporter(
62+
isPresented: $isPresented,
63+
allowedContentTypes: allowedContentTypes
64+
.resolve(on: element, in: context)
65+
.value,
66+
allowsMultipleSelection: allowsMultipleSelection.resolve(on: element, in: context)
67+
) { result in
68+
let id = id.resolve(on: element, in: context)
69+
70+
Task {
71+
do {
72+
for url in try result.get() {
73+
try await formModel?.queueFileUpload(name: name.resolve(on: element, in: context), id: id, url: url, coordinator: context.coordinator)
74+
}
75+
} catch {
76+
logger.log(level: .error, "\(error.localizedDescription)")
77+
}
78+
}
79+
}
80+
#else
81+
content
82+
#endif
83+
}
84+
}
85+
#endif

Sources/LiveViewNative/Utils/ElixirDateFormats.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ nonisolated(unsafe) fileprivate let dateTimeFormatter: ISO8601DateFormatter = {
1515
}()
1616

1717
/// A formatter that parses ISO8601 dates as produced by Elixir's `Date`.
18-
fileprivate let dateFormatter: DateFormatter = {
18+
nonisolated(unsafe) fileprivate let dateFormatter: DateFormatter = {
1919
let formatter = DateFormatter()
2020
formatter.dateFormat = "yyyy-MM-dd"
2121
return formatter

Sources/ModifierGenerator/ModifierGenerator.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ struct ModifierGenerator: ParsableCommand {
2323
func run() throws {
2424
guard let sdkRoot = ProcessInfo.processInfo.environment["SDKROOT"]
2525
.flatMap({ URL(filePath: $0) })
26-
else { throw ModifierGeneratorError.missingSDKROOT }
26+
else { throw ModifierGeneratorError.missingSDKROOT(environment: ProcessInfo.processInfo.environment) }
2727

2828
// get the `.swiftinterface` files from the matching SDK.
2929
let swiftUICoreInterface = try FileManager.default.contentsOfDirectory(at: sdkRoot.appending(path: "System/Library/Frameworks/SwiftUICore.framework/Modules/SwiftUICore.swiftmodule"), includingPropertiesForKeys: nil)
@@ -90,7 +90,14 @@ struct ModifierGenerator: ParsableCommand {
9090
}
9191
}
9292

93-
enum ModifierGeneratorError: Error {
93+
enum ModifierGeneratorError: LocalizedError {
9494
/// The `SDKROOT` environment variable is missing.
95-
case missingSDKROOT
95+
case missingSDKROOT(environment: [String:String])
96+
97+
var errorDescription: String? {
98+
switch self {
99+
case .missingSDKROOT(let environment):
100+
"Missing SDKROOT in environment: \(environment)"
101+
}
102+
}
96103
}

Sources/ModifierGenerator/denylist.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ let denylist = [
3333

3434
"onCommand",
3535

36-
"tag",
3736
"id",
3837

3938
"task",

Sources/ModifierGenerator/makeBuiltinModifier.swift

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ extension ModifierGenerator {
4444
])
4545
)
4646
])
47+
// case _builtinOverride(BuiltinOverrideModifiers<R>)
48+
EnumCaseDeclSyntax(elements: [
49+
EnumCaseElementSyntax(
50+
name: .identifier("_builtinOverride"),
51+
parameterClause: EnumCaseParameterClauseSyntax(parameters: [
52+
EnumCaseParameterSyntax(
53+
type: IdentifierTypeSyntax(name: .identifier("BuiltinOverrideModifiers"), genericArgumentClause: GenericArgumentClauseSyntax {
54+
GenericArgumentSyntax(argument: IdentifierTypeSyntax(name: .identifier("R")))
55+
})
56+
)
57+
])
58+
)
59+
])
4760
// case _imageModifier(ImageModifierRegistry)
4861
EnumCaseDeclSyntax(elements: [
4962
EnumCaseElementSyntax(
@@ -210,6 +223,87 @@ extension ModifierGenerator {
210223

211224
SwitchCaseSyntax(label: .default(SwitchDefaultLabelSyntax())) {
212225
// try specialty modifier types
226+
227+
// _customRegistry
228+
IfExprSyntax(conditions: ConditionElementListSyntax {
229+
ConditionElementSyntax(
230+
condition: .optionalBinding(OptionalBindingConditionSyntax(
231+
bindingSpecifier: .keyword(.let),
232+
pattern: IdentifierPatternSyntax(identifier: .identifier("modifier")),
233+
initializer: InitializerClauseSyntax(value: TryExprSyntax(
234+
questionOrExclamationMark: .postfixQuestionMarkToken(),
235+
expression: FunctionCallExprSyntax(
236+
callee: MemberAccessExprSyntax(
237+
base: DeclReferenceExprSyntax(baseName: .identifier("container")),
238+
period: .periodToken(),
239+
name: .identifier("decode")
240+
)
241+
) {
242+
LabeledExprSyntax(expression: MemberAccessExprSyntax(
243+
base: TypeExprSyntax(type: MemberTypeSyntax(
244+
baseType: IdentifierTypeSyntax(name: .identifier("R")),
245+
name: .identifier("CustomModifier")
246+
)),
247+
period: .periodToken(),
248+
name: .identifier("self")
249+
))
250+
}
251+
))
252+
))
253+
)
254+
}) {
255+
InfixOperatorExprSyntax(
256+
leftOperand: DeclReferenceExprSyntax(baseName: .identifier("self")),
257+
operator: AssignmentExprSyntax(),
258+
rightOperand: FunctionCallExprSyntax(
259+
callee: MemberAccessExprSyntax(name: .identifier("_customRegistry"))
260+
) {
261+
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: .identifier("modifier")))
262+
}
263+
)
264+
ReturnStmtSyntax()
265+
}
266+
267+
// _builtinOverride
268+
IfExprSyntax(conditions: ConditionElementListSyntax {
269+
ConditionElementSyntax(
270+
condition: .optionalBinding(OptionalBindingConditionSyntax(
271+
bindingSpecifier: .keyword(.let),
272+
pattern: IdentifierPatternSyntax(identifier: .identifier("modifier")),
273+
initializer: InitializerClauseSyntax(value: TryExprSyntax(
274+
questionOrExclamationMark: .postfixQuestionMarkToken(),
275+
expression: FunctionCallExprSyntax(
276+
callee: MemberAccessExprSyntax(
277+
base: DeclReferenceExprSyntax(baseName: .identifier("container")),
278+
period: .periodToken(),
279+
name: .identifier("decode")
280+
)
281+
) {
282+
LabeledExprSyntax(expression: MemberAccessExprSyntax(
283+
base: TypeExprSyntax(type: IdentifierTypeSyntax(name: .identifier("BuiltinOverrideModifiers"), genericArgumentClause: GenericArgumentClauseSyntax {
284+
GenericArgumentSyntax(argument: IdentifierTypeSyntax(name: .identifier("R")))
285+
})),
286+
period: .periodToken(),
287+
name: .identifier("self")
288+
))
289+
}
290+
))
291+
))
292+
)
293+
}) {
294+
InfixOperatorExprSyntax(
295+
leftOperand: DeclReferenceExprSyntax(baseName: .identifier("self")),
296+
operator: AssignmentExprSyntax(),
297+
rightOperand: FunctionCallExprSyntax(
298+
callee: MemberAccessExprSyntax(name: .identifier("_builtinOverride"))
299+
) {
300+
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: .identifier("modifier")))
301+
}
302+
)
303+
ReturnStmtSyntax()
304+
}
305+
306+
// _imageModifier
213307
IfExprSyntax(conditions: ConditionElementListSyntax {
214308
ConditionElementSyntax(
215309
condition: .optionalBinding(OptionalBindingConditionSyntax(
@@ -246,6 +340,7 @@ extension ModifierGenerator {
246340
ReturnStmtSyntax()
247341
}
248342

343+
// _shapeFinalizerModifier
249344
IfExprSyntax(conditions: ConditionElementListSyntax {
250345
ConditionElementSyntax(
251346
condition: .optionalBinding(OptionalBindingConditionSyntax(
@@ -419,6 +514,26 @@ extension ModifierGenerator {
419514
}
420515
}
421516

517+
// case let ._builtinOverride(modifier)
518+
SwitchCaseSyntax(label: .case(SwitchCaseLabelSyntax {
519+
SwitchCaseItemSyntax(pattern: ValueBindingPatternSyntax(
520+
bindingSpecifier: .keyword(.let),
521+
pattern: ExpressionPatternSyntax(expression: FunctionCallExprSyntax(
522+
callee: MemberAccessExprSyntax(name: .identifier("_builtinOverride"))
523+
) {
524+
LabeledExprSyntax(expression: PatternExprSyntax(pattern: IdentifierPatternSyntax(identifier: .identifier("modifier"))))
525+
})
526+
))
527+
})) {
528+
FunctionCallExprSyntax(callee: MemberAccessExprSyntax(
529+
base: DeclReferenceExprSyntax(baseName: .identifier("content")),
530+
period: .periodToken(),
531+
name: .identifier("modifier")
532+
)) {
533+
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: .identifier("modifier")))
534+
}
535+
}
536+
422537
// case let ._imageModifier(modifier)
423538
SwitchCaseSyntax(label: .case(SwitchCaseLabelSyntax {
424539
SwitchCaseItemSyntax(pattern: ValueBindingPatternSyntax(

Sources/SwiftSyntaxExtensions/Types.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ public extension TypeSyntaxProtocol {
9292
return false
9393
}
9494

95+
// Foundation.Predicate is not allowed
96+
if let memberType = self.as(MemberTypeSyntax.self),
97+
memberType.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Foundation",
98+
memberType.name.text == "Predicate"
99+
{
100+
return false
101+
}
102+
95103
// Decoder is not allowed
96104
if let memberType = self.as(MemberTypeSyntax.self),
97105
memberType.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift",

0 commit comments

Comments
 (0)