mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
5d05363f36
The app is one binary for iPhone and iPad, so what was built for the iPhone has to hold on an iPad too: - Widgets: the trip, today and bookings widgets offer the iPad's extra large size. The trip sets its countdown or its day beside the bookings or the day's stops, today sets the day, its progress and weather beside the timeline, bookings sets the next one as a card beside the rest. The other widgets stay at the sizes they were designed for. - The widget renderer draws every home-screen size of each widget as a 13" iPad, an 11" iPad and an iPad mini give it (a mini's small widget is smaller than an iPhone's), an iPad lock screen with the accessories and the travel day's Live Activity, and that Live Activity at the iPhone's and the iPad's widths. To draw it there, its lock-screen banner takes plain values (TripDayBanner, now beside the today widget). - Apple Wallet: an iPad cannot take a pass. The shell reports the wallet feature only where PKPassLibrary and PKAddPassesViewController can add one, so the UI shows no "Add to Apple Wallet" button there and hands a .pkpass to the share sheet like any other file. - Share extension: on an iPad it is presented over the full screen instead of in a form sheet, and in a window of regular width its card stands centred over the dimmed app, without the grabber, instead of rising from the bottom edge. A narrow Split View window keeps the iPhone's card.
129 lines
4.4 KiB
Swift
129 lines
4.4 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()
|
|
|
|
// On an iPad the system would put the extension in a form sheet, a box in the
|
|
// middle of the screen with this sheet's own dimming and card inside it. Over
|
|
// the full screen instead, the card stands centred over the app that shared,
|
|
// the way an iPad presents a small form.
|
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
|
presentOverFullScreenOnPad()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
presentOverFullScreenOnPad()
|
|
}
|
|
|
|
private func presentOverFullScreenOnPad() {
|
|
if UIDevice.current.userInterfaceIdiom == .pad { modalPresentationStyle = .overFullScreen }
|
|
}
|
|
|
|
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: "")
|
|
}
|