Skip to content

Commit

Permalink
iOS: Clarify reset necessity for users (#1197)
Browse files Browse the repository at this point in the history
^ALTAPPS-1350
  • Loading branch information
ivan-magda authored Sep 27, 2024
1 parent 3908166 commit 2a33dd0
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ abstract class DefaultStepQuizFragment :
StepQuizFeature.Action.ViewAction.UnhighlightCallToActionButton -> {
// no op
}
StepQuizFeature.Action.ViewAction.BounceCallToActionButton -> {
// TODO: ALTAPPS-1349 Implement bounce animation
}
}
}

Expand Down
1 change: 1 addition & 0 deletions config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ID>ComplexCondition:GamificationToolbarReducer.kt$GamificationToolbarReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
<ID>ComplexCondition:LeaderboardWidgetReducer.kt$LeaderboardWidgetReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
<ID>ComplexCondition:ProfileReducer.kt$ProfileReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.Error))</ID>
<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>
<ID>ComplexCondition:TopicsRepetitionsReducer.kt$TopicsRepetitionsReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.NetworkError))</ID>
<ID>ComplexCondition:WelcomeReducer.kt$WelcomeReducer$state is State.Idle || (message.forceUpdate &amp;&amp; (state is State.Content || state is State.NetworkError))</ID>
<ID>ComposableParamOrder:FindStage.kt$FindStage</ID>
Expand Down
12 changes: 12 additions & 0 deletions iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@
2C37960F2876F36F00C197E2 /* ProfileViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C37960E2876F36F00C197E2 /* ProfileViewData.swift */; };
2C3796122877001700C197E2 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3796112877001700C197E2 /* ProfileHeaderView.swift */; };
2C3B84E82C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */; };
2C3B90652CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */; };
2C3B90692CA66ABE00FDA6CB /* BounceEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */; };
2C3B906B2CA6861400FDA6CB /* JiggleEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */; };
2C3CE3962C1073990011BECA /* StepToolbarContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3CE3952C1073990011BECA /* StepToolbarContent.swift */; };
2C3D92D42C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3D92D32C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift */; };
2C3E656D2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */; };
Expand Down Expand Up @@ -978,6 +981,9 @@
2C37960E2876F36F00C197E2 /* ProfileViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewData.swift; sourceTree = "<group>"; };
2C3796112877001700C197E2 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksVariableInstructionView.swift; sourceTree = "<group>"; };
2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OnTapWhenDisabled.swift"; sourceTree = "<group>"; };
2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BounceEffect.swift; sourceTree = "<group>"; };
2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JiggleEffect.swift; sourceTree = "<group>"; };
2C3CE3952C1073990011BECA /* StepToolbarContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepToolbarContent.swift; sourceTree = "<group>"; };
2C3D92D32C857DAA00D271B7 /* StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyPlanWidgetViewStateSectionContentPageLoadingStateWrapper.swift; sourceTree = "<group>"; };
2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackSelectionListHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2226,6 +2232,7 @@
2C177EC22837B65500D841DB /* View+Frame.swift */,
2CF34F9A2C34079C0054477E /* View+ListRowSeparator.swift */,
E9FAF38E299F61AE001FC596 /* View+MeasureSize.swift */,
2C3B90642CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift */,
2CD4EDF82B79D51E0091F0B2 /* View+SafeAreaInset.swift */,
2C4605B02ABD75FC003C17E9 /* View+ScrollBounceBehavior.swift */,
2C7271232B6B634F005628B0 /* View+Task.swift */,
Expand Down Expand Up @@ -2463,6 +2470,8 @@
2C4677962C75E7D7000EB2EE /* Effects */ = {
isa = PBXGroup;
children = (
2C3B90682CA66ABE00FDA6CB /* BounceEffect.swift */,
2C3B906A2CA6861400FDA6CB /* JiggleEffect.swift */,
2C4677942C75E7CA000EB2EE /* PulseEffect.swift */,
2CAF254B2AB9C2E500595582 /* ShineEffect.swift */,
);
Expand Down Expand Up @@ -5124,6 +5133,7 @@
E94D238F280585110003273F /* AuthCredentialsFormView.swift in Sources */,
2C5CBBE52948FA7400113007 /* StepQuizSQLAssembly.swift in Sources */,
2C54E4222A1F6672003406B9 /* TrackSelectionDetailsFeatureViewStateKsExtensions.swift in Sources */,
2C3B90652CA65AA400FDA6CB /* View+OnTapWhenDisabled.swift in Sources */,
2CEEE03528916A6800282849 /* ProblemOfDayOutputProtocol.swift in Sources */,
2C66720B2A52974A0040EA2F /* ProgressScreenSectionTitleSkeletonView.swift in Sources */,
2C0EB9542A151C2B006DC84B /* TrackSelectionListFeatureViewStateKsExtensions.swift in Sources */,
Expand Down Expand Up @@ -5237,6 +5247,7 @@
2CD20ED12B73475400FB5269 /* ApplicationShortcutsService.swift in Sources */,
2C5F4A5A2971C71200677530 /* GamificationToolbarContent.swift in Sources */,
2C5B2A1F286595AF0097B270 /* CodeCompletionTableViewController.swift in Sources */,
2C3B906B2CA6861400FDA6CB /* JiggleEffect.swift in Sources */,
2CE0F6EE2BB40B760032C439 /* StepFeedbackViewModel.swift in Sources */,
2CCCA3A12862E62F00D98089 /* StepQuizStringViewData.swift in Sources */,
2C1061A2285C349400EBD614 /* StepQuizChildQuizAssembly.swift in Sources */,
Expand Down Expand Up @@ -5553,6 +5564,7 @@
2C0EB9502A151B56006DC84B /* TrackSelectionListViewModel.swift in Sources */,
E9ACD3412937342F0005E05B /* ProblemOfDaySolvedModalViewController.swift in Sources */,
2C0EB9562A15296D006DC84B /* TrackSelectionListFeatureViewStateContent+Placeholder.swift in Sources */,
2C3B90692CA66ABE00FDA6CB /* BounceEffect.swift in Sources */,
2CB0AE002B0525020089D557 /* ChallengeWidgetContentStateDescriptionView.swift in Sources */,
2CE58C5A2B07662300E5EBBE /* ChallengeWidgetContentStateProgressGridView.swift in Sources */,
E9D537D22A71330A00F21828 /* LinearGradientProgressView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import SwiftUI

extension View {
@ViewBuilder
func onTapWhenDisabled(isDisabled: Bool, action: @escaping () -> Void) -> some View {
if isDisabled {
self.overlay(
Color.clear
.contentShape(Rectangle())
.onTapGesture(perform: action)
)
} else {
self
}
}
}

#if DEBUG
@available(iOS 17.0, *)
#Preview {
@Previewable @State var isButtonDisabled = true

VStack {
Button(
action: {
print("Button tapped!")
},
label: {
Text("Submit")
.padding()
.background(isButtonDisabled ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
)
.disabled(isButtonDisabled)
.onTapWhenDisabled(isDisabled: isButtonDisabled) {
print("Button is disabled!")
}

Toggle("Disable View", isOn: $isButtonDisabled)
.padding()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ final class StepQuizViewModel: FeatureViewModel<
moduleOutput?.stepQuizDidRequestSkipStep()
}

func doChildQuizClickedWhenDisabledAction() {
onNewMessage(StepQuizFeatureMessageChildQuizClickedWhenDisabled())
}

func doUnsupportedQuizSolveOnTheWebAction() {
onNewMessage(StepQuizFeatureMessageUnsupportedQuizSolveOnTheWebClicked())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct StepQuizActionButtons: View {
StepQuizRetryButton(
appearance: retryButton.appearance,
style: retryButton.style,
isBounceEffectActive: retryButton.isBounceEffectActive,
onTap: retryButton.action
)
}
Expand Down Expand Up @@ -45,6 +46,8 @@ struct StepQuizActionButtons: View {

var style: StepQuizRetryButton.Style = .logoOnly

var isBounceEffectActive = false

let action: () -> Void
}

Expand Down Expand Up @@ -103,8 +106,17 @@ extension StepQuizActionButtons {
)
}

static func retry(action: @escaping () -> Void) -> StepQuizActionButtons {
StepQuizActionButtons(retryButton: .init(style: .roundedRectangle, action: action))
static func retry(
isBounceEffectActive: Bool,
action: @escaping () -> Void
) -> StepQuizActionButtons {
StepQuizActionButtons(
retryButton: .init(
style: .roundedRectangle,
isBounceEffectActive: isBounceEffectActive,
action: action
)
)
}

static func `continue`(isLoading: Bool, action: @escaping () -> Void) -> StepQuizActionButtons {
Expand Down Expand Up @@ -168,7 +180,7 @@ struct StepQuizActionButtons_Previews: PreviewProvider {
action: {}
)

StepQuizActionButtons.retry {}
StepQuizActionButtons.retry(isBounceEffectActive: false) {}

StepQuizActionButtons.continue(isLoading: false) {}
StepQuizActionButtons.continue(isLoading: true) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct StepQuizRetryButton: View {

var style: Style

let isBounceEffectActive: Bool

var onTap: () -> Void

@Environment(\.isEnabled) private var isEnabled
Expand Down Expand Up @@ -45,6 +47,7 @@ struct StepQuizRetryButton: View {
action: onTap
)
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
.bounceEffect(isActive: isBounceEffectActive && isEnabled)
}
}

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

StepQuizRetryButton(style: .logoOnly, onTap: {})
StepQuizRetryButton(style: .logoOnly, isBounceEffectActive: true, onTap: {})
.disabled(true)
}

StepQuizRetryButton(style: .roundedRectangle, onTap: {})
StepQuizRetryButton(style: .roundedRectangle, isBounceEffectActive: false, onTap: {})

StepQuizRetryButton(style: .roundedRectangle, onTap: {})
StepQuizRetryButton(style: .roundedRectangle, isBounceEffectActive: true, onTap: {})
.disabled(true)
}
.previewLayout(.sizeThatFits)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct StepQuizView: View {

@State private var scrollPosition: ScrollPosition?
@State private var isActionButtonAnimationEffectActive = false
@State private var isActionButtonBounceAnimationEffectActive = false

var body: some View {
UIViewControllerEventsWrapper(
Expand Down Expand Up @@ -169,15 +170,15 @@ struct StepQuizView: View {
step: Step,
attemptLoadedState: StepQuizFeatureStepQuizStateAttemptLoaded
) -> some View {
let stepQuizCodeBlanksState = viewModel.state.stepQuizCodeBlanksState
if stepQuizCodeBlanksState is StepQuizCodeBlanksFeatureStateContent {
let isQuizDisabled = !StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState)
if viewModel.state.stepQuizCodeBlanksState is StepQuizCodeBlanksFeatureStateContent {
StepQuizCodeBlanksAssembly(
state: stepQuizCodeBlanksState,
state: viewModel.state.stepQuizCodeBlanksState,
moduleOutput: viewModel
)
.makeModule()
.equatable()
.disabled(!StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState))
.disabled(isQuizDisabled)
} else if let dataset = attemptLoadedState.attempt.dataset {
let reply = StepQuizStateExtensionsKt.reply(attemptLoadedState.submissionState)

Expand All @@ -189,7 +190,11 @@ struct StepQuizView: View {
provideModuleInputCallback: { viewModel.childQuizModuleInput = $0 },
moduleOutput: viewModel
)
.disabled(!StepQuizResolver.shared.isQuizEnabled(state: attemptLoadedState))
.disabled(isQuizDisabled)
.onTapWhenDisabled(
isDisabled: isQuizDisabled,
action: viewModel.doChildQuizClickedWhenDisabledAction
)
}
}

Expand All @@ -215,8 +220,11 @@ struct StepQuizView: View {
)
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
} else if StepQuizResolver.shared.isNeedRecreateAttemptForNewSubmission(step: viewModel.step) {
StepQuizActionButtons.retry(action: viewModel.doQuizRetryAction)
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
StepQuizActionButtons.retry(
isBounceEffectActive: isActionButtonBounceAnimationEffectActive,
action: viewModel.doQuizRetryAction
)
.disabled(StepQuizResolver.shared.isQuizLoading(state: state.stepQuizState))
} else {
StepQuizActionButtons.submit(
state: .init(submissionStatus: submissionStatus),
Expand Down Expand Up @@ -325,6 +333,11 @@ private extension StepQuizView {
isActionButtonAnimationEffectActive = true
case .unhighlightCallToActionButton:
isActionButtonAnimationEffectActive = false
case .bounceCallToActionButton:
isActionButtonBounceAnimationEffectActive = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
isActionButtonBounceAnimationEffectActive = false
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SwiftUI

private struct BounceEffectViewModifier: ViewModifier {
@State private var isBouncing = false

func body(content: Content) -> some View {
content
.scaleEffect(isBouncing ? 0.95 : 1)
.animation(
.easeInOut(duration: 0.15)
.repeatForever(autoreverses: true)
.delay(0.33),
value: isBouncing
)
.onAppear {
isBouncing = true
}
}
}

extension View {
@ViewBuilder
func bounceEffect(isActive: Bool = true) -> some View {
if isActive {
modifier(BounceEffectViewModifier())
} else {
self
}
}
}

#if DEBUG
@available(iOS 17.0, *)
#Preview {
@Previewable @State var isBouncing = false

Button {
isBouncing.toggle()
} label: {
Text("Retry")
}
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
.bounceEffect(isActive: isBouncing)
.padding()
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import SwiftUI

private struct JiggleEffectViewModifier: ViewModifier {
let amount: Double

@State private var isJiggling = false

func body(content: Content) -> some View {
content
.rotationEffect(.degrees(isJiggling ? amount : 0))
.animation(
.easeInOut(duration: randomize(interval: 0.14, withVariance: 0.025))
.repeatForever(autoreverses: true),
value: isJiggling
)
.animation(
.easeInOut(duration: randomize(interval: 0.18, withVariance: 0.025))
.repeatForever(autoreverses: true),
value: isJiggling
)
.onAppear {
isJiggling.toggle()
}
}

private func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
interval + variance * (Double.random(in: 500...1_000) / 500)
}
}

extension View {
@ViewBuilder
func jiggleEffect(amount: Double = 2, isActive: Bool = true) -> some View {
if isActive {
modifier(JiggleEffectViewModifier(amount: amount))
} else {
self
}
}
}

#if DEBUG
@available(iOS 17.0, *)
#Preview {
@Previewable @State var isJiggling = false

Button {
isJiggling.toggle()
} label: {
Text("Retry")
}
.buttonStyle(RoundedRectangleButtonStyle(style: .violet))
.jiggleEffect(amount: 2, isActive: isJiggling)
.padding()
}
#endif
Loading

0 comments on commit 2a33dd0

Please sign in to comment.