Skip to content

Commit 2a33dd0

Browse files
authored
iOS: Clarify reset necessity for users (#1197)
^ALTAPPS-1350
1 parent 3908166 commit 2a33dd0

File tree

13 files changed

+300
-14
lines changed

13 files changed

+300
-14
lines changed

androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/fragment/DefaultStepQuizFragment.kt

+3
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ abstract class DefaultStepQuizFragment :
342342
StepQuizFeature.Action.ViewAction.UnhighlightCallToActionButton -> {
343343
// no op
344344
}
345+
StepQuizFeature.Action.ViewAction.BounceCallToActionButton -> {
346+
// TODO: ALTAPPS-1349 Implement bounce animation
347+
}
345348
}
346349
}
347350

config/detekt/baseline.xml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<ID>ComplexCondition:GamificationToolbarReducer.kt$GamificationToolbarReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
1111
<ID>ComplexCondition:LeaderboardWidgetReducer.kt$LeaderboardWidgetReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
1212
<ID>ComplexCondition:ProfileReducer.kt$ProfileReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
13+
<ID>ComplexCondition:StepQuizReducer.kt$StepQuizReducer$state.stepQuizState is StepQuizState.AttemptLoaded &amp;&amp; !StepQuizResolver.isQuizEnabled(state.stepQuizState) &amp;&amp; state.stepQuizState.submission?.status.isWrongOrRejected &amp;&amp; StepQuizResolver.isNeedRecreateAttemptForNewSubmission(state.stepQuizState.step)</ID>
1314
<ID>ComplexCondition:TopicsRepetitionsReducer.kt$TopicsRepetitionsReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.NetworkError))</ID>
1415
<ID>ComplexCondition:WelcomeReducer.kt$WelcomeReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.NetworkError))</ID>
1516
<ID>ComposableParamOrder:FindStage.kt$FindStage</ID>

iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj

