Skip to content

Commit

Permalink
iOS first problem onboarding (#702)
Browse files Browse the repository at this point in the history
^ALTAPPS-997

---------

Co-authored-by: Ivan Magda <ivan.magda@hyperskill.org>
  • Loading branch information
vladkash and ivan-magda authored Oct 11, 2023
1 parent d6119b8 commit 1e44ea9
Show file tree
Hide file tree
Showing 20 changed files with 527 additions and 10 deletions.
40 changes: 40 additions & 0 deletions iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,12 @@
E99D4CED2826A37400B49D52 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99D4CEC2826A37400B49D52 /* AppAppearance.swift */; };
E99D4CEF2826B91100B49D52 /* StepQuizStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99D4CEE2826B91100B49D52 /* StepQuizStatusView.swift */; };
E9A022AE291D0E3F004317DB /* TopicsRepetitionsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A022AD291D0E3F004317DB /* TopicsRepetitionsCardView.swift */; };
E9A1DA672ACFF180006A9D4B /* FirstProblemOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA662ACFF180006A9D4B /* FirstProblemOnboardingView.swift */; };
E9A1DA6A2ACFF31D006A9D4B /* FirstProblemOnboardingOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA692ACFF31D006A9D4B /* FirstProblemOnboardingOutputProtocol.swift */; };
E9A1DA6C2ACFF38C006A9D4B /* FirstProblemOnboardingAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA6B2ACFF38C006A9D4B /* FirstProblemOnboardingAssembly.swift */; };
E9A1DA6E2ACFF428006A9D4B /* FirstProblemOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA6D2ACFF428006A9D4B /* FirstProblemOnboardingViewModel.swift */; };
E9A1DA702ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA6F2ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift */; };
E9A1DA722AD00D41006A9D4B /* FirstProblemOnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1DA712AD00D41006A9D4B /* FirstProblemOnboardingContentView.swift */; };
E9A22BA0295081BD001700B7 /* StreakFreezeModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A22B9F295081BD001700B7 /* StreakFreezeModalViewController.swift */; };
E9A3435029113FA400E0C0A4 /* StepQuizHintReactionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A3434F29113FA400E0C0A4 /* StepQuizHintReactionButtonView.swift */; };
E9A6250D28ABAE30009423EE /* OnboardingAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A6250C28ABAE30009423EE /* OnboardingAssembly.swift */; };
Expand Down Expand Up @@ -1108,6 +1114,12 @@
E99D4CEC2826A37400B49D52 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = "<group>"; };
E99D4CEE2826B91100B49D52 /* StepQuizStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizStatusView.swift; sourceTree = "<group>"; };
E9A022AD291D0E3F004317DB /* TopicsRepetitionsCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsRepetitionsCardView.swift; sourceTree = "<group>"; };
E9A1DA662ACFF180006A9D4B /* FirstProblemOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingView.swift; sourceTree = "<group>"; };
E9A1DA692ACFF31D006A9D4B /* FirstProblemOnboardingOutputProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingOutputProtocol.swift; sourceTree = "<group>"; };
E9A1DA6B2ACFF38C006A9D4B /* FirstProblemOnboardingAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingAssembly.swift; sourceTree = "<group>"; };
E9A1DA6D2ACFF428006A9D4B /* FirstProblemOnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingViewModel.swift; sourceTree = "<group>"; };
E9A1DA6F2ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingFeatureViewStateKsExtensions.swift; sourceTree = "<group>"; };
E9A1DA712AD00D41006A9D4B /* FirstProblemOnboardingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProblemOnboardingContentView.swift; sourceTree = "<group>"; };
E9A22B9F295081BD001700B7 /* StreakFreezeModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreakFreezeModalViewController.swift; sourceTree = "<group>"; };
E9A3434F29113FA400E0C0A4 /* StepQuizHintReactionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizHintReactionButtonView.swift; sourceTree = "<group>"; };
E9A6250C28ABAE30009423EE /* OnboardingAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAssembly.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1420,6 +1432,7 @@
2C1860FB2923C540007D4EBF /* AppFeatureStateKsExtensions.swift */,
2C93C2D7292EBBB5004D1861 /* AuthSocialFeatureStateKsExtensions.swift */,
2C8CD9AD2994EFC5008DC09D /* DebugFeatureViewStateKsExtensions.swift */,
E9A1DA6F2ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift */,
2CC7833D295DAE3E00A867CD /* OnboardingFeatureStateKsExtensions.swift */,
E9B55A5429C8A03E0066900E /* ProblemsLimitFeatureViewStateKsExtensions.swift */,
2C0DFB992923BB4300D30921 /* ProfileFeatureStateKsExtensions.swift */,
Expand Down Expand Up @@ -1461,6 +1474,7 @@
2C725B6428091ACF00A49043 /* AuthCredentials */,
2C005DD027EF5C4800DC6503 /* AuthSocial */,
2CA5F8EA2994C3880013B854 /* Debug */,
E9A1DA642ACFF10D006A9D4B /* FirstProblemOnboarding */,
2C5F4A582971C6C500677530 /* GamificationToolbar */,
2C963BC32812D16C0036DD53 /* Home */,
E9B641FE2A6BCBCA001A9653 /* NextLearningActivity */,
Expand Down Expand Up @@ -3361,6 +3375,26 @@
path = Views;
sourceTree = "<group>";
};
E9A1DA642ACFF10D006A9D4B /* FirstProblemOnboarding */ = {
isa = PBXGroup;
children = (
E9A1DA6B2ACFF38C006A9D4B /* FirstProblemOnboardingAssembly.swift */,
E9A1DA692ACFF31D006A9D4B /* FirstProblemOnboardingOutputProtocol.swift */,
E9A1DA6D2ACFF428006A9D4B /* FirstProblemOnboardingViewModel.swift */,
E9A1DA652ACFF11B006A9D4B /* Views */,
);
path = FirstProblemOnboarding;
sourceTree = "<group>";
};
E9A1DA652ACFF11B006A9D4B /* Views */ = {
isa = PBXGroup;
children = (
E9A1DA712AD00D41006A9D4B /* FirstProblemOnboardingContentView.swift */,
E9A1DA662ACFF180006A9D4B /* FirstProblemOnboardingView.swift */,
);
path = Views;
sourceTree = "<group>";
};
E9A3434E29113F8A00E0C0A4 /* Views */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4028,6 +4062,7 @@
2CF2DA3A27EC5B2D0055426D /* Assembly.swift in Sources */,
2C27C77C28772F8A006A641A /* ImageDecoders+SVG.swift in Sources */,
2CEB50D0288AADA40044F9AB /* StepQuizCodeFullScreenDetailsView.swift in Sources */,
E9A1DA702ACFF86B006A9D4B /* FirstProblemOnboardingFeatureViewStateKsExtensions.swift in Sources */,
2C8E4FB42848CB980011ADFA /* StepQuizTableSelectColumnsColumnView.swift in Sources */,
2CEB33772949930B00B9E437 /* StepQuizSQLSkeletonView.swift in Sources */,
2CB45762288EC29D007C2D77 /* StepQuizActionButtons.swift in Sources */,
Expand Down Expand Up @@ -4076,6 +4111,7 @@
2C336D102865BF4200C91342 /* CodeEditorThemeService.swift in Sources */,
2C46D0632807E4C100B3636E /* TextFieldWrapper.swift in Sources */,
2C08A939287866A200F1DCA2 /* LazyAvatarView.swift in Sources */,
E9A1DA6A2ACFF31D006A9D4B /* FirstProblemOnboardingOutputProtocol.swift in Sources */,
2CAFD38F27FC517D00F88B0B /* ColorPalette.swift in Sources */,
2C05AC642A0EDFD80039C7EF /* ProjectSelectionListGridCellBadgesView.swift in Sources */,
2C93AF2329B34F66004639E0 /* StepQuizPyCharmView.swift in Sources */,
Expand Down Expand Up @@ -4143,6 +4179,8 @@
2CCCA39B2862E3BB00D98089 /* StepQuizStringDataType.swift in Sources */,
E99B21852887E9B0006A6154 /* StepQuizMatchingSkeletonView.swift in Sources */,
E9C3506D2886B0FE0080D277 /* MainBundleInfo.swift in Sources */,
E9A1DA6C2ACFF38C006A9D4B /* FirstProblemOnboardingAssembly.swift in Sources */,
E9A1DA672ACFF180006A9D4B /* FirstProblemOnboardingView.swift in Sources */,
2CAE8D0528055D8300E6C83D /* StepActionButton.swift in Sources */,
2C45E7BD2A0FD9D600DFF32D /* ProjectSelectionListGridCellProjectLevelView.swift in Sources */,
2C37960F2876F36F00C197E2 /* ProfileViewData.swift in Sources */,
Expand Down Expand Up @@ -4242,6 +4280,7 @@
E9AD65AA292DC0BE00E574F0 /* TopicsRepetitionsFeatureStateKsExtensions.swift in Sources */,
2C23C00D2879F2290083709F /* StreakView.swift in Sources */,
2C20FBCB284F7A45006D879E /* ProcessedContentView.swift in Sources */,
E9A1DA6E2ACFF428006A9D4B /* FirstProblemOnboardingViewModel.swift in Sources */,
2C1F587B280D2E5100372A37 /* URLExtensions.swift in Sources */,
2C7036E82943A2A800775E87 /* ProblemOfDaySkeletonView.swift in Sources */,
2C6672062A527C0D0040EA2F /* ProgressScreenSectionTitleView.swift in Sources */,
Expand Down Expand Up @@ -4362,6 +4401,7 @@
E9E964872A0B8D8200841DF6 /* StepQuizProblemsLimitView.swift in Sources */,
2C8CD9AE2994EFC5008DC09D /* DebugFeatureViewStateKsExtensions.swift in Sources */,
2C967432288824370091B6C9 /* StepQuizCodeViewModel.swift in Sources */,
E9A1DA722AD00D41006A9D4B /* FirstProblemOnboardingContentView.swift in Sources */,
2CF72AA5284775BF00E1C192 /* StepQuizTableView.swift in Sources */,
D64BC006B49702DFDA107FA5 /* TrackSelectionDetailsAssembly.swift in Sources */,
E9D537D42A71393A00F21828 /* ProfileBadgesGridView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "first-problem-onboarding-existing-user-illustration.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "first-problem-onboarding-new-user-illustration.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
import shared

extension FirstProblemOnboardingFeatureViewStateKs: Equatable {
public static func == (
lhs: FirstProblemOnboardingFeatureViewStateKs,
rhs: FirstProblemOnboardingFeatureViewStateKs
) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle):
return true
case (.loading, .loading):
return true
case (.error, .error):
return true
case (.content(let lhsData), .content(let rhsData)):
return lhsData.isEqual(rhsData)
case (.content, .idle):
return false
case (.content, .loading):
return false
case (.content, .error):
return false
case (.error, .idle):
return false
case (.error, .loading):
return false
case (.error, .content):
return false
case (.loading, .idle):
return false
case (.loading, .error):
return false
case (.loading, .content):
return false
case (.idle, .loading):
return false
case (.idle, .error):
return false
case (.idle, .content):
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,12 @@ enum Strings {
static let buttonSecondary = sharedStrings.notifications_onboarding_button_later.localized()
}

// MARK: - FirstProblemOnboarding -

enum FirstProblemOnboarding {
static let networkError = sharedStrings.first_problem_onboarding_network_error.localized()
}

// MARK: - ProjectSelectionList -

enum ProjectSelectionList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ extension AppViewModel: NotificationsOnboardingOutputProtocol {
}
}

// MARK: - AppViewModel: FirstProblemOnboardingOutputProtocol -

extension AppViewModel: FirstProblemOnboardingOutputProtocol {
func handleFirstProblemOnboardingCompleted(stepRoute: StepRoute?) {
onNewMessage(AppFeatureMessageFirstProblemOnboardingCompleted(firstProblemStepRoute: stepRoute))
}
}

// MARK: - AppViewModel: AppTabBarControllerDelegate -

extension AppViewModel: AppTabBarControllerDelegate {
Expand All @@ -149,6 +157,13 @@ private extension AppViewModel {
func subscribeForNotifications() {
let notificationCenter = NotificationCenter.default

notificationCenter.addObserver(
self,
selector: #selector(handleTrackSelectionDetailsDidRequestNavigateToFirstProblemOnboarding),
name: .trackSelectionDetailsDidRequestNavigateToFirstProblemOnboarding,
object: nil
)

notificationCenter.addObserver(
self,
selector: #selector(handleProjectSelectionDetailsDidRequestNavigateToHomeAsNewRootScreen),
Expand Down Expand Up @@ -176,6 +191,11 @@ private extension AppViewModel {
onViewAction?(AppFeatureActionViewActionNavigateToHomeScreen())
}

@objc
func handleTrackSelectionDetailsDidRequestNavigateToFirstProblemOnboarding() {
onViewAction?(AppFeatureActionViewActionNavigateToFirstProblemOnBoardingScreen(isNewUserMode: true))
}

@objc
func handleRemoteNotificationClicked(notification: Foundation.Notification) {
let key = NotificationsService.PayloadKey.pushNotificationData.rawValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,30 @@ extension AppViewController: AppViewControllerProtocol {
case .onboardingScreen:
return UIHostingController(rootView: OnboardingAssembly(output: viewModel).makeModule())
case .homeScreen:
let controller = AppTabBarController()
controller.appTabBarControllerDelegate = viewModel
return controller
return AppTabBarController(initialTab: .home, appTabBarControllerDelegate: viewModel)
case .studyPlan:
return AppTabBarController(initialTab: .studyPlan, appTabBarControllerDelegate: viewModel)
case .homeScreenWithStep(let navigateToHomeScreenWithStepViewAction):
let tabBarController = AppTabBarController(
initialTab: .home,
appTabBarControllerDelegate: viewModel
)

if !tabBarController.isViewLoaded {
_ = tabBarController.view
}

DispatchQueue.main.async {
let index = tabBarController.selectedIndex
guard let navigationController = tabBarController.children[index] as? UINavigationController else {
return assertionFailure("Expected UINavigationController")
}

let stepAssembly = StepAssembly(stepRoute: navigateToHomeScreenWithStepViewAction.stepRoute)
navigationController.pushViewController(stepAssembly.makeModule(), animated: false)
}

return tabBarController
case .authScreen(let data):
let assembly = AuthSocialAssembly(isInSignUpMode: data.isInSignUpMode, output: viewModel)
return UIHostingController(rootView: assembly.makeModule())
Expand All @@ -115,9 +136,9 @@ extension AppViewController: AppViewControllerProtocol {
case .notificationOnBoardingScreen:
let assembly = NotificationsOnboardingAssembly(output: viewModel)
return assembly.makeModule()
case .firstProblemOnBoardingScreen, .homeScreenWithStep, .studyPlan:
#warning("TODO")
exit(0)
case .firstProblemOnBoardingScreen(let data):
let assembly = FirstProblemOnboardingAssembly(isNewUserMode: data.isNewUserMode, output: viewModel)
return assembly.makeModule()
}
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,36 @@ protocol AppTabBarControllerDelegate: AnyObject {
}

final class AppTabBarController: UITabBarController {
weak var appTabBarControllerDelegate: AppTabBarControllerDelegate?
private weak var appTabBarControllerDelegate: AppTabBarControllerDelegate?

private var currentTabItem = AppTabItem.home
private var currentTabItem: AppTabItem

init(
initialTab: AppTabItem = .home,
appTabBarControllerDelegate: AppTabBarControllerDelegate?
) {
self.currentTabItem = initialTab
self.appTabBarControllerDelegate = appTabBarControllerDelegate
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
delegate = self

setViewControllers()

if let initialTabIndex = AppTabItem.availableItems.firstIndex(of: currentTabItem) {
selectedIndex = initialTabIndex
} else {
selectedIndex = 0
}

delegate = self
}

private func setViewControllers() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import shared
import SwiftUI

final class FirstProblemOnboardingAssembly: UIKitAssembly {
private let isNewUserMode: Bool

private weak var moduleOutput: FirstProblemOnboardingOutputProtocol?

init(
isNewUserMode: Bool,
output: FirstProblemOnboardingOutputProtocol?
) {
self.isNewUserMode = isNewUserMode
self.moduleOutput = output
}

func makeModule() -> UIViewController {
let firstProblemOnboardingComponent = AppGraphBridge.sharedAppGraph.buildFirstProblemOnboardingComponent()

let viewModel = FirstProblemOnboardingViewModel(
feature: firstProblemOnboardingComponent.firstProblemOnboardingFeature(isNewUserMode: isNewUserMode)
)
viewModel.moduleOutput = moduleOutput

let firstProblemOnboardingView = FirstProblemOnboardingView(
viewModel: viewModel
)

let hostingController = StyledHostingController(
rootView: firstProblemOnboardingView
)
hostingController.navigationItem.largeTitleDisplayMode = .never

return hostingController
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation
import shared

protocol FirstProblemOnboardingOutputProtocol: AnyObject {
func handleFirstProblemOnboardingCompleted(stepRoute: StepRoute?)
}
Loading

0 comments on commit 1e44ea9

Please sign in to comment.