mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
7b8bd4b166
A share extension puts TREK in the share sheet of Mail, Safari, Files, Wallet and any app that shares a document, a link, text or a photo. It shows what came in (a Wallet pass, a PDF, a mail, the page as Safari rendered it, text, a photo) and the user's trips with their covers, the next one already picked, and hands both to the app, which opens that trip's booking import with the file in place. "Choose later" leaves the trip question to the app's import sheet as before. - ShareExtension target (add-extension.rb learns "share": JavaScript and the localizations go in as resources), bundle com.liketrek.trek.share, signed in the release lane, its plists linted in native.yml - Safari hands over the rendered page (SharePage.js), so a confirmation behind a sign-in arrives whole; other apps' links are fetched - The item travels through the shared keychain group the widgets already use; the app takes it on com.liketrek.trek://share or when it next becomes active - Feature shareSheet; strings in the app's 28 languages
111 lines
3.7 KiB
Swift
111 lines
3.7 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
/// The share sheet's entry point ("TREK" in the share sheet of Mail, Safari,
|
|
/// Files, Wallet and any app that shares a document or a link). It reads what
|
|
/// was shared, lets the user pick the trip it belongs to, leaves both for the
|
|
/// app (ShareHandoff) and opens TREK, where the booking import takes over.
|
|
@objc(ShareViewController)
|
|
final class ShareViewController: UIViewController {
|
|
private let model = ShareModel()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .clear
|
|
|
|
model.onCancel = { [weak self] in
|
|
self?.extensionContext?.cancelRequest(withError: NSError(domain: "com.liketrek.trek.share", code: 0))
|
|
}
|
|
model.onDone = { [weak self] in
|
|
self?.openApp()
|
|
self?.extensionContext?.completeRequest(returningItems: nil)
|
|
}
|
|
|
|
let host = UIHostingController(rootView: ShareView(model: model))
|
|
host.view.backgroundColor = .clear
|
|
addChild(host)
|
|
host.view.frame = view.bounds
|
|
host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.addSubview(host.view)
|
|
host.didMove(toParent: self)
|
|
|
|
let items = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
|
|
Task { await model.load(items) }
|
|
}
|
|
|
|
/// Extensions cannot reach UIApplication.shared; the application is still
|
|
/// up the responder chain, and opening our own scheme brings TREK forward.
|
|
/// Should that ever fail, the app picks the item up the next time it opens.
|
|
private func openApp() {
|
|
guard let url = URL(string: "com.liketrek.trek://share") else { return }
|
|
var responder: UIResponder? = self
|
|
while let current = responder {
|
|
if let application = current as? UIApplication {
|
|
application.open(url, options: [:], completionHandler: nil)
|
|
return
|
|
}
|
|
responder = current.next
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class ShareModel: ObservableObject {
|
|
enum Phase: Equatable {
|
|
case loading
|
|
case ready
|
|
case saving
|
|
case done
|
|
case failed(String)
|
|
}
|
|
|
|
@Published var phase: Phase = .loading
|
|
@Published var item: ShareItem?
|
|
@Published var trips: [ShareHandoff.Trip] = []
|
|
/// nil: the app asks which trip once it is open.
|
|
@Published var selectedTrip: Int?
|
|
|
|
var onCancel: () -> Void = {}
|
|
var onDone: () -> Void = {}
|
|
|
|
func load(_ items: [NSExtensionItem]) async {
|
|
trips = ShareHandoff.trips()
|
|
selectedTrip = trips.first?.id
|
|
do {
|
|
item = try await ShareLoader.load(items)
|
|
phase = .ready
|
|
} catch ShareLoadError.tooLarge {
|
|
phase = .failed(L("share.tooLarge"))
|
|
} catch {
|
|
phase = .failed(L("share.unsupported"))
|
|
}
|
|
}
|
|
|
|
func add() {
|
|
guard let item, phase == .ready else { return }
|
|
phase = .saving
|
|
let haptics = UINotificationFeedbackGenerator()
|
|
haptics.prepare()
|
|
Task {
|
|
guard ShareHandoff.store(item, tripId: selectedTrip) else {
|
|
haptics.notificationOccurred(.error)
|
|
phase = .failed(L("share.failed"))
|
|
return
|
|
}
|
|
haptics.notificationOccurred(.success)
|
|
withAnimation(.spring(response: 0.42, dampingFraction: 0.72)) { phase = .done }
|
|
try? await Task.sleep(nanoseconds: 900_000_000)
|
|
onDone()
|
|
}
|
|
}
|
|
|
|
var selectedTitle: String? {
|
|
trips.first { $0.id == selectedTrip }?.title
|
|
}
|
|
}
|
|
|
|
/// The extension's own strings (ShareExtension/<lang>.lproj/Localizable.strings).
|
|
func L(_ key: String) -> String {
|
|
NSLocalizedString(key, comment: "")
|
|
}
|