Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ComputerSolitaire.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
4B75C1E02F1A233500761A12 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = QueenOfHeartsAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
Expand Down Expand Up @@ -267,6 +268,7 @@
4B75C1E12F1A233500761A12 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = QueenOfHeartsAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions ComputerSolitaire/AppIcons/QueenOfHeartsAppIcon.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"fill" : {
"automatic-gradient" : "display-p3:0.45728,0.63330,0.83252,1.00000",
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 1
},
"stop" : {
"x" : 0.5,
"y" : 0.3
}
}
},
"groups" : [
{
"layers" : [
{
"glass" : false,
"image-name" : "computer-solitaire.png",
"name" : "computer-solitaire",
"position" : {
"scale" : 0.6,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "default.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "queen-of-hearts.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
158 changes: 158 additions & 0 deletions ComputerSolitaire/Views/AppIconPickerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#if os(iOS)
import SwiftUI
import UIKit

/// To add a new alternate icon:
/// 1. Add its Icon Composer package to `AppIcons/`.
/// 2. Append the icon name to `ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES` in build settings.
/// 3. Add a flattened preview export to `Assets.xcassets/AppIconPreviews/` and declare it below.
struct AppIcon: Identifiable, Equatable {
/// The name passed to `setAlternateIconName(_:)`; `nil` selects the primary icon.
let alternateIconName: String?
let name: String
let previewImageName: String

var id: String { previewImageName }

static let `default` = AppIcon(
alternateIconName: nil,
name: "Default",
previewImageName: "AppIconPreviewDefault"
)

static let queenOfHearts = AppIcon(
alternateIconName: "QueenOfHeartsAppIcon",
name: "Queen of Hearts",
previewImageName: "AppIconPreviewQueenOfHearts"
)

static let all: [AppIcon] = [.default, .queenOfHearts]

static func current() -> AppIcon {
let activeName = UIApplication.shared.alternateIconName
return all.first { $0.alternateIconName == activeName } ?? .default
}
}

struct AppIconPreviewView: View {
let icon: AppIcon
var size: CGFloat = 56

private var shape: RoundedRectangle {
// Matches the home screen icon corner radius ratio.
RoundedRectangle(cornerRadius: size * 0.2237, style: .continuous)
}

var body: some View {
Image(icon.previewImageName)
.resizable()
.scaledToFill()
.frame(width: size, height: size)
.clipShape(shape)
.overlay {
shape.stroke(Color.primary.opacity(0.12), lineWidth: 0.5)
}
}
}

struct AppIconPickerView: View {
@Environment(\.dismiss) private var dismiss
@Binding var selection: AppIcon

private let columns = [GridItem(.adaptive(minimum: 130), spacing: 12)]

var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(AppIcon.all) { icon in
iconTile(icon)
}
}
.padding(24)
}
.navigationTitle("App Icon")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
.keyboardShortcut(.cancelAction)
}
}
}

private func iconTile(_ icon: AppIcon) -> some View {
let isSelected = selection == icon

return Button {
guard !isSelected else { return }
HapticManager.shared.play(.settingsSelection)
let previous = selection
withAnimation(.smooth(duration: 0.3)) {
selection = icon
}
UIApplication.shared.setAlternateIconName(icon.alternateIconName) { error in
guard error != nil else { return }
Task { @MainActor in
withAnimation(.smooth(duration: 0.3)) {
selection = previous
}
}
}
} label: {
VStack(spacing: 6) {
AppIconPreviewView(icon: icon)

Text(icon.name)
.font(.caption.weight(.bold))
}
.padding(.vertical, 12)
.padding(.horizontal, 10)
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(isSelected ? .thickMaterial : .thinMaterial)
.shadow(
color: .black.opacity(isSelected ? 0.12 : 0.04),
radius: isSelected ? 8 : 2,
y: isSelected ? 4 : 1
)
}
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(
Color.accentColor.opacity(isSelected ? 1 : 0),
lineWidth: 2.5
)
}
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(
Color.primary.opacity(isSelected ? 0 : 0.1),
lineWidth: 1
)
}
.overlay(alignment: .topTrailing) {
if isSelected {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18, weight: .semibold))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, Color.accentColor)
.padding(6)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("\(icon.name) app icon")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}

#Preview {
NavigationStack {
AppIconPickerView(selection: .constant(.default))
}
}
#endif
37 changes: 37 additions & 0 deletions ComputerSolitaire/Views/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Foundation
import SwiftUI
#if os(iOS)
import UIKit
#endif

enum TableBackgroundColor: String, CaseIterable, Identifiable {
case ocean = "#0671b7"
Expand Down Expand Up @@ -84,6 +87,10 @@ struct SettingsView: View {
@AppStorage(SettingsKey.soundEffectsEnabled) private var isSoundEffectsEnabled = true
@AppStorage(SettingsKey.showHintButton) private var isHintButtonVisible = true
@AppStorage(SettingsKey.cardStyle) private var cardStyleRawValue = CardStyle.classic.rawValue
#if os(iOS)
@State private var selectedAppIcon = AppIcon.current()
@State private var isShowingAppIconPicker = false
#endif

var body: some View {
ScrollView {
Expand Down Expand Up @@ -197,6 +204,28 @@ struct SettingsView: View {
}
}

#if os(iOS)
if UIApplication.shared.supportsAlternateIcons {
SettingsCard(title: "App Icon") {
Button {
isShowingAppIconPicker = true
} label: {
HStack(spacing: 10) {
AppIconPreviewView(icon: selectedAppIcon, size: 30)
Text(selectedAppIcon.name)
.font(.subheadline.weight(.semibold))
Spacer()
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
#endif

SettingsCard(title: "Help") {
Button {
isShowingRulesAndScoring = true
Expand Down Expand Up @@ -234,6 +263,14 @@ struct SettingsView: View {
RulesAndScoringView()
}
}
#if os(iOS)
.sheet(isPresented: $isShowingAppIconPicker) {
NavigationStack {
AppIconPickerView(selection: $selectedAppIcon)
}
.presentationDetents([.medium, .large])
}
#endif
.onChange(of: drawModeRawValue) { oldValue, newValue in
guard oldValue != newValue else { return }
HapticManager.shared.play(.settingsSelection)
Expand Down
Loading