Skip to content

Commit

Permalink
Add fileImporter support
Browse files Browse the repository at this point in the history
  • Loading branch information
carson-katri committed Oct 23, 2024
1 parent c17e7c0 commit 337fa49
Show file tree
Hide file tree
Showing 6 changed files with 457 additions and 315 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {

var url: URL

private weak var liveChannel: LiveViewNativeCore.LiveChannel?
private(set) var liveChannel: LiveViewNativeCore.LiveChannel?
private weak var channel: LiveViewNativeCore.Channel?

@Published var document: LiveViewNativeCore.Document?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// FileImporterModifier.swift
//
//
// Created by Carson Katri on 10/23/24.
//

import SwiftUI
import LiveViewNativeCore
import LiveViewNativeStylesheet
import UniformTypeIdentifiers
import OSLog

private let logger = Logger(subsystem: "LiveViewNative", category: "_FileImporterModifier")

/// 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.
///
/// ### fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)
/// - `isPresented`: `attr("...")` (required)
/// - `allowedContentTypes`: `attr("...")` or list of ``UniformTypeIdentifiers/UTType`` (required)
/// - `allowsMultipleSelection`: `attr("...")` or ``Swift/Bool`` (required)
///
/// 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.
///
/// Example:
///
/// ```heex
/// <.live_file_input upload={@uploads.avatar} />
/// ```
@_documentation(visibility: public)
@ParseableExpression
struct _FileImporterModifier<R: RootRegistry>: ViewModifier {
static var name: String { "fileImporter" }

@Environment(\.formModel) private var formModel

private let id: AttributeReference<String>
private let name: AttributeReference<String>
@ChangeTracked private var isPresented: Bool
private let allowedContentTypes: AttributeReference<UTType.ResolvableSet>
private let allowsMultipleSelection: AttributeReference<Bool>

@ObservedElement private var element
@LiveContext<R> private var context

@available(iOS 14.0, macOS 11.0, visionOS 1.0, *)
init(
id: AttributeReference<String>,
name: AttributeReference<String>,
isPresented: ChangeTracked<Bool>,
allowedContentTypes: AttributeReference<UTType.ResolvableSet>,
allowsMultipleSelection: AttributeReference<Bool>
) {
self.id = id
self.name = name
self._isPresented = isPresented
self.allowedContentTypes = allowedContentTypes
self.allowsMultipleSelection = allowsMultipleSelection
}

func body(content: Content) -> some View {
#if os(iOS) || os(macOS) || os(visionOS)
content.fileImporter(
isPresented: $isPresented,
allowedContentTypes: allowedContentTypes.resolve(on: element, in: context).values,
allowsMultipleSelection: allowsMultipleSelection.resolve(on: element, in: context)
) { result in
let id = id.resolve(on: element, in: context)

guard let liveChannel = context.coordinator.liveChannel
else { return }

do {
let files = try result.get().map({ url in
LiveFile(
try Data(contentsOf: url),
url.pathExtension,
url.deletingPathExtension().lastPathComponent,
id
)
})
Task {
do {
for file in files {
try await liveChannel.validateUpload(file)
}
} catch {
logger.log(level: .error, "\(error.localizedDescription)")
}
}
self.formModel?.fileUploads.append(
contentsOf: files.map({ file in
{
try await liveChannel.uploadFile(file)
print("upload complete")
}
})
)
} catch {
logger.log(level: .error, "\(error.localizedDescription)")
}
}
#else
content
#endif
}
}

extension UTType: AttributeDecodable {
struct ResolvableSet: AttributeDecodable, ParseableModifierValue {
nonisolated let values: [UTType]

init(values: [UTType]) {
self.values = values
}

nonisolated init(from attribute: LiveViewNativeCore.Attribute?, on element: ElementNode) throws {
guard let value = attribute?.value
else { throw AttributeDecodingError.missingAttribute(Self.self) }
self.values = value.split(separator: ",").compactMap({ UTType(filenameExtension: String($0.dropFirst())) })
}

static func parser(in context: ParseableModifierContext) -> some Parser<Substring.UTF8View, Self> {
Array<String>.parser(in: context).compactMap({ Self.init(values: $0.compactMap(UTType.init)) })
}
}
}
8 changes: 8 additions & 0 deletions Sources/LiveViewNative/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
/// A publisher that emits a value before sending the form submission event.
var formWillSubmit = PassthroughSubject<(), Never>()

var fileUploads: [() async throws -> ()] = []

init(elementID: String) {
self.elementID = elementID
}

@_spi(LiveForm) public func updateFromElement(_ element: ElementNode, submitAction: @escaping () -> ()) {
self.fileUploads.removeAll()
let pushEventImpl = pushEventImpl!
self.changeEvent = element.attributeValue(for: .init(name: "phx-change")).flatMap({ event in
{ value in
Expand Down Expand Up @@ -95,6 +98,11 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
/// See ``LiveViewCoordinator/pushEvent(type:event:value:target:)`` for more information.
public func sendSubmitEvent() async throws {
formWillSubmit.send(())
for fileUpload in fileUploads {
print("Upload...")
try await fileUpload()
}
print("All uploads done")
if let submitEvent = submitEvent {
try await pushFormEvent(submitEvent)
} else if let submitAction {
Expand Down
Loading

0 comments on commit 337fa49

Please sign in to comment.