Skip to content

Commit 528a7d6

Browse files
authored
Изменение иконки приложения (#15)
* В процессе * Доработал тесты - Убрал лишний вариант теста, который ломал тесты - Добавил новые тесты * Рефактор
1 parent ad708e8 commit 528a7d6

File tree

70 files changed

+1028
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1028
-25
lines changed

SwiftUI-Days.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,10 @@
503503
672050522BA9F8C7005B00E7 /* Debug */ = {
504504
isa = XCBuildConfiguration;
505505
buildSettings = {
506-
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
506+
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon2 AppIcon3 AppIcon4 AppIcon5 AppIcon6 AppIcon7";
507+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon1;
507508
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
509+
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
508510
CODE_SIGN_ENTITLEMENTS = "SwiftUI-Days/SupportingFiles/SwiftUI_Days.entitlements";
509511
CODE_SIGN_STYLE = Automatic;
510512
CURRENT_PROJECT_VERSION = 1;
@@ -532,7 +534,7 @@
532534
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
533535
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
534536
MACOSX_DEPLOYMENT_TARGET = 14.0;
535-
MARKETING_VERSION = 1.6.1;
537+
MARKETING_VERSION = 1.7;
536538
PRODUCT_BUNDLE_IDENTIFIER = "com.oleg991.SwiftUI-Days";
537539
PRODUCT_NAME = "$(TARGET_NAME)";
538540
SDKROOT = auto;
@@ -549,8 +551,10 @@
549551
672050532BA9F8C7005B00E7 /* Release */ = {
550552
isa = XCBuildConfiguration;
551553
buildSettings = {
552-
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
554+
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon2 AppIcon3 AppIcon4 AppIcon5 AppIcon6 AppIcon7";
555+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon1;
553556
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
557+
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
554558
CODE_SIGN_ENTITLEMENTS = "SwiftUI-Days/SupportingFiles/SwiftUI_Days.entitlements";
555559
CODE_SIGN_STYLE = Automatic;
556560
CURRENT_PROJECT_VERSION = 1;
@@ -578,7 +582,7 @@
578582
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
579583
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
580584
MACOSX_DEPLOYMENT_TARGET = 14.0;
581-
MARKETING_VERSION = 1.6.1;
585+
MARKETING_VERSION = 1.7;
582586
PRODUCT_BUNDLE_IDENTIFIER = "com.oleg991.SwiftUI-Days";
583587
PRODUCT_NAME = "$(TARGET_NAME)";
584588
SDKROOT = auto;

SwiftUI-Days/Models/Item.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ final class Item {
3838
let yearsCount = components.year ?? 0
3939
let monthsCount = components.month ?? 0
4040
let daysCount = components.day ?? 0
41-
let todayString = NSLocalizedString("Today", comment: "Today")
41+
let todayString = String(localized: "Today")
4242
guard yearsCount != 0 || monthsCount != 0 || daysCount != 0 else {
4343
return todayString
4444
}

SwiftUI-Days/Screens/More/MoreScreen.swift

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct MoreScreen: View {
1212
Spacer().containerRelativeFrame([.vertical])
1313
VStack(spacing: 16) {
1414
Group {
15-
appThemePicker
15+
appThemeIconButton
1616
appDataButton
1717
feedbackButton
1818
rateAppButton
@@ -30,24 +30,9 @@ struct MoreScreen: View {
3030
}
3131
}
3232

33-
private var appThemePicker: some View {
34-
Menu {
35-
Picker(
36-
"App theme",
37-
selection: .init(
38-
get: { appSettings.appTheme },
39-
set: { appSettings.appTheme = $0 }
40-
)
41-
) {
42-
ForEach(AppTheme.allCases) {
43-
Text($0.title).tag($0)
44-
}
45-
}
46-
} label: {
47-
Text("App theme")
48-
}
49-
.accessibilityIdentifier("appThemeButton")
50-
.accessibilityValue(Text(appSettings.appTheme.title))
33+
private var appThemeIconButton: some View {
34+
NavigationLink("App theme and Icon", destination: ThemeIconScreen())
35+
.accessibilityIdentifier("appThemeIconButton")
5136
}
5237

5338
private var appDataButton: some View {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
extension ThemeIconScreen {
5+
enum IconVariant: String, CaseIterable {
6+
case primary = "AppIcon1"
7+
case one = "AppIcon2"
8+
case two = "AppIcon3"
9+
case three = "AppIcon4"
10+
case four = "AppIcon5"
11+
case five = "AppIcon6"
12+
case six = "AppIcon7"
13+
14+
/// Название альтернативной иконки, для дефолтной иконки всегда `nil`
15+
var alternateName: String? {
16+
switch self {
17+
case .primary: nil
18+
default: rawValue
19+
}
20+
}
21+
22+
/// Уменьшенная картинка (обычный ассет) для отображения в списке
23+
var listImage: Image {
24+
Image("\(rawValue)Small")
25+
}
26+
27+
@MainActor
28+
var isSelected: Bool {
29+
alternateName == UIApplication.shared.alternateIconName
30+
}
31+
32+
@MainActor
33+
var accessibilityLabel: String {
34+
let isSelectedText = isSelected ? String(localized: "Selected") : String(localized: "Not selected")
35+
switch self {
36+
case .primary:
37+
return String(localized: "Primary icon") + ", " + isSelectedText
38+
default:
39+
return String(format: String(localized: "Variant %lld"), variantNumber) + ", " + isSelectedText
40+
}
41+
}
42+
43+
init(name: String?) {
44+
self = IconVariant(rawValue: name ?? "") ?? .primary
45+
}
46+
47+
private var variantNumber: Int {
48+
switch self {
49+
case .primary: 1
50+
case .one: 2
51+
case .two: 3
52+
case .three: 4
53+
case .four: 5
54+
case .five: 6
55+
case .six: 7
56+
}
57+
}
58+
}
59+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
import Observation
3+
import OSLog
4+
import UIKit.UIApplication
5+
6+
extension ThemeIconScreen {
7+
@Observable @MainActor
8+
final class IconViewModel {
9+
private let logger = Logger(
10+
subsystem: Bundle.main.bundleIdentifier!,
11+
category: String(describing: IconViewModel.self)
12+
)
13+
private(set) var currentAppIcon: IconVariant
14+
15+
init() {
16+
if let currentIconName = UIApplication.shared.alternateIconName {
17+
currentAppIcon = IconVariant(name: currentIconName)
18+
} else {
19+
currentAppIcon = .primary
20+
}
21+
}
22+
23+
func setIcon(_ icon: IconVariant) async {
24+
do {
25+
guard UIApplication.shared.supportsAlternateIcons else {
26+
throw IconError.alternateIconsNotSupported
27+
}
28+
guard icon.alternateName != UIApplication.shared.alternateIconName else { return }
29+
try await UIApplication.shared.setAlternateIconName(icon.alternateName)
30+
currentAppIcon = icon
31+
logger.info("Установили иконку: \(icon.rawValue)")
32+
} catch {
33+
logger.error("\(error.localizedDescription)")
34+
}
35+
}
36+
}
37+
}
38+
39+
extension ThemeIconScreen.IconViewModel {
40+
enum IconError: Error, LocalizedError {
41+
case alternateIconsNotSupported
42+
43+
var errorDescription: String? {
44+
switch self {
45+
case .alternateIconsNotSupported:
46+
"Альтернативные иконки не поддерживаются"
47+
}
48+
}
49+
}
50+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import SwiftUI
2+
3+
struct ThemeIconScreen: View {
4+
@Environment(AppSettings.self) private var appSettings
5+
@State private var iconViewModel = IconViewModel()
6+
7+
var body: some View {
8+
ScrollView {
9+
VStack(alignment: .leading, spacing: 24) {
10+
themePicker
11+
iconsGrid
12+
.animation(.default, value: iconViewModel.currentAppIcon)
13+
}
14+
.padding()
15+
}
16+
.scrollBounceBehavior(.basedOnSize)
17+
.navigationTitle("App theme and Icon")
18+
.navigationBarTitleDisplayMode(.inline)
19+
}
20+
21+
private var themePicker: some View {
22+
HStack(spacing: 12) {
23+
Text("App theme")
24+
.bold()
25+
.frame(maxWidth: .infinity, alignment: .leading)
26+
.accessibilityHidden(true)
27+
Picker(
28+
"App theme",
29+
selection: .init(
30+
get: { appSettings.appTheme },
31+
set: { appSettings.appTheme = $0 }
32+
)
33+
) {
34+
ForEach(AppTheme.allCases) {
35+
Text($0.title).tag($0)
36+
}
37+
}
38+
.accessibilityAddTraits(.isButton)
39+
.accessibilityIdentifier("appThemePicker")
40+
}
41+
}
42+
43+
private var iconsGrid: some View {
44+
VStack(spacing: 16) {
45+
Text("App Icon")
46+
.bold()
47+
.frame(maxWidth: .infinity, alignment: .leading)
48+
LazyVGrid(
49+
columns: [GridItem(.adaptive(minimum: 65), spacing: 32, alignment: .leading)],
50+
spacing: 32
51+
) {
52+
ForEach(IconVariant.allCases, id: \.self) { icon in
53+
Button {
54+
Task { await iconViewModel.setIcon(icon) }
55+
} label: {
56+
makeView(for: icon)
57+
}
58+
.accessibilityLabel(Text(icon.accessibilityLabel))
59+
}
60+
}
61+
.accessibilityIdentifier("appIconsGrid")
62+
}
63+
}
64+
65+
private func makeView(for icon: ThemeIconScreen.IconVariant) -> some View {
66+
icon
67+
.listImage
68+
.resizable()
69+
.scaledToFit()
70+
.frame(width: 65, height: 65)
71+
.clipShape(.rect(cornerRadius: 12))
72+
.overlay {
73+
RoundedRectangle(cornerRadius: 12)
74+
.strokeBorder(.iconBorder, lineWidth: 1)
75+
}
76+
.drawingGroup()
77+
.overlay(alignment: .topTrailing) {
78+
if icon == iconViewModel.currentAppIcon {
79+
Image(.thumbCheckmark)
80+
.offset(x: 6, y: -6)
81+
.transition(.opacity.combined(with: .scale))
82+
.accessibilityHidden(true)
83+
}
84+
}
85+
}
86+
}
87+
88+
#Preview {
89+
NavigationStack {
90+
ThemeIconScreen()
91+
}
92+
.environment(AppSettings())
93+
}
11.8 KB
Loading
10.9 KB
Loading

0 commit comments

Comments
 (0)