Embed an iOS and Catalyst share extension using existing discovery and player APIs. Share saved players through an App Group snapshot, preserve the queue, and verify advancing playback before reporting success. Cover URL provider transport, settings migration, discovery addresses, duplicate handling, cancellation, and partial enqueue/play outcomes with focused tests. Document authentication and signing limits.
140 lines
4.2 KiB
Swift
140 lines
4.2 KiB
Swift
//
|
|
// Settings.swift
|
|
// QueueCube
|
|
//
|
|
// Created by James Magahern on 6/10/25.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct Settings: Codable
|
|
{
|
|
var selectedServer: Server?
|
|
|
|
var configuredServers: [Server] {
|
|
willSet {
|
|
// Set selected server to whatever the first server is, if we're adding the first one.
|
|
if configuredServers.isEmpty && !newValue.isEmpty && selectedServer == nil {
|
|
selectedServer = newValue.first
|
|
}
|
|
|
|
// If the selected server is being removed, set it to something else
|
|
if !newValue.contains(where: { $0 == selectedServer }) {
|
|
selectedServer = newValue.first // nil if empty
|
|
}
|
|
}
|
|
}
|
|
|
|
var isConfigured: Bool { !configuredServers.isEmpty }
|
|
|
|
static func fromDefaults(defaults: UserDefaults = .standard,
|
|
sharedDefaults: UserDefaults? = UserDefaults(suiteName: appGroupIdentifier)) -> Settings {
|
|
let settings = Settings(
|
|
selectedServer: defaults[SelectedServerKey.self],
|
|
configuredServers: defaults[ConfiguredServersKey.self]
|
|
)
|
|
settings.publishForSharing(defaults: sharedDefaults)
|
|
return settings
|
|
}
|
|
|
|
func save(defaults: UserDefaults = .standard,
|
|
sharedDefaults: UserDefaults? = UserDefaults(suiteName: Self.appGroupIdentifier)) {
|
|
defaults[ConfiguredServersKey.self] = configuredServers
|
|
defaults[SelectedServerKey.self] = selectedServer
|
|
|
|
publishForSharing(defaults: sharedDefaults)
|
|
postSettingsChanged()
|
|
}
|
|
|
|
// The containing app remains the owner of settings. Publish one snapshot so
|
|
// the extension never observes a selected player from a different revision.
|
|
static let appGroupIdentifier = "group.net.buzzert.QueueCube"
|
|
private static let sharedSettingsKey = "shareSettings.v1"
|
|
|
|
static func fromSharedDefaults(defaults: UserDefaults? = UserDefaults(suiteName: appGroupIdentifier)) -> Settings {
|
|
guard let defaults,
|
|
let data = defaults.data(forKey: sharedSettingsKey),
|
|
let settings = try? PropertyListDecoder().decode(Settings.self, from: data)
|
|
else { return Settings(selectedServer: nil, configuredServers: []) }
|
|
return settings
|
|
}
|
|
|
|
private func publishForSharing(defaults: UserDefaults?) {
|
|
guard let defaults,
|
|
let data = try? PropertyListEncoder().encode(self) else { return }
|
|
defaults.set(data, forKey: Self.sharedSettingsKey)
|
|
}
|
|
|
|
func postSettingsChanged() {
|
|
NotificationCenter.default.post(name: .settingsChanged, object: nil)
|
|
}
|
|
|
|
// MARK: - Modifiers
|
|
|
|
func selectedServer(_ server: Server?) -> Self {
|
|
var copy = self
|
|
copy.selectedServer = server
|
|
return copy
|
|
}
|
|
|
|
func configuredServers(_ servers: [Server]) -> Self {
|
|
var copy = self
|
|
copy.configuredServers = servers
|
|
return copy
|
|
}
|
|
|
|
// MARK: - Types
|
|
|
|
enum Keys: String
|
|
{
|
|
case selectedServer
|
|
case configuredServers
|
|
}
|
|
|
|
fileprivate protocol Key
|
|
{
|
|
associatedtype Value: Codable
|
|
|
|
static var defaultValue: Value { get }
|
|
static var key: String { get }
|
|
}
|
|
|
|
private struct ConfiguredServersKey: Key {
|
|
static var defaultValue: [Server] { [] }
|
|
}
|
|
|
|
private struct SelectedServerKey: Key {
|
|
static var defaultValue: Server? { nil }
|
|
}
|
|
}
|
|
|
|
extension UserDefaults
|
|
{
|
|
fileprivate subscript<T: Settings.Key>(_ type: T.Type) -> T.Value {
|
|
get {
|
|
guard let data = data(forKey: type.key)
|
|
else { return type.defaultValue }
|
|
|
|
guard let value = try? PropertyListDecoder().decode(type.Value, from: data)
|
|
else { return type.defaultValue }
|
|
|
|
return value
|
|
}
|
|
|
|
set {
|
|
let data = try? PropertyListEncoder().encode(newValue)
|
|
set(data, forKey: type.key)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Settings.Key
|
|
{
|
|
static var key: String { Mirror(reflecting: Self.self).description }
|
|
}
|
|
|
|
extension Notification.Name
|
|
{
|
|
static let settingsChanged = Notification.Name("settingsChanged")
|
|
}
|