+12
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@
170170
2C37960F2876F36F00C197E2 /* ProfileViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C37960E2876F36F00C197E2 /* ProfileViewData.swift */; };
171171
2C3796122877001700C197E2 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3796112877001700C197E2 /* ProfileHeaderView.swift */; };
172172
2C3B84E82C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */; };
173+
2C3B90652CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */; };
174+
2C3B90692CA66ABE00FDA6CB /* BounceEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */; };
175+
2C3B906B2CA6861400FDA6CB /* JiggleEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */; };
173176
2C3CE3962C1073990011BECA /* StepToolbarContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3CE3952C1073990011BECA /* StepToolbarContent.swift */; };
174177
2C3D92D42C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3D92D32C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift */; };
175178
2C3E656D2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */; };
@@ -978,6 +981,9 @@
978981
2C37960E2876F36F00C197E2 /* ProfileViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewData.swift; sourceTree = "<group>"; };
979982
2C3796112877001700C197E2 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
980983
2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksVariableInstructionView.swift; sourceTree = "<group>"; };
984+
2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OnTapWhenDisabled.swift"; sourceTree = "<group>"; };
985+
2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BounceEffect.swift; sourceTree = "<group>"; };
986+
2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JiggleEffect.swift; sourceTree = "<group>"; };
981987
2C3CE3952C1073990011BECA /* StepToolbarContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepToolbarContent.swift; sourceTree = "<group>"; };
982988
2C3D92D32C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift; sourceTree = "<group>"; };
983989
2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackSelectionListHeaderView.swift; sourceTree = "<group>"; };
@@ -2226,6 +2232,7 @@
22262232
2C177EC22837B65500D841DB /* View+Frame.swift */,
22272233
2CF34F9A2C34079C0054477E /* View+ListRowSeparator.swift */,
22282234
E9FAF38E299F61AE001FC596 /* View+MeasureSize.swift */,
2235+
2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */,
22292236
2CD4EDF82B79D51E0091F0B2 /* View+SafeAreaInset.swift */,
22302237
2C4605B02ABD75FC003C17E9 /* View+ScrollBounceBehavior.swift */,
22312238
2C7271232B6B634F005628B0 /* View+Task.swift */,
@@ -2463,6 +2470,8 @@
24632470
2C4677962C75E7D7000EB2EE /* Effects */ = {
24642471
isa = PBXGroup;
24652472
children = (
2473+
2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */,
2474+
2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */,
24662475
2C4677942C75E7CA000EB2EE /* PulseEffect.swift */,
24672476
2CAF254B2AB9C2E500595582 /* ShineEffect.swift */,
24682477
);
@@ -5124,6 +5133,7 @@
51245133
E94D238F280585110003273F /* AuthCredentialsFormView.swift in Sources */,
51255134
2C5CBBE52948FA7400113007 /* StepQuizSQLAssembly.swift in Sources */,
51265135
2C54E4222A1F6672003406B9 /* TrackSelectionDetailsFeatureViewStateKsExtensions.swift in Sources */,
5136+
2C3B90652CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift in Sources */,
51275137
2CEEE03528916A6800282849 /* ProblemOfDayOutputProtocol.swift in Sources */,
51285138
2C66720B2A52974A0040EA2F /* ProgressScreenSectionTitleSkeletonView.swift in Sources */,
51295139
2C0EB9542A151C2B006DC84B /* TrackSelectionListFeatureViewStateKsExtensions.swift in Sources */,
@@ -5237,6 +5247,7 @@
52375247
2CD20ED12B73475400FB5269 /* ApplicationShortcutsService.swift in Sources */,
52385248
2C5F4A5A2971C71200677530 /* GamificationToolbarContent.swift in Sources */,
52395249
2C5B2A1F286595AF0097B270 /* CodeCompletionTableViewController.swift in Sources */,
5250+
2C3B906B2CA6861400FDA6CB /* JiggleEffect.swift in Sources */,
52405251
2CE0F6EE2BB40B760032C439 /* StepFeedbackViewModel.swift in Sources */,
52415252
2CCCA3A12862E62F00D98089 /* StepQuizStringViewData.swift in Sources */,
52425253
2C1061A2285C349400EBD614 /* StepQuizChildQuizAssembly.swift in Sources */,
@@ -5553,6 +5564,7 @@
55535564
2C0EB9502A151B56006DC84B /* TrackSelectionListViewModel.swift in Sources */,
55545565
E9ACD3412937342F0005E05B /* ProblemOfDaySolvedModalViewController.swift in Sources */,
55555566
2C0EB9562A15296D006DC84B /* TrackSelectionListFeatureViewStateContent+Placeholder.swift in Sources */,
5567+
2C3B90692CA66ABE00FDA6CB /* BounceEffect.swift in Sources */,
55565568
2CB0AE002B0525020089D557 /* ChallengeWidgetContentStateDescriptionView.swift in Sources */,
55575569
2CE58C5A2B07662300E5EBBE /* ChallengeWidgetContentStateProgressGridView.swift in Sources */,
55585570
E9D537D22A71330A00F21828 /* LinearGradientProgressView.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import SwiftUI
2+
3+
extension View {
4+
@ViewBuilder
5+
func onTapWhenDisabled(isDisabled: Bool, action: @escaping () -> Void) -> some View {
6+
if isDisabled {
7+
self.overlay(
8+
Color.clear
9+
.contentShape(Rectangle())
10+
.onTapGesture(perform: action)
11+
)
12+
} else {
13+
self
14+
}
15+
}
16+
}
17+
18+
#if DEBUG
19+
@available(iOS 17.0, *)
20+
#Preview {
21+
@Previewable @State var isButtonDisabled = true
22+
23+
VStack {
24+
Button(
25+
action: {
26+
print("Button tapped!")
27+
},
28+
label: {
29+
Text("Submit")
30+
.padding()
31+
.background(isButtonDisabled ? Color.gray : Color.blue)
32+
.foregroundColor(.white)
33+
.cornerRadius(8)
34+
}
35+
)
36+
.disabled(isButtonDisabled)
37+
.onTapWhenDisabled(isDisabled: isButtonDisabled) {
38+
print("Button is disabled!")
39+
}
40+
41+
Toggle("Disable View", isOn: $isButtonDisabled)
42+
.padding()
43+
}
44+
}
45+
#endif

iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift

+4
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ final class StepQuizViewModel: FeatureViewModel<
134134
moduleOutput?.stepQuizDidRequestSkipStep()
135135
}
136136

137+
func doChildQuizClickedWhenDisabledAction() {
138+
onNewMessage(StepQuizFeatureMessageChildQuizClickedWhenDisabled())
139+
}
140+
137141
func doUnsupportedQuizSolveOnTheWebAction() {
138142
onNewMessage(StepQuizFeatureMessageUnsupportedQuizSolveOnTheWebClicked())
139143
}

iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/Views/StepQuizActionButtons/StepQuizActionButtons.swift

+15-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ struct StepQuizActionButtons: View {
1414
StepQuizRetryButton(
1515
appearance: retryButton.appearance,
1616
style: retryButton.style,
17+
isBounceEffectActive: retryButton.isBounceEffectActive,
1718
onTap: retryButton.action
1819
)
1920
}
@@ -45,6 +46,8 @@ struct StepQuizActionButtons: View {
4546

4647
var style: StepQuizRetryButton.Style = .logoOnly
4748

49+
var isBounceEffectActive = false
50+
4851
let action: () -> Void
4952
}
5053

@@ -103,8 +106,17 @@ extension StepQuizActionButtons {
103106
)
104107
}
105108

106-
static func retry(action: @escaping () -> Void) -> StepQuizActionButtons {
107-
StepQuizActionButtons(retryButton: .init(style: .roundedRectangle, action: action))
109+
static func retry(
110+
isBounceEffectActive: Bool,
111+
action: @escaping () -> Void
112+
) -> StepQuizActionButtons {
113+
StepQuizActionButtons(
114+
retryButton: .init(
115+
style: .roundedRectangle,
116+
isBounceEffectActive: isBounceEffectActive,
117+
action: action
118+
)
119+
)
108120
}
109121

110122
static func `continue`(isLoading: Bool, action: @escaping () -> Void) -> StepQuizActionButtons {
@@ -168,7 +180,7 @@ struct StepQuizActionButtons_Previews: PreviewProvider {
168180
action: {}
169181
)
170182

171-
StepQuizActionButtons.retry {}
183+
StepQuizActionButtons.retry(isBounceEffectActive: false) {}
172184

173185
StepQuizActionButtons.continue(isLoading: false) {}
174186
StepQuizActionButtons.continue(isLoading: true) {}

iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/Views/StepQuizActionButtons/StepQuizRetryButton.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ struct StepQuizRetryButton: View {
1818

1919
var style: Style
2020

21+
let isBounceEffectActive: Bool
22+
2123
var onTap: () -> Void
2224

2325
@Environment(\.isEnabled) private var isEnabled
@@ -45,6 +47,7 @@ struct StepQuizRetryButton: View {
4547
action: onTap
4648
)
4749
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
50+
.bounceEffect(isActive: isBounceEffectActive && isEnabled)
4851
}
4952
}
5053

@@ -58,15 +61,15 @@ struct StepQuizRetryButton_Previews: PreviewProvider {
5861
static var previews: some View {
5962
VStack {
6063
HStack {
61-
StepQuizRetryButton(style: .logoOnly, onTap: {})
64+
StepQuizRetryButton(style: .logoOnly, isBounceEffectActive: false, onTap: {})
6265

63-
StepQuizRetryButton(style: .logoOnly, onTap: {})
66+
StepQuizRetryButton(style: .logoOnly, isBounceEffectActive: true, onTap: {})
6467
.disabled(true)
6568
}
6669

67-
StepQuizRetryButton(style: .roundedRectangle, onTap: {})
70+
StepQuizRetryButton(style: .roundedRectangle, isBounceEffectActive: false, onTap: {})
6871

69-
StepQuizRetryButton(style: .roundedRectangle, onTap: {})
72+
StepQuizRetryButton(style: .roundedRectangle, isBounceEffectActive: true, onTap: {})
7073
.disabled(true)
7174
}
7275
.previewLayout(.sizeThatFits)

iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/Views/StepQuizView.swift

+20-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct StepQuizView: View {
1919

2020
@State private var scrollPosition: ScrollPosition?
2121
@State private var isActionButtonAnimationEffectActive = false
22+
@State private var isActionButtonBounceAnimationEffectActive = false
2223

2324
var body: some View {
2425
UIViewControllerEventsWrapper(
@@ -169,15 +170,15 @@ struct StepQuizView: View {
169170
step: Step,
170171
attemptLoadedState: StepQuizFeatureStepQuizStateAttemptLoaded
171172
) -> some View {
172-
let stepQuizCodeBlanksState = viewModel.state.stepQuizCodeBlanksState
173-
if stepQuizCodeBlanksState is StepQuizCodeBlanksFeatureStateContent {
173+
let isQuizDisabled = !StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState)
174+
if viewModel.state.stepQuizCodeBlanksState is StepQuizCodeBlanksFeatureStateContent {
174175
StepQuizCodeBlanksAssembly(
175-
state: stepQuizCodeBlanksState,
176+
state: viewModel.state.stepQuizCodeBlanksState,
176177
moduleOutput: viewModel
177178
)
178179
.makeModule()
179180
.equatable()
180-
.disabled(!StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState))
181+
.disabled(isQuizDisabled)
181182
} else if let dataset = attemptLoadedState.attempt.dataset {
182183
let reply = StepQuizStateExtensionsKt.reply(attemptLoadedState.submissionState)
183184

@@ -189,7 +190,11 @@ struct StepQuizView: View {
189190
provideModuleInputCallback: { viewModel.childQuizModuleInput = $0 },
190191
moduleOutput: viewModel
191192
)
192-
.disabled(!StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState))
193+
.disabled(isQuizDisabled)
194+
.onTapWhenDisabled(
195+
isDisabled: isQuizDisabled,
196+
action: viewModel.doChildQuizClickedWhenDisabledAction
197+
)
193198
}
194199
}
195200

@@ -215,8 +220,11 @@ struct StepQuizView: View {
215220
)
216221
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
217222
} else if StepQuizResolver.shared.isNeedRecreateAttemptForNewSubmission(step: viewModel.step) {
218-
StepQuizActionButtons.retry(action: viewModel.doQuizRetryAction)
219-
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
223+
StepQuizActionButtons.retry(
224+
isBounceEffectActive: isActionButtonBounceAnimationEffectActive,
225+
action: viewModel.doQuizRetryAction
226+
)
227+
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
220228
} else {
221229
StepQuizActionButtons.submit(
222230
state: .init(submissionStatus: submissionStatus),
@@ -325,6 +333,11 @@ private extension StepQuizView {
325333
isActionButtonAnimationEffectActive = true
326334
case .unhighlightCallToActionButton:
327335
isActionButtonAnimationEffectActive = false
336+
case .bounceCallToActionButton:
337+
isActionButtonBounceAnimationEffectActive = true
338+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
339+
isActionButtonBounceAnimationEffectActive = false
340+
}
328341
}
329342
}
330343

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import SwiftUI
2+
3+
private struct BounceEffectViewModifier: ViewModifier {
4+
@State private var isBouncing = false
5+
6+
func body(content: Content) -> some View {
7+
content
8+
.scaleEffect(isBouncing ? 0.95 : 1)
9+
.animation(
10+
.easeInOut(duration: 0.15)
11+
.repeatForever(autoreverses: true)
12+
.delay(0.33),
13+
value: isBouncing
14+
)
15+
.onAppear {
16+
isBouncing = true
17+
}
18+
}
19+
}
20+
21+
extension View {
22+
@ViewBuilder
23+
func bounceEffect(isActive: Bool = true) -> some View {
24+
if isActive {
25+
modifier(BounceEffectViewModifier())
26+
} else {
27+
self
28+
}
29+
}
30+
}
31+
32+
#if DEBUG
33+
@available(iOS 17.0, *)
34+
#Preview {
35+
@Previewable @State var isBouncing = false
36+
37+
Button {
38+
isBouncing.toggle()
39+
} label: {
40+
Text("Retry")
41+
}
42+
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
43+
.bounceEffect(isActive: isBouncing)
44+
.padding()
45+
}
46+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import SwiftUI
2+
3+
private struct JiggleEffectViewModifier: ViewModifier {
4+
let amount: Double
5+
6+
@State private var isJiggling = false
7+
8+
func body(content: Content) -> some View {
9+
content
10+
.rotationEffect(.degrees(isJiggling ? amount : 0))
11+
.animation(
12+
.easeInOut(duration: randomize(interval: 0.14, withVariance: 0.025))
13+
.repeatForever(autoreverses: true),
14+
value: isJiggling
15+
)
16+
.animation(
17+
.easeInOut(duration: randomize(interval: 0.18, withVariance: 0.025))
18+
.repeatForever(autoreverses: true),
19+
value: isJiggling
20+
)
21+
.onAppear {
22+
isJiggling.toggle()
23+
}
24+
}
25+
26+
private func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
27+
interval + variance * (Double.random(in: 500...1_000) / 500)
28+
}
29+
}
30+
31+
extension View {
32+
@ViewBuilder
33+
func jiggleEffect(amount: Double = 2, isActive: Bool = true) -> some View {
34+
if isActive {
35+
modifier(JiggleEffectViewModifier(amount: amount))
36+
} else {
37+
self
38+
}
39+
}
40+
}
41+
42+
#if DEBUG
43+
@available(iOS 17.0, *)
44+
#Preview {
45+
@Previewable @State var isJiggling = false
46+
47+
Button {
48+
isJiggling.toggle()
49+
} label: {
50+
Text("Retry")
51+
}
52+
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
53+
.jiggleEffect(amount: 2, isActive: isJiggling)
54+
.padding()
55+
}
56+
#endif

0 commit comments

Comments
 (0)