macOS: die App läuft jetzt auf dem Mac
Und dein Argument dafür war das beste: auf dem Mac kann ich selbst hinsehen. Kein Simulator-Tunnel, der mitten im Prüfen abstürzt — `screencapture` auf das Fenster der App, und zwar nur auf ihres, nicht auf den Bildschirm. **Ein eigenes Ziel, nicht ein gemeinsames.** Die Info.plist der iOS-App ist voller iOS-Angaben: Startbildschirm, erlaubte Ausrichtungen, Live Activities. Die alle bedingt zu machen hieße, dass jede spätere Änderung an der iOS-Seite über eine Stelle geht, die auch den Mac betrifft. Zwei Ziele sind ein paar Zeilen mehr und eine Sorge weniger. **Was den Unterschied ausmacht, steht an einer Stelle.** Ein neuer `PlatformShims` in FlixyDesign übersetzt, was auf beiden Seiten verschieden heißt: schmale Titelzeile, halbhohes Blatt, Eingabefelder ohne Großschreibung, die Plätze in der Werkzeugleiste, die Zwischenablage. Zwanzig Ansichten benutzen ihn, statt zwanzigmal `#if os(iOS)` zu tragen — sonst wandert die Fallunterscheidung in jede neue Datei und wird beim übernächsten Bildschirm vergessen. Wirklich verschieden sind nur fünf Stellen: die Bildfläche (`UIView` gegen `NSView`), der AirPlay-Knopf, die Ausrichtungssperre (auf dem Mac gibt es nichts zu sperren), der App-Delegat für Hintergrund-Downloads und das Anmeldefenster, das ein anderes Fenster als Anker braucht. **Das Karussell blättert jetzt mit einem einrastenden Streifen** statt mit `TabView(.page)` — den Seitenstil gibt es auf dem Mac nicht. Das ist nebenbei die bessere Lösung: das verschachtelte TabView hatte die Eigenschaft, beim Wechsel kurz zwei Seiten gleichzeitig zu zeigen. **Was auf dem Mac fehlt und warum:** VLC. Das Binärpaket ist auf iOS festgelegt; sein xcframework enthält zwar Mac-Teile, aber das ist eine eigene Runde. Solange fällt die Wiedergabe dort auf den Systemplayer zurück — MKV und DTS gehen also noch nicht. Live Activities gibt es auf dem Mac nicht, Bild-in- Bild startet von Hand statt beim Verlassen der App. Gesehen: die App startet, der Anmeldebildschirm steht, beide Wege sind da. Angemeldet habe ich mich auf dem Mac **nicht** — das ginge nur, indem ich einen Token in deinen Schlüsselbund schreibe, und dann fragt macOS dich beim ersten Zugriff nach dem Passwort. Das machst du mit einem Klick selbst. iOS baut unverändert, 263 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
21c5a8b46b
commit
d54954b9c2
@@ -1,7 +1,24 @@
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import FlixyPlayback
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(FlixyVLC)
|
||||
import FlixyVLC
|
||||
#endif
|
||||
|
||||
/// Die Ansichtsklasse der jeweiligen Plattform.
|
||||
///
|
||||
/// UIKit und AppKit heißen verschieden und meinen dasselbe. Ein Name dafür
|
||||
/// spart in dieser Datei ein Dutzend `#if`.
|
||||
#if canImport(UIKit)
|
||||
typealias PlatformView = UIView
|
||||
#else
|
||||
typealias PlatformView = NSView
|
||||
#endif
|
||||
|
||||
/// Die Bildfläche des laufendie Engine.
|
||||
///
|
||||
@@ -22,14 +39,21 @@ struct VideoSurface: View {
|
||||
let surface: PlayerLayerView
|
||||
|
||||
var body: some View {
|
||||
switch engine {
|
||||
case is AVPlayerEngine:
|
||||
UIViewContainer(view: surface)
|
||||
case let vlcEngine as VLCPlayerEngine:
|
||||
UIViewContainer(view: vlcEngine.videoView)
|
||||
default:
|
||||
#if canImport(FlixyVLC)
|
||||
if let vlcEngine = engine as? VLCPlayerEngine {
|
||||
SurfaceContainer(view: vlcEngine.videoView)
|
||||
} else if engine is AVPlayerEngine {
|
||||
SurfaceContainer(view: surface)
|
||||
} else {
|
||||
Color.black
|
||||
}
|
||||
#else
|
||||
if engine is AVPlayerEngine {
|
||||
SurfaceContainer(view: surface)
|
||||
} else {
|
||||
Color.black
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +62,31 @@ struct VideoSurface: View {
|
||||
/// Der übliche Weg: eine Ebene von Hand als Unterebene einzuhängen bedeutet,
|
||||
/// ihre Größe bei jeder Drehung selbst nachzuführen. Als Hauptebene macht das
|
||||
/// UIKit.
|
||||
#if canImport(UIKit)
|
||||
final class PlayerLayerView: UIView {
|
||||
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
||||
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
||||
}
|
||||
#else
|
||||
/// Auf dem Mac gibt es kein `layerClass`: eine `NSView` bekommt ihre Ebene
|
||||
/// zugewiesen, statt ihre Art vorzugeben.
|
||||
final class PlayerLayerView: NSView {
|
||||
let playerLayer = AVPlayerLayer()
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
wantsLayer = true
|
||||
layer = playerLayer
|
||||
// Ohne das bleibt die Ebene beim Vergrößern des Fensters stehen, wo
|
||||
// sie war, und das Bild sitzt in der Ecke.
|
||||
layer?.needsDisplayOnBoundsChange = true
|
||||
autoresizingMask = [.width, .height]
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("nicht aus einem Storyboard") }
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Hängt eine bestehende `UIView` in den SwiftUI-Baum.
|
||||
///
|
||||
@@ -58,25 +103,57 @@ final class PlayerLayerView: UIView {
|
||||
///
|
||||
/// Mit eigener Hülle gehört jeder Ansicht ihre eigene, und `updateUIView` holt
|
||||
/// die Fläche zurück, falls sie zwischenzeitlich woanders eingehängt wurde.
|
||||
private struct UIViewContainer: UIViewRepresentable {
|
||||
let view: UIView
|
||||
#if canImport(UIKit)
|
||||
private struct SurfaceContainer: UIViewRepresentable {
|
||||
let view: PlatformView
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
func makeUIView(context: Context) -> PlatformView {
|
||||
let host = UIView()
|
||||
host.backgroundColor = .black
|
||||
adopt(into: host)
|
||||
adopt(view, into: host)
|
||||
return host
|
||||
}
|
||||
|
||||
func updateUIView(_ host: UIView, context: Context) {
|
||||
func updateUIView(_ host: PlatformView, context: Context) {
|
||||
guard view.superview !== host else { return }
|
||||
adopt(into: host)
|
||||
}
|
||||
|
||||
private func adopt(into host: UIView) {
|
||||
view.translatesAutoresizingMaskIntoConstraints = true
|
||||
view.frame = host.bounds
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
host.addSubview(view)
|
||||
adopt(view, into: host)
|
||||
}
|
||||
}
|
||||
#else
|
||||
private struct SurfaceContainer: NSViewRepresentable {
|
||||
let view: PlatformView
|
||||
|
||||
func makeNSView(context: Context) -> PlatformView {
|
||||
let host = NSView()
|
||||
host.wantsLayer = true
|
||||
host.layer?.backgroundColor = NSColor.black.cgColor
|
||||
adopt(view, into: host)
|
||||
return host
|
||||
}
|
||||
|
||||
func updateNSView(_ host: PlatformView, context: Context) {
|
||||
guard view.superview !== host else { return }
|
||||
adopt(view, into: host)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Hängt die Fläche in ihre Hülle.
|
||||
///
|
||||
/// Sie wird **nicht** unmittelbar an SwiftUI zurückgegeben, sondern in eine
|
||||
/// eigene Hülle gehängt. Der Unterschied zählt genau in dem Moment, in dem es
|
||||
/// Kachel und Vollbild kurz gleichzeitig gibt: eine Ansicht hat nur einen
|
||||
/// Vorgesetzten, und wer sie zuletzt bekommt, gewinnt. Gab SwiftUI sie
|
||||
/// unmittelbar heraus, nahm das Abräumen der einen der anderen das Bild weg.
|
||||
private func adopt(_ view: PlatformView, into host: PlatformView) {
|
||||
#if canImport(UIKit)
|
||||
view.translatesAutoresizingMaskIntoConstraints = true
|
||||
view.frame = host.bounds
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
#else
|
||||
view.translatesAutoresizingMaskIntoConstraints = true
|
||||
view.frame = host.bounds
|
||||
view.autoresizingMask = [.width, .height]
|
||||
#endif
|
||||
host.addSubview(view)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user