Files
Termind/TFI/Platform/MobileSFTPOpenWithPresenter.swift
T

103 lines
3.3 KiB
Swift

import SwiftUI
import UIKit
/// The workspace owns presentation intent; the edit service owns the local file.
@MainActor
final class MobileSFTPOpenWithPresenter: NSObject, UIDocumentInteractionControllerDelegate {
weak var sourceView: UIView?
var failed: (String) -> Void = { _ in }
var finished: () -> Void = {}
var isEnabled = false {
didSet { if !isEnabled { invalidate() } }
}
private var requestID: UUID?
private(set) var controller: UIDocumentInteractionController?
private var sending = false
private var handoffRetention: MobileSFTPOpenWithPresenter?
private let present: @MainActor (UIDocumentInteractionController, UIView) -> Bool
init(
present: @escaping @MainActor (UIDocumentInteractionController, UIView) -> Bool = { controller, view in
controller.presentOpenInMenu(
from: CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 1, height: 1),
in: view, animated: true)
}
) {
self.present = present
}
func begin() -> UUID? {
guard isEnabled, !sending else { return nil }
invalidate()
let id = UUID()
requestID = id
return id
}
func downloaded(_ url: URL, request: UUID) {
guard requestID == request else { return }
requestID = nil
guard isEnabled, let sourceView, sourceView.window != nil else { return }
let controller = UIDocumentInteractionController(url: url)
controller.delegate = self
self.controller = controller
if !present(controller, sourceView) {
self.controller = nil
failed(String(localized: "No installed app can open this file."))
}
}
func invalidate() {
requestID = nil
// Switching to the receiving app can deactivate this scene before handoff finishes.
guard !sending else { return }
let previous = controller
controller = nil
previous?.dismissMenu(animated: false)
}
func documentInteractionController(
_ controller: UIDocumentInteractionController, willBeginSendingToApplication application: String?
) {
guard self.controller === controller else { return }
sending = true
handoffRetention = self
}
func documentInteractionControllerDidDismissOpenInMenu(_ controller: UIDocumentInteractionController) {
guard self.controller === controller, !sending else { return }
self.controller = nil
finished()
}
func documentInteractionController(
_ controller: UIDocumentInteractionController, didEndSendingToApplication application: String?
) {
guard self.controller === controller else { return }
sending = false
self.controller = nil
finished()
handoffRetention = nil
}
}
struct MobileSFTPOpenWithAnchor: UIViewRepresentable {
let presenter: MobileSFTPOpenWithPresenter
let isEnabled: Bool
let failed: (String) -> Void
let finished: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.isUserInteractionEnabled = false
return view
}
func updateUIView(_ view: UIView, context: Context) {
presenter.sourceView = view
presenter.failed = failed
presenter.finished = finished
presenter.isEnabled = isEnabled
}
}