Skip to content

Commit 1f61300

Browse files
committed
Add fileImporter support
1 parent ddc557e commit 1f61300

File tree

5 files changed

+477
-335
lines changed

5 files changed

+477
-335
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// FileImporterModifier.swift
3+
//
4+
//
5+
// Created by Carson Katri on 10/23/24.
6+
//
7+
8+
import SwiftUI
9+
import LiveViewNativeCore
10+
import LiveViewNativeStylesheet
11+
import UniformTypeIdentifiers
12+
import OSLog
13+
14+
private let logger = Logger(subsystem: "LiveViewNative", category: "_FileImporterModifier")
15+
16+
/// See [`SwiftUI.View/fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)`](https://developer.apple.com/documentation/swiftui/view/fileimporter(ispresented:allowedcontenttypes:allowsmultipleselection:oncompletion:)) for more details on this ViewModifier.
17+
///
18+
/// ### fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)
19+
/// - `isPresented`: `attr("...")` (required)
20+
/// - `allowedContentTypes`: `attr("...")` or list of ``UniformTypeIdentifiers/UTType`` (required)
21+
/// - `allowsMultipleSelection`: `attr("...")` or ``Swift/Bool`` (required)
22+
///
23+
/// See [`SwiftUI.View/fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)`](https://developer.apple.com/documentation/swiftui/view/fileimporter(ispresented:allowedcontenttypes:allowsmultipleselection:oncompletion:)) for more details on this ViewModifier.
24+
///
25+
/// Example:
26+
///
27+
/// ```heex
28+
/// <.live_file_input upload={@uploads.avatar} />
29+
/// ```
30+
@_documentation(visibility: public)
31+
@ParseableExpression
32+
struct _FileImporterModifier<R: RootRegistry>: ViewModifier {
33+
static var name: String { "fileImporter" }
34+
35+
@Environment(\.formModel) private var formModel
36+
37+
private let id: AttributeReference<String>
38+
private let name: AttributeReference<String>
39+
@ChangeTracked private var isPresented: Bool
40+
private let allowedContentTypes: AttributeReference<UTType.ResolvableSet>
41+
private let allowsMultipleSelection: AttributeReference<Bool>
42+
43+
@ObservedElement private var element
44+
@LiveContext<R> private var context
45+
46+
@available(iOS 14.0, macOS 11.0, visionOS 1.0, *)
47+
init(
48+
id: AttributeReference<String>,
49+
name: AttributeReference<String>,
50+
isPresented: ChangeTracked<Bool>,
51+
allowedContentTypes: AttributeReference<UTType.ResolvableSet>,
52+
allowsMultipleSelection: AttributeReference<Bool>
53+
) {
54+
self.id = id
55+
self.name = name
56+
self._isPresented = isPresented
57+
self.allowedContentTypes = allowedContentTypes
58+
self.allowsMultipleSelection = allowsMultipleSelection
59+
}
60+
61+
func body(content: Content) -> some View {
62+
#if os(iOS) || os(macOS) || os(visionOS)
63+
content.fileImporter(
64+
isPresented: $isPresented,
65+
allowedContentTypes: allowedContentTypes.resolve(on: element, in: context).values,
66+
allowsMultipleSelection: allowsMultipleSelection.resolve(on: element, in: context)
67+
) { result in
68+
let id = id.resolve(on: element, in: context)
69+
70+
guard let liveChannel = context.coordinator.liveChannel
71+
else { return }
72+
73+
do {
74+
let files = try result.get().map({ url in
75+
LiveFile(
76+
try Data(contentsOf: url),
77+
url.pathExtension,
78+
url.deletingPathExtension().lastPathComponent,
79+
id
80+
)
81+
})
82+
Task {
83+
do {
84+
for file in files {
85+
try await liveChannel.validateUpload(file)
86+
}
87+
} catch {
88+
logger.log(level: .error, "\(error.localizedDescription)")
89+
}
90+
}
91+
self.formModel?.fileUploads.append(
92+
contentsOf: files.map({ file in
93+
{
94+
try await liveChannel.uploadFile(file)
95+
print("upload complete")
96+
}
97+
})
98+
)
99+
} catch {
100+
logger.log(level: .error, "\(error.localizedDescription)")
101+
}
102+
}
103+
#else
104+
content
105+
#endif
106+
}
107+
}
108+
109+
extension UTType: AttributeDecodable {
110+
struct ResolvableSet: AttributeDecodable, ParseableModifierValue {
111+
nonisolated let values: [UTType]
112+
113+
init(values: [UTType]) {
114+
self.values = values
115+
}
116+
117+
nonisolated init(from attribute: LiveViewNativeCore.Attribute?, on element: ElementNode) throws {
118+
guard let value = attribute?.value
119+
else { throw AttributeDecodingError.missingAttribute(Self.self) }
120+
self.values = value.split(separator: ",").compactMap({ UTType(filenameExtension: String($0.dropFirst())) })
121+
}
122+
123+
static func parser(in context: ParseableModifierContext) -> some Parser<Substring.UTF8View, Self> {
124+
Array<String>.parser(in: context).compactMap({ Self.init(values: $0.compactMap(UTType.init)) })
125+
}
126+
}
127+
}

Sources/LiveViewNative/ViewModel.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,14 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
5959
/// A publisher that emits a value before sending the form submission event.
6060
var formWillSubmit = PassthroughSubject<(), Never>()
6161

62+
var fileUploads: [() async throws -> ()] = []
63+
6264
init(elementID: String) {
6365
self.elementID = elementID
6466
}
6567

6668
@_spi(LiveForm) @preconcurrency public func updateFromElement(_ element: ElementNode, submitAction: @escaping () -> ()) {
69+
self.fileUploads.removeAll()
6770
let pushEventImpl = pushEventImpl!
6871
self.changeEvent = element.attributeValue(for: .init(name: "phx-change")).flatMap({ event in
6972
{ value in
@@ -95,6 +98,11 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
9598
/// See ``LiveViewCoordinator/pushEvent(type:event:value:target:)`` for more information.
9699
public func sendSubmitEvent() async throws {
97100
formWillSubmit.send(())
101+
for fileUpload in fileUploads {
102+
print("Upload...")
103+
try await fileUpload()
104+
}
105+
print("All uploads done")
98106
if let submitEvent = submitEvent {
99107
try await pushFormEvent(submitEvent)
100108
} else if let submitAction {

0 commit comments

Comments
 (0)