Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALTAPPS-634: iOS stage implementation unsupported bottom sheet #378

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
2C5B2A23286596400097B270 /* UITableView+RegisterReusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B2A22286596400097B270 /* UITableView+RegisterReusable.swift */; };
2C5B2A25286596A80097B270 /* UICollectionView+RegisterReusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B2A24286596A80097B270 /* UICollectionView+RegisterReusable.swift */; };
2C5B2A2828659AD80097B270 /* CodeElementSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B2A2728659AD80097B270 /* CodeElementSize.swift */; };
2C5B9EB329B7A016007E4943 /* StageImplementUnsupportedModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B9EB229B7A016007E4943 /* StageImplementUnsupportedModalView.swift */; };
2C5CBBE12948EBEA00113007 /* StepQuizSQLViewDataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CBBE02948EBEA00113007 /* StepQuizSQLViewDataMapper.swift */; };
2C5CBBE32948F4B600113007 /* StepQuizSQLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CBBE22948F4B600113007 /* StepQuizSQLViewModel.swift */; };
2C5CBBE52948FA7400113007 /* StepQuizSQLAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CBBE42948FA7400113007 /* StepQuizSQLAssembly.swift */; };
Expand Down Expand Up @@ -347,6 +348,7 @@
E900D10128434D0400A77BBC /* StepQuizSortingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E900D10028434D0400A77BBC /* StepQuizSortingView.swift */; };
E900D10328434E0D00A77BBC /* StepQuizSortingItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E900D10228434E0D00A77BBC /* StepQuizSortingItemView.swift */; };
E900D1052843573A00A77BBC /* StepQuizSortingIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E900D1042843573A00A77BBC /* StepQuizSortingIcon.swift */; };
E90DF95929AE288600EC40DA /* StageImplementUnsupportedModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90DF95829AE288600EC40DA /* StageImplementUnsupportedModalViewController.swift */; };
E9101713283296F3002E70F5 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9101712283296F3002E70F5 /* RadioButton.swift */; };
E91017152832975C002E70F5 /* CheckboxButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91017142832975C002E70F5 /* CheckboxButton.swift */; };
E910171728329808002E70F5 /* StepQuizChoiceElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E910171628329808002E70F5 /* StepQuizChoiceElementView.swift */; };
Expand Down Expand Up @@ -588,6 +590,7 @@
2C5B2A22286596400097B270 /* UITableView+RegisterReusable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+RegisterReusable.swift"; sourceTree = "<group>"; };
2C5B2A24286596A80097B270 /* UICollectionView+RegisterReusable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+RegisterReusable.swift"; sourceTree = "<group>"; };
2C5B2A2728659AD80097B270 /* CodeElementSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeElementSize.swift; sourceTree = "<group>"; };
2C5B9EB229B7A016007E4943 /* StageImplementUnsupportedModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageImplementUnsupportedModalView.swift; sourceTree = "<group>"; };
2C5CBBE02948EBEA00113007 /* StepQuizSQLViewDataMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSQLViewDataMapper.swift; sourceTree = "<group>"; };
2C5CBBE22948F4B600113007 /* StepQuizSQLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSQLViewModel.swift; sourceTree = "<group>"; };
2C5CBBE42948FA7400113007 /* StepQuizSQLAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSQLAssembly.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -798,6 +801,7 @@
E900D10028434D0400A77BBC /* StepQuizSortingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSortingView.swift; sourceTree = "<group>"; };
E900D10228434E0D00A77BBC /* StepQuizSortingItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSortingItemView.swift; sourceTree = "<group>"; };
E900D1042843573A00A77BBC /* StepQuizSortingIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizSortingIcon.swift; sourceTree = "<group>"; };
E90DF95829AE288600EC40DA /* StageImplementUnsupportedModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageImplementUnsupportedModalViewController.swift; sourceTree = "<group>"; };
E9101712283296F3002E70F5 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
E91017142832975C002E70F5 /* CheckboxButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxButton.swift; sourceTree = "<group>"; };
E910171628329808002E70F5 /* StepQuizChoiceElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizChoiceElementView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1289,6 +1293,23 @@
path = Nuke;
sourceTree = "<group>";
};
2C2C8ABE29B7994A00615893 /* Modals */ = {
isa = PBXGroup;
children = (
2C5B9EB429B7A9CD007E4943 /* UnsupportedModal */,
);
path = Modals;
sourceTree = "<group>";
};
2C2C8ABF29B7995900615893 /* Views */ = {
isa = PBXGroup;
children = (
2C9E5E8729B215D7003AEC16 /* StageImplementView.swift */,
2C2C8ABE29B7994A00615893 /* Modals */,
);
path = Views;
sourceTree = "<group>";
};
2C2FF9CA285073340069C092 /* UIKit */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1496,6 +1517,15 @@
path = CodeTextView;
sourceTree = "<group>";
};
2C5B9EB429B7A9CD007E4943 /* UnsupportedModal */ = {
isa = PBXGroup;
children = (
2C5B9EB229B7A016007E4943 /* StageImplementUnsupportedModalView.swift */,
E90DF95829AE288600EC40DA /* StageImplementUnsupportedModalViewController.swift */,
);
path = UnsupportedModal;
sourceTree = "<group>";
};
2C5CBBDE2948EB9400113007 /* StepQuizSQL */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1833,8 +1863,8 @@
isa = PBXGroup;
children = (
2C9E5E8329B2156D003AEC16 /* StageImplementAssembly.swift */,
2C9E5E8729B215D7003AEC16 /* StageImplementView.swift */,
2C9E5E8529B215CA003AEC16 /* StageImplementViewModel.swift */,
2C2C8ABF29B7995900615893 /* Views */,
);
path = StageImplement;
sourceTree = "<group>";
Expand Down Expand Up @@ -2934,6 +2964,7 @@
2CD3652828797D3600D61855 /* Formatter.swift in Sources */,
2C0DB90328643C47001EA35E /* CodeTextView.swift in Sources */,
2C96743F288831BB0091B6C9 /* StepQuizCodeSamplesView.swift in Sources */,
2C5B9EB329B7A016007E4943 /* StageImplementUnsupportedModalView.swift in Sources */,
2CCCA3992862D58E00D98089 /* StepQuizStringView.swift in Sources */,
2C7036EA2943A34000775E87 /* TopicsRepetitionsCardSkeletonView.swift in Sources */,
2CE31F4B27F1E070008EEE66 /* AppViewModel.swift in Sources */,
Expand Down Expand Up @@ -3257,6 +3288,7 @@
2C079685285CFFEE00EE0487 /* StepQuizSortingViewModel.swift in Sources */,
E94A6D0B28748C3E005F9ACC /* StreakIcon.swift in Sources */,
2CD3652C287987FA00D61855 /* ProfileSocialAccount.swift in Sources */,
E90DF95929AE288600EC40DA /* StageImplementUnsupportedModalViewController.swift in Sources */,
E9F655CA2875914200291143 /* StreakCardView.swift in Sources */,
2C9ECBA3284736090015CFD2 /* StepViewDataMapper.swift in Sources */,
2CAE8CF2280525C900E6C83D /* StepView.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,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "study-plan-ide-required-modal-monitor-light.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
Expand Up @@ -4,16 +4,21 @@ import PanModal
final class PanModalPresenter: ObservableObject {
private let sourcelessRouter: SourcelessRouter

init(sourcelessRouter: SourcelessRouter = SourcelessRouter()) {
weak var rootViewController: UIViewController?

init(sourcelessRouter: SourcelessRouter = SourcelessRouter(), rootViewController: UIViewController? = nil) {
self.sourcelessRouter = sourcelessRouter
self.rootViewController = rootViewController
}

func presentPanModal(_ panModal: PanModalPresentableViewController) {
guard let currentPresentedViewController = sourcelessRouter.currentPresentedViewController() else {
return
let presentationViewController = rootViewController ?? sourcelessRouter.currentPresentedViewController()

guard let presentationViewController else {
return assertionFailure("PanModalPresenter :: presentationViewController is nil")
}

currentPresentedViewController.presentIfPanModalWithCustomModalPresentationStyle(panModal)
presentationViewController.presentIfPanModalWithCustomModalPresentationStyle(panModal)
}

@discardableResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ enum Images {
}
}

// MARK: - StageImplement -

enum StageImplement {
enum UnsupportedModal {
static let icon = "stage-implement-unsupported-modal-icon"
}
}

// MARK: - Track -

enum Track {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ enum Strings {
static let placeholder = sharedStrings.step_quiz_text_field_hint.localized()
}

// MARK: - StageImplement -

enum StageImplement {
enum UnsupportedModal {
static let title = sharedStrings.stage_implement_unsupported_modal_title.localized()
static let description = sharedStrings.stage_implement_unsupported_modal_description.localized()
}
}

// MARK: - Home -

enum Home {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ final class StageImplementAssembly: UIKitAssembly {
)

let stackRouter = SwiftUIStackRouter()
let panModalPresenter = PanModalPresenter()

let stageImplementView = StageImplementView(
viewModel: stageImplementViewModel,
stackRouter: stackRouter
stackRouter: stackRouter,
panModalPresenter: panModalPresenter
)

let hostingController = StyledHostingController(
rootView: stageImplementView,
appearance: .withoutBackButtonTitle
)

stackRouter.rootViewController = hostingController
panModalPresenter.rootViewController = hostingController

return hostingController
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,25 @@ final class StageImplementViewModel: FeatureViewModel<
onNewMessage(StageImplementFeatureMessageViewedEventMessage())
}
}

// MARK: - StageImplementViewModel: StageImplementUnsupportedModalViewControllerDelegate -

extension StageImplementViewModel: StageImplementUnsupportedModalViewControllerDelegate {
func stageImplementUnsupportedModalViewControllerViewControllerDidAppear(
_ viewController: StageImplementUnsupportedModalViewController
) {
onNewMessage(StageImplementFeatureMessageUnsupportedModalShownEventMessage())
}

func stageImplementUnsupportedModalViewControllerDidDisappear(
_ viewController: StageImplementUnsupportedModalViewController
) {
onNewMessage(StageImplementFeatureMessageUnsupportedModalHiddenEventMessage())
}

func stageImplementUnsupportedModalViewControllerDidTapGoToHomescreenButton(
_ viewController: StageImplementUnsupportedModalViewController
) {
onNewMessage(StageImplementFeatureMessageUnsupportedModalGoToHomeScreenClicked())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import SnapKit
import UIKit

extension StageImplementUnsupportedModalView {
struct Appearance {
let contentStackViewSpacing: CGFloat = LayoutInsets.defaultInset * 2
let contentStackViewInsets = LayoutInsets(
horizontal: LayoutInsets.defaultInset,
vertical: LayoutInsets.defaultInset
)

let imageViewImageName = Images.StageImplement.UnsupportedModal.icon
let imageViewSizeRatio = CGSize(width: 0.63, height: 0.20)

let textContainerStackViewSpacing: CGFloat = LayoutInsets.defaultInset

let titleLabelText = Strings.StageImplement.UnsupportedModal.title
let titleLabelTextFont = UIFont.preferredFont(
forTextStyle: .title2,
compatibleWith: .init(legibilityWeight: .bold)
)
let titleLabelTextColor = UIColor.primaryText

let descriptionLabelText = Strings.StageImplement.UnsupportedModal.description
let descriptionLabelFont = UIFont.preferredFont(forTextStyle: .body)
let descriptionLabelTextColor = UIColor.primaryText

let callToActionButtonTitle = Strings.General.goToHomescreen
let callToActionButtonHeight: CGFloat = 44

let backgroundColor = UIColor.systemBackground
}
}

final class StageImplementUnsupportedModalView: UIView {
let appearance: Appearance

private lazy var contentStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = appearance.contentStackViewSpacing
stackView.alignment = .leading
stackView.distribution = .fill
return stackView
}()

private lazy var textContainerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = appearance.textContainerStackViewSpacing
stackView.alignment = .leading
stackView.distribution = .fill
return stackView
}()

private lazy var imageView: UIImageView = {
let image = UIImage(named: appearance.imageViewImageName)?.withRenderingMode(.alwaysOriginal)

let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit

return imageView
}()

private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = appearance.titleLabelText
label.font = appearance.titleLabelTextFont
label.textColor = appearance.titleLabelTextColor
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
return label
}()

private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.text = appearance.descriptionLabelText
label.font = appearance.descriptionLabelFont
label.textColor = appearance.descriptionLabelTextColor
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
return label
}()

private lazy var callToActionButton: UIButton = {
let button = UIKitRoundedRectangleButton(style: .violet)
button.setTitle(appearance.callToActionButtonTitle, for: .normal)
button.addTarget(self, action: #selector(callToActionButtonTapped), for: .touchUpInside)
return button
}()

override var intrinsicContentSize: CGSize {
let contentStackViewSize = contentStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

let height = appearance.contentStackViewInsets.top
+ contentStackViewSize.height
+ appearance.contentStackViewInsets.bottom

return CGSize(width: UIView.noIntrinsicMetric, height: height)
}

var onCallToActionButtonTapped: (() -> Void)?

init(
frame: CGRect = .zero,
appearance: Appearance = Appearance()
) {
self.appearance = appearance
super.init(frame: frame)

setupView()
addSubviews()
makeConstraints()
}

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

@objc
private func callToActionButtonTapped() {
onCallToActionButtonTapped?()
}
}

extension StageImplementUnsupportedModalView: ProgrammaticallyInitializableViewProtocol {
func setupView() {
backgroundColor = appearance.backgroundColor
}

func addSubviews() {
addSubview(contentStackView)

contentStackView.addArrangedSubview(imageView)
contentStackView.addArrangedSubview(textContainerStackView)

textContainerStackView.addArrangedSubview(titleLabel)
textContainerStackView.addArrangedSubview(descriptionLabel)

contentStackView.addArrangedSubview(callToActionButton)
}

func makeConstraints() {
contentStackView.translatesAutoresizingMaskIntoConstraints = false
contentStackView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(appearance.contentStackViewInsets.top)
make.leading.equalTo(safeAreaLayoutGuide).offset(appearance.contentStackViewInsets.leading)
make.bottom.lessThanOrEqualToSuperview().offset(-appearance.contentStackViewInsets.bottom)
make.trailing.equalTo(safeAreaLayoutGuide).offset(-appearance.contentStackViewInsets.trailing)
}

imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.snp.makeConstraints { make in
make.width.equalTo(self).multipliedBy(appearance.imageViewSizeRatio.width)
make.height.equalTo(self).multipliedBy(appearance.imageViewSizeRatio.height)
}

callToActionButton.translatesAutoresizingMaskIntoConstraints = false
callToActionButton.snp.makeConstraints { make in
make.width.equalToSuperview()
make.height.equalTo(appearance.callToActionButtonHeight)
}
}
}
Loading