Skip to content

Commit adbeecc

Browse files
committed
feat: add alert / hot switch for theme changes (Orange-OpenSource/ouds-ios#850)
Closes Orange-OpenSource/ouds-ios#850 Signed-off-by: Pierre-Yves Lapersonne <[email protected]>
1 parent f23381c commit adbeecc

File tree

6 files changed

+219
-5
lines changed

6 files changed

+219
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11+
- [DesignToolbox] Alert killing or not the app when theme changed (if toggled) (Orange-OpenSource/ouds-ios#850)
1112
- [Library] Chip component (Orange-OpenSource/ouds-ios#407)
1213
- [Library] Add `badge` component (Orange-OpenSource/ouds-ios#514)
1314
- [Library] Chip component (Orange-OpenSource/ouds-ios#407)

DesignToolbox/DesignToolbox.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@
320320
51D6D58C2D887E94009CD879 /* CheckboxIndeterminatePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6D58A2D887E94009CD879 /* CheckboxIndeterminatePage.swift */; };
321321
51D6D5912D887FA9009CD879 /* CheckboxIndeterminateConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6D5902D887FA9009CD879 /* CheckboxIndeterminateConfiguration.swift */; };
322322
51D6D5922D887FA9009CD879 /* CheckboxIndeterminateConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6D5902D887FA9009CD879 /* CheckboxIndeterminateConfiguration.swift */; };
323+
51E5999D2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5999C2E26AC24002BCB27 /* UserDefaultsWrapper.swift */; };
324+
51E5999E2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5999C2E26AC24002BCB27 /* UserDefaultsWrapper.swift */; };
325+
51E5999F2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5999C2E26AC24002BCB27 /* UserDefaultsWrapper.swift */; };
323326
51EB73952DBFDDF600F37FEC /* CheckboxPickerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EB737F2DBFD4BB00F37FEC /* CheckboxPickerUITests.swift */; };
324327
51EB73962DBFDDF600F37FEC /* AppTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EB737B2DBFD26000F37FEC /* AppTestCase.swift */; };
325328
51EB73982DBFE3FD00F37FEC /* String+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5122E0292D65E7AC00CD2857 /* String+extension.swift */; };
@@ -611,6 +614,7 @@
611614
51D6D58A2D887E94009CD879 /* CheckboxIndeterminatePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxIndeterminatePage.swift; sourceTree = "<group>"; };
612615
51D6D5902D887FA9009CD879 /* CheckboxIndeterminateConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxIndeterminateConfiguration.swift; sourceTree = "<group>"; };
613616
51E3FF0A2CAFD9AE00F1BC59 /* DesignToolboxElementPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignToolboxElementPage.swift; sourceTree = "<group>"; };
617+
51E5999C2E26AC24002BCB27 /* UserDefaultsWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapper.swift; sourceTree = "<group>"; };
614618
51EB737B2DBFD26000F37FEC /* AppTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTestCase.swift; sourceTree = "<group>"; };
615619
51EB737F2DBFD4BB00F37FEC /* CheckboxPickerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxPickerUITests.swift; sourceTree = "<group>"; };
616620
51EB73842DBFDC5500F37FEC /* DesignToolboxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DesignToolboxUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1420,6 +1424,7 @@
14201424
51BD761D2C466FCF0033365D /* Utils */ = {
14211425
isa = PBXGroup;
14221426
children = (
1427+
51E5999C2E26AC24002BCB27 /* UserDefaultsWrapper.swift */,
14231428
51F676DA2E0D45F300C15CB6 /* A11YIdentifiers.swift */,
14241429
5122E0292D65E7AC00CD2857 /* String+extension.swift */,
14251430
51BD761C2C466FCF0033365D /* WebView.swift */,
@@ -1899,6 +1904,7 @@
18991904
518EC0B22E1FF33300FC77E4 /* SoshTheme+BadgeUITests.swift in Sources */,
19001905
513442072D75B880000B5DE4 /* CheckboxElement.swift in Sources */,
19011906
513442082D75B880000B5DE4 /* CheckboxPage.swift in Sources */,
1907+
51E5999D2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */,
19021908
5134420B2D75B880000B5DE4 /* CheckboxConfiguration.swift in Sources */,
19031909
5134420C2D75B880000B5DE4 /* CheckboxElements.swift in Sources */,
19041910
6DB260DD2CD0F01F0091F72E /* NamedSpace+Scaled.swift in Sources */,
@@ -1990,6 +1996,7 @@
19901996
buildActionMask = 2147483647;
19911997
files = (
19921998
0762276D2DA93D62009C3632 /* DesignToolboxColorPicker.swift in Sources */,
1999+
51E5999F2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */,
19932000
51952A672D3F9B9E0068B807 /* DesignToolboxConfiguration.swift in Sources */,
19942001
51952A682D3F9B9E0068B807 /* NamedColor+Opacity.swift in Sources */,
19952002
07AB458B2D50CB9B0001D237 /* DesignToolboxTextField.swift in Sources */,
@@ -2131,6 +2138,7 @@
21312138
buildActionMask = 2147483647;
21322139
files = (
21332140
51F676DC2E0D469500C15CB6 /* A11YIdentifiers.swift in Sources */,
2141+
51E5999E2E26AC2A002BCB27 /* UserDefaultsWrapper.swift in Sources */,
21342142
51F676ED2E0D678600C15CB6 /* SwitchUITests.swift in Sources */,
21352143
51F676D32E0C413700C15CB6 /* RadioPickerUITests.swift in Sources */,
21362144
51F676E82E0D651200C15CB6 /* RadioUITests.swift in Sources */,

DesignToolbox/DesignToolbox/Pages/ThemeSelection/ThemeSelection.swift

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,20 @@ extension OUDSTheme: @retroactive Identifiable, @retroactive Hashable {
5959

6060
/// Theme provider that proposes all supported themes for the demo application.
6161
/// It also stores the current theme, selected by user.
62-
final class ThemeProvider: ObservableObject {
62+
@MainActor final class ThemeProvider: ObservableObject {
6363

6464
let themes: [OUDSTheme]
65+
var hotSwitchWarning: HotSwitchWarning
66+
67+
@UserDefaultsWrapper(key: "com.orange.ouds.demoapp.themeName", defaultValue: "Orange")
68+
private static var currentThemeName
6569

6670
@Published var currentTheme: OUDSTheme {
6771
didSet {
68-
UserDefaults.standard.set(currentTheme.name, forKey: "themeName")
72+
ThemeProvider.currentThemeName = currentTheme.name
73+
if currentTheme != oldValue {
74+
hotSwitchWarning.showAlert = true
75+
}
6976
}
7077
}
7178

@@ -75,13 +82,13 @@ final class ThemeProvider: ObservableObject {
7582
let defaultTheme = orangeTheme
7683
themes = [orangeTheme, soshTheme]
7784

78-
if let themeName = UserDefaults.standard.value(forKey: "themeName") as? String,
79-
let theme = themes.first(where: { $0.name == themeName })
80-
{
85+
if let theme = themes.first(where: { $0.name == ThemeProvider.currentThemeName }) {
8186
currentTheme = theme
8287
} else {
8388
currentTheme = defaultTheme
8489
}
90+
91+
hotSwitchWarning = HotSwitchWarning()
8592
}
8693

8794
deinit {}
@@ -120,5 +127,46 @@ struct ThemeSelectionButton: View {
120127
.accessibilityHint("app_topBar_theme_button_hint_a11y")
121128
}
122129
.oudsForegroundColor(themeProvider.currentTheme.colors.colorContentBrandPrimary)
130+
.modifier(HotSwitchWarningModifier(hotSwitchWarningIndicator: themeProvider.hotSwitchWarning))
131+
}
132+
}
133+
134+
// MARK: - Hot Switch
135+
136+
final class HotSwitchWarning: ObservableObject {
137+
@Published var showAlert: Bool = false
138+
139+
deinit {}
140+
}
141+
142+
/// `ViewModifier` displaying an alert (if set in app settings) notifying the user if it wants to restart the app
143+
/// if the theme changed. If so, kills the app. If not, let iy as is.
144+
/// Thus if the app is killed or if the user restarts it, we won't be in unstable cases like the one explained in issue 850:
145+
/// a situation where an unexisting token is used but with the bad theme.
146+
///
147+
/// See https://github.com/Orange-OpenSource/ouds-ios/issues/850
148+
struct HotSwitchWarningModifier: ViewModifier {
149+
150+
@ObservedObject var hotSwitchWarningIndicator: HotSwitchWarning
151+
152+
@UserDefaultsWrapper(key: "com.orange.ouds.demoapp.askToRestartIfThemeChanged", defaultValue: false) // Defined in Settings.bundle
153+
private var askToRestart: Bool
154+
155+
@ViewBuilder
156+
func body(content: Content) -> some View {
157+
if askToRestart {
158+
content
159+
.alert(isPresented: $hotSwitchWarningIndicator.showAlert) {
160+
Alert(
161+
title: Text("app_settings_themeSwitch_title"),
162+
message: Text("app_settings_themeSwitch_description"),
163+
primaryButton: .destructive(Text("app_settings_themeSwitch_restart")) {
164+
exit(0) // ( ˶°ㅁ°) !! BOOM
165+
},
166+
secondaryButton: .cancel(Text("app_settings_themeSwitch_cancel")))
167+
}
168+
} else {
169+
content
170+
}
123171
}
124172
}

DesignToolbox/DesignToolbox/Resources/Localizable.xcstrings

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2971,6 +2971,101 @@
29712971
},
29722972
"shouldTranslate" : false
29732973
},
2974+
"app_settings_themeSwitch_cancel" : {
2975+
"comment" : "App - Settings",
2976+
"extractionState" : "manual",
2977+
"localizations" : {
2978+
"ar" : {
2979+
"stringUnit" : {
2980+
"state" : "translated",
2981+
"value" : "لا، سأعيد تشغيل التطبيق بنفسي"
2982+
}
2983+
},
2984+
"en" : {
2985+
"stringUnit" : {
2986+
"state" : "translated",
2987+
"value" : "No, I will restart the app myself"
2988+
}
2989+
},
2990+
"fr" : {
2991+
"stringUnit" : {
2992+
"state" : "translated",
2993+
"value" : "Non, je vais relancer l’app moi-même"
2994+
}
2995+
}
2996+
}
2997+
},
2998+
"app_settings_themeSwitch_description" : {
2999+
"comment" : "App - Settings",
3000+
"extractionState" : "manual",
3001+
"localizations" : {
3002+
"ar" : {
3003+
"stringUnit" : {
3004+
"state" : "translated",
3005+
"value" : "لقد سمحت في الإعدادات بإعادة تشغيل التطبيق عند تغيير السمة للتأكد من أن الرموز المتاحة هي الرموز المتوقعة"
3006+
}
3007+
},
3008+
"en" : {
3009+
"stringUnit" : {
3010+
"state" : "translated",
3011+
"value" : "You allowed in settings to restart app when theme changed to be sure available tokens are the expected ones"
3012+
}
3013+
},
3014+
"fr" : {
3015+
"stringUnit" : {
3016+
"state" : "translated",
3017+
"value" : "Vous avez défini dans les réglages de l’app qu’il faut relancer l’app si le thème venait à changer."
3018+
}
3019+
}
3020+
}
3021+
},
3022+
"app_settings_themeSwitch_restart" : {
3023+
"comment" : "App - Settings",
3024+
"extractionState" : "manual",
3025+
"localizations" : {
3026+
"ar" : {
3027+
"stringUnit" : {
3028+
"state" : "translated",
3029+
"value" : "حسنًا، قم بإنهاء التطبيق"
3030+
}
3031+
},
3032+
"en" : {
3033+
"stringUnit" : {
3034+
"state" : "translated",
3035+
"value" : "Ok, terminate the app"
3036+
}
3037+
},
3038+
"fr" : {
3039+
"stringUnit" : {
3040+
"state" : "translated",
3041+
"value" : "Oui, tuer l’app"
3042+
}
3043+
}
3044+
}
3045+
},
3046+
"app_settings_themeSwitch_title" : {
3047+
"comment" : "App - Settings",
3048+
"localizations" : {
3049+
"ar" : {
3050+
"stringUnit" : {
3051+
"state" : "translated",
3052+
"value" : "تبديل السمة وتصحيح الأخطاء"
3053+
}
3054+
},
3055+
"en" : {
3056+
"stringUnit" : {
3057+
"state" : "translated",
3058+
"value" : "Theme switch and debugging"
3059+
}
3060+
},
3061+
"fr" : {
3062+
"stringUnit" : {
3063+
"state" : "translated",
3064+
"value" : "Changement de thème et débogage"
3065+
}
3066+
}
3067+
}
3068+
},
29743069
"app_tokens_border_description_text" : {
29753070
"comment" : "Tokens - Border",
29763071
"extractionState" : "manual",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Software Name: OUDS iOS
3+
// SPDX-FileCopyrightText: Copyright (c) Orange SA
4+
// SPDX-License-Identifier: MIT
5+
//
6+
// This software is distributed under the MIT license,
7+
// the text of which is available at https://opensource.org/license/MIT/
8+
// or see the "LICENSE" file for more details.
9+
//
10+
// Authors: See CONTRIBUTORS.txt
11+
// Software description: A SwiftUI components library with code examples for Orange Unified Design System
12+
//
13+
14+
import Foundation
15+
16+
/// Struct for user defaults management with wrapper to as to read and write values which can be shared
17+
@propertyWrapper
18+
struct UserDefaultsWrapper<T> {
19+
let key: String
20+
let defaultValue: T
21+
let storage: UserDefaults
22+
23+
init(key: String, defaultValue: T) {
24+
self.key = key
25+
self.defaultValue = defaultValue
26+
storage = .standard
27+
}
28+
29+
var wrappedValue: T {
30+
get {
31+
storage.object(forKey: key) as? T ?? defaultValue
32+
}
33+
set {
34+
// Remove value if T is an optional and is nil otherwise storage.set(nil) will crash
35+
if let optionalType = newValue as? OptionalType, optionalType.hasValue {
36+
storage.removeObject(forKey: key)
37+
} else {
38+
storage.set(newValue, forKey: key)
39+
}
40+
}
41+
}
42+
}
43+
44+
private protocol OptionalType {
45+
var hasValue: Bool { get }
46+
}
47+
48+
extension Optional: OptionalType {
49+
var hasValue: Bool {
50+
self == nil
51+
}
52+
}

DesignToolbox/Settings.bundle/Root.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
<string>Root</string>
77
<key>PreferenceSpecifiers</key>
88
<array>
9+
<dict>
10+
<key>Type</key>
11+
<string>PSToggleSwitchSpecifier</string>
12+
<key>Title</key>
13+
<string>Restart app on theme switch</string>
14+
<key>Key</key>
15+
<string>com.orange.ouds.demoapp.askToRestartIfThemeChanged</string>
16+
<key>DefaultValue</key>
17+
<false/>
18+
</dict>
919
<dict>
1020
<key>Type</key>
1121
<string>PSGroupSpecifier</string>

0 commit comments

Comments
 (0)