241 lines
8.3 KiB
Swift
241 lines
8.3 KiB
Swift
import AppIntents
|
|
import Shared
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
struct WidgetBasicContainerView: View {
|
|
@Environment(\.widgetFamily) var family: WidgetFamily
|
|
|
|
let emptyViewGenerator: () -> AnyView
|
|
let contents: [WidgetBasicViewModel]
|
|
let type: WidgetType
|
|
|
|
init(emptyViewGenerator: @escaping () -> AnyView, contents: [WidgetBasicViewModel], type: WidgetType) {
|
|
self.emptyViewGenerator = emptyViewGenerator
|
|
self.contents = contents
|
|
self.type = type
|
|
|
|
// Use the opportunity of widget refresh to also refresh control center controls
|
|
// since those controls dont have a refresh interval
|
|
DataWidgetsUpdater.updateControlCenterControls()
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if contents.isEmpty {
|
|
emptyViewGenerator()
|
|
} else {
|
|
content(for: contents)
|
|
}
|
|
}
|
|
// Whenever Apple allow apps to use material backgrounds we should update this
|
|
.widgetBackground(Color.asset(Asset.Colors.primaryBackground))
|
|
}
|
|
|
|
@available(iOS 16.4, *)
|
|
private func intent(for model: WidgetBasicViewModel) -> (any AppIntent)? {
|
|
switch model.interactionType {
|
|
case .widgetURL:
|
|
return nil
|
|
case let .appIntent(widgetIntentType):
|
|
switch widgetIntentType {
|
|
case .action:
|
|
let intent = PerformAction()
|
|
intent.action = IntentActionAppEntity(id: model.id, displayString: model.title)
|
|
intent.hapticConfirmation = true
|
|
return intent
|
|
case let .script(id, entityId, serverId, name, showConfirmationNotification):
|
|
let intent = ScriptAppIntent()
|
|
intent.script = .init(
|
|
id: id,
|
|
entityId: entityId,
|
|
serverId: serverId,
|
|
serverName: "", // not used in this context
|
|
displayString: name,
|
|
iconName: "" // not used in this context
|
|
)
|
|
intent.hapticConfirmation = true
|
|
intent.showConfirmationNotification = showConfirmationNotification
|
|
return intent
|
|
case .refresh:
|
|
return ReloadWidgetsAppIntent()
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
func content(for models: [WidgetBasicViewModel]) -> some View {
|
|
let actionCount = models.count
|
|
let columnCount = Self.columnCount(family: family, modelCount: actionCount)
|
|
let rows = Array(columnify(count: columnCount, models: models))
|
|
|
|
let sizeStyle: WidgetBasicSizeStyle = {
|
|
if models.count == 1 {
|
|
return .single
|
|
}
|
|
|
|
let compactBp = Self.compactSizeBreakpoint(for: family)
|
|
|
|
let condensed = compactBp < actionCount
|
|
let compactRowCount = compactBp / Self.columnCount(family: family, modelCount: compactBp)
|
|
|
|
if condensed {
|
|
return .condensed
|
|
} else if rows.count < compactRowCount {
|
|
return .expanded
|
|
} else {
|
|
return .regular
|
|
}
|
|
}()
|
|
|
|
basicView(rows: rows, sizeStyle: sizeStyle)
|
|
}
|
|
|
|
private func basicView(rows: [[WidgetBasicViewModel]], sizeStyle: WidgetBasicSizeStyle) -> some View {
|
|
VStack(alignment: .leading, spacing: Spaces.one) {
|
|
ForEach(rows, id: \.self) { column in
|
|
HStack(spacing: Spaces.one) {
|
|
ForEach(column) { model in
|
|
if case let .widgetURL(url) = model.interactionType {
|
|
Link(destination: url.withWidgetAuthenticity()) {
|
|
if #available(iOS 18.0, *) {
|
|
tintedWrapperView(model: model, sizeStyle: sizeStyle)
|
|
} else {
|
|
normalView(model: model, sizeStyle: sizeStyle)
|
|
}
|
|
}
|
|
} else {
|
|
if #available(iOS 17.0, *), let intent = intent(for: model) {
|
|
Button(intent: intent) {
|
|
tintedWrapperView(model: model, sizeStyle: sizeStyle)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(sizeStyle == .single ? 0 : Spaces.one)
|
|
}
|
|
|
|
private func normalView(model: WidgetBasicViewModel, sizeStyle: WidgetBasicSizeStyle) -> some View {
|
|
switch type {
|
|
case .button:
|
|
return AnyView(WidgetBasicButtonView(
|
|
model: model,
|
|
sizeStyle: sizeStyle,
|
|
tinted: false
|
|
))
|
|
case .sensor:
|
|
return AnyView(WidgetBasicSensorView(
|
|
model: model,
|
|
sizeStyle: sizeStyle,
|
|
tinted: false
|
|
))
|
|
}
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
private func tintedWrapperView(model: WidgetBasicViewModel, sizeStyle: WidgetBasicSizeStyle) -> some View {
|
|
switch type {
|
|
case .button:
|
|
return AnyView(WidgetBasicViewTintedWrapper(
|
|
model: model,
|
|
sizeStyle: sizeStyle,
|
|
viewType: WidgetBasicButtonView.self
|
|
))
|
|
case .sensor:
|
|
return AnyView(WidgetBasicViewTintedWrapper(
|
|
model: model,
|
|
sizeStyle: sizeStyle,
|
|
viewType: WidgetBasicSensorView.self
|
|
))
|
|
}
|
|
}
|
|
|
|
private func columnify(count: Int, models: [WidgetBasicViewModel]) -> AnyIterator<[WidgetBasicViewModel]> {
|
|
var perActionIterator = models.makeIterator()
|
|
return AnyIterator { () -> [WidgetBasicViewModel]? in
|
|
let column = stride(from: 0, to: count, by: 1)
|
|
.compactMap { _ in perActionIterator.next() }
|
|
return column.isEmpty == false ? column : nil
|
|
}
|
|
}
|
|
|
|
static func columnCount(family: WidgetFamily, modelCount: Int) -> Int {
|
|
switch family {
|
|
#if !targetEnvironment(macCatalyst) // no ventura SDK yet
|
|
case .accessoryCircular, .accessoryInline, .accessoryRectangular: return 1
|
|
#endif
|
|
case .systemSmall: return 1
|
|
case .systemMedium: return 2
|
|
case .systemLarge:
|
|
if modelCount <= 2 {
|
|
// 2 'landscape' actions looks better than 2 'portrait'
|
|
return 1
|
|
} else {
|
|
return 2
|
|
}
|
|
case .systemExtraLarge:
|
|
if modelCount <= 4 {
|
|
return 1
|
|
} else if modelCount <= 15 {
|
|
// note this is 15 and not 16 - divisibility by 3 here
|
|
return 3
|
|
} else {
|
|
return 4
|
|
}
|
|
@unknown default: return 2
|
|
}
|
|
}
|
|
|
|
/// More than this number: show compact (icon left, text right) version
|
|
static func compactSizeBreakpoint(for family: WidgetFamily) -> Int {
|
|
switch family {
|
|
#if !targetEnvironment(macCatalyst) // no ventura SDK yet
|
|
case .accessoryCircular,
|
|
.accessoryInline,
|
|
.accessoryRectangular:
|
|
return 1
|
|
#endif
|
|
case .systemSmall: return 2
|
|
case .systemMedium: return 4
|
|
case .systemLarge: return 10
|
|
case .systemExtraLarge: return 20
|
|
@unknown default: return 8
|
|
}
|
|
}
|
|
|
|
static func maximumCount(family: WidgetFamily) -> Int {
|
|
switch family {
|
|
#if !targetEnvironment(macCatalyst) // no ventura SDK yet
|
|
case .accessoryCircular,
|
|
.accessoryInline,
|
|
.accessoryRectangular:
|
|
return 1
|
|
#endif
|
|
case .systemSmall: return 2
|
|
case .systemMedium: return 4
|
|
case .systemLarge: return 10
|
|
case .systemExtraLarge: return 20
|
|
@unknown default: return 4
|
|
}
|
|
}
|
|
|
|
// This is all widgets that are on the lock screen
|
|
// Lock screen widgets are transparent and don't need a colored background
|
|
private static var transparentFamilies: [WidgetFamily] {
|
|
if #available(iOS 16.0, *) {
|
|
[.accessoryCircular, .accessoryRectangular]
|
|
} else {
|
|
[]
|
|
}
|
|
}
|
|
|
|
enum WidgetType: String {
|
|
case button
|
|
case sensor
|
|
}
|
|
}
|