diff --git a/Source/CourseAnnouncementsViewController.swift b/Source/CourseAnnouncementsViewController.swift index 780f83fb79..b5e2c33a66 100644 --- a/Source/CourseAnnouncementsViewController.swift +++ b/Source/CourseAnnouncementsViewController.swift @@ -18,7 +18,7 @@ private func announcementsDeserializer(response: HTTPURLResponse, json: JSON) -> } } -class CourseAnnouncementsViewController: OfflineSupportViewController, LoadStateViewReloadSupport, InterfaceOrientationOverriding { +class CourseAnnouncementsViewController: OfflineSupportViewController, LoadStateViewReloadSupport, InterfaceOrientationOverriding, ScrollableDelegateProvider { typealias Environment = OEXAnalyticsProvider & OEXConfigProvider & DataManagerProvider & NetworkManagerProvider & OEXRouterProvider & OEXInterfaceProvider & ReachabilityProvider & OEXSessionProvider & OEXStylesProvider @@ -32,6 +32,9 @@ class CourseAnnouncementsViewController: OfflineSupportViewController, LoadState private let fontStyle = OEXTextStyle(weight : .normal, size: .base, color: OEXStyles.shared().neutralBlack()) private let switchStyle = OEXStyles.shared().standardSwitchStyle() + weak var scrollableDelegate: ScrollableDelegate? + private var scrollByDragging = false + @objc init(environment: Environment, courseID: String) { self.courseID = courseID self.environment = environment @@ -54,6 +57,7 @@ class CourseAnnouncementsViewController: OfflineSupportViewController, LoadState webView.backgroundColor = OEXStyles.shared().standardBackgroundColor() webView.isOpaque = false webView.navigationDelegate = self + webView.scrollView.delegate = self loadController.setupInController(controller: self, contentView: webView) announcementsLoader.listen(self) {[weak self] in @@ -174,3 +178,19 @@ extension CourseAnnouncementsViewController: WKNavigationDelegate { loadController.state = LoadState.failed(error: error as NSError) } } + +extension CourseAnnouncementsViewController: UIScrollViewDelegate { + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollByDragging = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollByDragging { + scrollableDelegate?.scrollViewDidScroll(scrollView: scrollView) + } + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + scrollByDragging = false + } +} diff --git a/Source/CourseDashboardAccessErrorCell.swift b/Source/CourseDashboardAccessErrorView.swift similarity index 92% rename from Source/CourseDashboardAccessErrorCell.swift rename to Source/CourseDashboardAccessErrorView.swift index e3eb5987d1..f08a697c33 100644 --- a/Source/CourseDashboardAccessErrorCell.swift +++ b/Source/CourseDashboardAccessErrorView.swift @@ -1,5 +1,5 @@ // -// CourseDashboardAccessErrorCell.swift +// CourseDashboardAccessErrorView.swift // edX // // Created by Saeed Bashir on 12/2/22. @@ -8,21 +8,22 @@ import Foundation -protocol CourseDashboardAccessErrorCellDelegate: AnyObject { +protocol CourseDashboardAccessErrorViewDelegate: AnyObject { func findCourseAction() func upgradeCourseAction(course: OEXCourse, price: String?, completion: @escaping ((Bool)->())) - func coursePrice(cell: CourseDashboardAccessErrorCell, price: String?, elapsedTime: Int) + func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, elapsedTime: Int) } -class CourseDashboardAccessErrorCell: UITableViewCell { - static let identifier = "CourseDashboardAccessErrorCell" +class CourseDashboardAccessErrorView: UIView { typealias Environment = OEXConfigProvider & ServerConfigProvider - weak var delegate: CourseDashboardAccessErrorCellDelegate? + weak var delegate: CourseDashboardAccessErrorViewDelegate? private lazy var infoMessagesView = ValuePropMessagesView() private var environment: Environment? + private lazy var contentView = UIView() + private lazy var upgradeButton: CourseUpgradeButtonView = { let upgradeButton = CourseUpgradeButtonView() upgradeButton.tapAction = { [weak self] in @@ -37,20 +38,20 @@ class CourseDashboardAccessErrorCell: UITableViewCell { private var titleLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 - label.accessibilityIdentifier = "CourseDashboardAccessErrorCell:title-label" + label.accessibilityIdentifier = "CourseDashboardAccessErrorView:title-label" return label }() private var infoLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 - label.accessibilityIdentifier = "CourseDashboardAccessErrorCell:info-label" + label.accessibilityIdentifier = "CourseDashboardAccessErrorView:info-label" return label }() private lazy var findCourseButton: UIButton = { let button = UIButton(type: .system) - button.accessibilityIdentifier = "CourseDashboardAccessErrorCell:findcourse-button" + button.accessibilityIdentifier = "CourseDashboardAccessErrorView:findcourse-button" button.oex_addAction({ [weak self] _ in self?.delegate?.findCourseAction() }, for: .touchUpInside) @@ -59,16 +60,14 @@ class CourseDashboardAccessErrorCell: UITableViewCell { private lazy var hiddenView: UIView = { let view = UIView() - view.accessibilityIdentifier = "CourseDashboardAccessErrorCell:hidden-view" + view.accessibilityIdentifier = "CourseDashboardAccessErrorView:hidden-view" view.backgroundColor = .clear return view }() - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - selectionStyle = .none - accessibilityIdentifier = "CourseDashboardAccessErrorCell:view" + init() { + super.init(frame: .zero) } private var course: OEXCourse? @@ -100,6 +99,8 @@ class CourseDashboardAccessErrorCell: UITableViewCell { } private func configureViews() { + accessibilityIdentifier = "CourseDashboardAccessErrorView:view" + addSubview(contentView) contentView.addSubview(titleLabel) contentView.addSubview(infoLabel) contentView.addSubview(infoMessagesView) @@ -113,6 +114,10 @@ class CourseDashboardAccessErrorCell: UITableViewCell { } private func setConstraints(showValueProp: Bool, showUpgradeButton: Bool) { + contentView.snp.remakeConstraints { make in + make.edges.equalTo(self) + } + titleLabel.snp.remakeConstraints { make in make.top.equalTo(contentView).offset(StandardVerticalMargin * 2) make.leading.equalTo(contentView).offset(StandardHorizontalMargin) diff --git a/Source/CourseDashboardErrorViewCell.swift b/Source/CourseDashboardErrorView.swift similarity index 91% rename from Source/CourseDashboardErrorViewCell.swift rename to Source/CourseDashboardErrorView.swift index 3a1bf48796..482a50d739 100644 --- a/Source/CourseDashboardErrorViewCell.swift +++ b/Source/CourseDashboardErrorView.swift @@ -1,5 +1,5 @@ // -// CourseDashboardErrorViewCell.swift +// CourseDashboardErrorView.swift // edX // // Created by Saeed Bashir on 11/29/22. @@ -8,16 +8,15 @@ import Foundation -class CourseDashboardErrorViewCell: UITableViewCell { - static let identifier = "CourseDashboardErrorView" - +class CourseDashboardErrorView: UIView { var myCoursesAction: (() -> Void)? + private let contentView = UIView() private let containerView = UIView() private let bottomContainer = UIView() private let errorLabel: UILabel = { let label = UILabel() - label.accessibilityIdentifier = "CourseDashboardErrorViewCell:error-label" + label.accessibilityIdentifier = "CourseDashboardErrorView:error-label" label.numberOfLines = 0 let style = OEXMutableTextStyle(weight: .bold, size: .xxLarge, color: OEXStyles.shared().neutralBlackT()) style.alignment = .center @@ -29,13 +28,13 @@ class CourseDashboardErrorViewCell: UITableViewCell { private lazy var errorImageView: UIImageView = { guard let image = UIImage(named: "dashboard_error_image") else { return UIImageView() } let imageView = UIImageView(image: image) - imageView.accessibilityIdentifier = "CourseDashboardErrorViewCell:error-imageView" + imageView.accessibilityIdentifier = "CourseDashboardErrorView:error-imageView" return imageView }() private lazy var gotoMyCoursesButton: UIButton = { let button = UIButton(type: .system) - button.accessibilityIdentifier = "CourseDashboardErrorViewCell:gotocourses-button" + button.accessibilityIdentifier = "CourseDashboardErrorView:gotocourses-button" button.backgroundColor = OEXStyles.shared().secondaryBaseColor() button.oex_addAction({ [weak self] _ in self?.myCoursesAction?() @@ -47,19 +46,13 @@ class CourseDashboardErrorViewCell: UITableViewCell { return button }() - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - selectionStyle = .none - + init() { + super.init(frame: .zero) addSubViews() setAccessibilityIdentifiers() setConstraints() } - override func prepareForReuse() { - setConstraints() - } - override func layoutSubviews() { super.layoutSubviews() containerView.addShadow(offset: CGSize(width: 0, height: 2), color: OEXStyles.shared().primaryDarkColor(), radius: 2, opacity: 0.35, cornerRadius: 6) @@ -76,9 +69,8 @@ class CourseDashboardErrorViewCell: UITableViewCell { private func addSubViews() { backgroundColor = OEXStyles.shared().neutralWhiteT() - + addSubview(contentView) contentView.addSubview(containerView) - containerView.addSubview(errorImageView) containerView.addSubview(bottomContainer) @@ -89,12 +81,16 @@ class CourseDashboardErrorViewCell: UITableViewCell { } private func setAccessibilityIdentifiers() { - accessibilityIdentifier = "CourseDashboardErrorViewCell:view" - containerView.accessibilityIdentifier = "CourseDashboardErrorViewCell:container-view" - bottomContainer.accessibilityIdentifier = "CourseDashboardErrorViewCell:bottom-container-view" + accessibilityIdentifier = "CourseDashboardErrorView:view" + containerView.accessibilityIdentifier = "CourseDashboardErrorView:container-view" + bottomContainer.accessibilityIdentifier = "CourseDashboardErrorView:bottom-container-view" } private func addPortraitConstraints() { + contentView.snp.remakeConstraints { make in + make.edges.equalTo(self) + } + containerView.snp.remakeConstraints { make in make.top.equalTo(contentView).offset(StandardVerticalMargin * 2) make.leading.equalTo(contentView).offset(StandardHorizontalMargin) diff --git a/Source/CourseDashboardHeaderView.swift b/Source/CourseDashboardHeaderView.swift index 92c320e524..210265c96a 100644 --- a/Source/CourseDashboardHeaderView.swift +++ b/Source/CourseDashboardHeaderView.swift @@ -15,7 +15,13 @@ protocol CourseDashboardHeaderViewDelegate: AnyObject { func didTapTabbarItem(at position: Int, tabbarItem: TabBarItem) } -class CourseDashboardHeaderView: UITableViewHeaderFooterView { +enum CourseDashboardHeaderViewState { + case animating + case expanded + case collapsed +} + +class CourseDashboardHeaderView: UIView { typealias Environment = OEXAnalyticsProvider & DataManagerProvider & OEXInterfaceProvider & NetworkManagerProvider & ReachabilityProvider & OEXRouterProvider & OEXConfigProvider & OEXStylesProvider & ServerConfigProvider & OEXSessionProvider & RemoteConfigProvider @@ -27,6 +33,7 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { private lazy var containerView = UIView() private lazy var courseInfoContainerView = UIView() + private var bottomContainer = UIView() private lazy var orgLabel: UILabel = { let label = UILabel() @@ -34,6 +41,13 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { return label }() + private lazy var courseTitleLabel: UILabel = { + let label = UILabel() + label.accessibilityIdentifier = "CourseDashboardHeaderView:course-label-header" + label.backgroundColor = .clear + return label + }() + private lazy var courseTitle: UITextView = { let textView = UITextView() textView.accessibilityIdentifier = "CourseDashboardHeaderView:course-label" @@ -43,7 +57,7 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { textView.backgroundColor = .clear textView.isScrollEnabled = false let padding = textView.textContainer.lineFragmentPadding - textView.textContainerInset = UIEdgeInsets(top: 0, left: -padding, bottom: 0, right: -padding) + textView.textContainerInset = UIEdgeInsets(top: 0, left: -padding, bottom: 0, right: -padding) let tapGesture = AttachmentTapGestureRecognizer { [weak self] _ in self?.delegate?.didTapOnShareCourse() @@ -127,6 +141,12 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { return style }() + private lazy var courseTextLabelStyle: OEXMutableTextStyle = { + let style = OEXMutableTextStyle(textStyle: OEXTextStyle(weight: .bold, size: .base, color: environment.styles.neutralWhiteT())) + style.lineBreakMode = .byWordWrapping + return style + }() + private lazy var accessTextStyle = OEXTextStyle(weight: .normal, size: .xSmall, color: environment.styles.neutralXLight()) private var canShowValuePropView: Bool { @@ -134,14 +154,14 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { let enrollment = environment.interface?.enrollmentForCourse(withID: course.course_id) else { return false } - if let error = error { - if error.type == .auditExpired || error.type == .isEndDateOld { - return false - } + if let error = error, error.type == .auditExpired || error.type == .isEndDateOld { + return false } return enrollment.type == .audit && environment.serverConfig.valuePropEnabled } + private var showTabbar = false + private let environment: Environment private let course: OEXCourse? private let error: CourseAccessErrorHelper? @@ -150,7 +170,7 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { self.environment = environment self.course = course self.error = error - super.init(reuseIdentifier: nil) + super.init(frame: .zero) addSubViews() addConstraints() @@ -158,6 +178,8 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { } private func configureView() { + courseTitleLabel.attributedText = courseTextLabelStyle.attributedString(withText: course?.name) + let courseTitleText = [ courseTextStyle.attributedString(withText: course?.name), attributedUnicodeSpace, @@ -174,6 +196,7 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { closeButton.tintColor = environment.styles.neutralWhiteT() addSubview(containerView) + containerView.addSubview(courseTitleLabel) containerView.addSubview(closeButton) containerView.addSubview(courseInfoContainerView) containerView.addSubview(tabbarView) @@ -181,9 +204,11 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { courseInfoContainerView.addSubview(orgLabel) courseInfoContainerView.addSubview(courseTitle) courseInfoContainerView.addSubview(accessLabel) + + showCourseTitleHeaderLabel(show: false) } - private func addConstraints(hidetabbar: Bool = true) { + private func addConstraints() { containerView.snp.remakeConstraints { make in make.edges.equalTo(self) } @@ -195,6 +220,13 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { make.width.equalTo(imageSize) } + courseTitleLabel.snp.remakeConstraints { make in + make.top.equalTo(closeButton) + make.centerY.equalTo(closeButton) + make.leading.equalTo(containerView).offset(StandardHorizontalMargin) + make.trailing.equalTo(closeButton.snp.leading).offset(-StandardHorizontalMargin) + } + courseInfoContainerView.snp.remakeConstraints { make in make.top.equalTo(closeButton.snp.bottom) make.leading.equalTo(containerView).offset(StandardHorizontalMargin) @@ -220,7 +252,7 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { make.bottom.equalTo(courseInfoContainerView).inset(StandardVerticalMargin) } - var bottomContainer = courseInfoContainerView + bottomContainer = courseInfoContainerView if canShowValuePropView { containerView.addSubview(valuePropView) @@ -240,17 +272,40 @@ class CourseDashboardHeaderView: UITableViewHeaderFooterView { make.leading.equalTo(containerView) make.trailing.equalTo(containerView) make.bottom.equalTo(containerView) - make.height.equalTo(hidetabbar ? 0 : StandardVerticalMargin * 4.8) + make.height.equalTo(showTabbar ? StandardVerticalMargin * 4.8 : 0) } } func showTabbarView(show: Bool) { - addConstraints(hidetabbar: !show) + showTabbar = show + addConstraints() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func updateHeader(collapse: Bool) { + courseInfoContainerView.alpha = collapse ? 0 : 1 + valuePropView.alpha = collapse ? 0 : (canShowValuePropView ? 1 : 0) + updateTabbarConstraints(collapse: collapse) + } + + func showCourseTitleHeaderLabel(show: Bool) { + courseTitleLabel.alpha = show ? 1 : 0 + } + + func updateTabbarConstraints(collapse: Bool) { + tabbarView.snp.remakeConstraints { make in + make.top.equalTo(collapse ? closeButton.snp.bottom : bottomContainer.snp.bottom).offset(StandardVerticalMargin * 2) + make.leading.equalTo(containerView) + make.trailing.equalTo(containerView) + make.height.equalTo(collapse ? StandardVerticalMargin * 5.5 : showTabbar ? StandardVerticalMargin * 4.8 : 0) + if !collapse { + make.bottom.equalTo(containerView) + } + } + } } extension CourseDashboardHeaderView: CourseDashboardTabbarViewDelegate { diff --git a/Source/CourseDatesViewController.swift b/Source/CourseDatesViewController.swift index 0e922ac008..f1f3fac7f4 100644 --- a/Source/CourseDatesViewController.swift +++ b/Source/CourseDatesViewController.swift @@ -9,7 +9,7 @@ import UIKit import WebKit -class CourseDatesViewController: UIViewController, InterfaceOrientationOverriding { +class CourseDatesViewController: UIViewController, InterfaceOrientationOverriding, ScrollableDelegateProvider { private enum Pacing: String { case user = "self" @@ -115,6 +115,9 @@ class CourseDatesViewController: UIViewController, InterfaceOrientationOverridin private var courseBanner: CourseDateBannerModel? + weak var scrollableDelegate: ScrollableDelegate? + private var scrollByDragging = false + init(environment: Environment, courseID: String) { self.courseID = courseID self.environment = environment @@ -644,6 +647,22 @@ extension CourseDatesViewController: CourseDatesHeaderViewDelegate { } } +extension CourseDatesViewController { + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollByDragging = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollByDragging { + scrollableDelegate?.scrollViewDidScroll(scrollView: scrollView) + } + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + scrollByDragging = false + } +} + // For use in testing only extension CourseDatesViewController { func t_loadData(data: CourseDateModel) { diff --git a/Source/CourseHandoutsViewController.swift b/Source/CourseHandoutsViewController.swift index 5f3880d4d6..89d58b587c 100644 --- a/Source/CourseHandoutsViewController.swift +++ b/Source/CourseHandoutsViewController.swift @@ -9,7 +9,7 @@ import UIKit import WebKit -public class CourseHandoutsViewController: OfflineSupportViewController, LoadStateViewReloadSupport, InterfaceOrientationOverriding { +public class CourseHandoutsViewController: OfflineSupportViewController, LoadStateViewReloadSupport, InterfaceOrientationOverriding, ScrollableDelegateProvider { public typealias Environment = DataManagerProvider & NetworkManagerProvider & ReachabilityProvider & OEXAnalyticsProvider & OEXStylesProvider & OEXConfigProvider @@ -19,6 +19,9 @@ public class CourseHandoutsViewController: OfflineSupportViewController, LoadSta let loadController : LoadStateViewController let handouts : BackedStream = BackedStream() + public weak var scrollableDelegate: ScrollableDelegate? + private var scrollByDragging = false + init(environment : Environment, courseID : String) { self.environment = environment self.courseID = courseID @@ -42,6 +45,8 @@ public class CourseHandoutsViewController: OfflineSupportViewController, LoadSta setConstraints() setStyles() webView.navigationDelegate = self + webView.scrollView.delegate = self + view.backgroundColor = environment.styles.standardBackgroundColor() setAccessibilityIdentifiers() @@ -149,3 +154,19 @@ extension CourseHandoutsViewController: WKNavigationDelegate { } } + +extension CourseHandoutsViewController: UIScrollViewDelegate { + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollByDragging = true + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollByDragging { + scrollableDelegate?.scrollViewDidScroll(scrollView: scrollView) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + scrollByDragging = false + } +} diff --git a/Source/CourseOutlineTableSource.swift b/Source/CourseOutlineTableSource.swift index e973f5424e..1bdec9d4b5 100644 --- a/Source/CourseOutlineTableSource.swift +++ b/Source/CourseOutlineTableSource.swift @@ -22,15 +22,17 @@ protocol CourseOutlineTableControllerDelegate: AnyObject { func resetCourseDate(controller: CourseOutlineTableController) } -class CourseOutlineTableController : UITableViewController, CourseVideoTableViewCellDelegate, CourseSectionTableViewCellDelegate, CourseVideosHeaderViewDelegate, VideoDownloadQualityDelegate { +class CourseOutlineTableController : UITableViewController, CourseVideoTableViewCellDelegate, CourseSectionTableViewCellDelegate, CourseVideosHeaderViewDelegate, VideoDownloadQualityDelegate, ScrollableDelegateProvider { typealias Environment = DataManagerProvider & OEXInterfaceProvider & NetworkManagerProvider & OEXConfigProvider & OEXRouterProvider & OEXAnalyticsProvider & OEXStylesProvider & ServerConfigProvider weak var delegate: CourseOutlineTableControllerDelegate? + private let environment: Environment - let courseQuerier: CourseOutlineQuerier - let courseID: String + private let courseID: String private var courseOutlineMode: CourseOutlineMode + private var courseBlockID: CourseBlockID? + private let courseQuerier: CourseOutlineQuerier private let courseDateBannerView = CourseDateBannerView(frame: .zero) private let courseCard = CourseCardView(frame: .zero) @@ -38,12 +40,15 @@ class CourseOutlineTableController : UITableViewController, CourseVideoTableView private let headerContainer = UIView() private lazy var resumeCourseView = CourseOutlineHeaderView(frame: .zero, styles: OEXStyles.shared(), titleText: Strings.resume, subtitleText: "Placeholder") private lazy var valuePropView = UIView() - + var courseVideosHeaderView: CourseVideosHeaderView? - private var isResumeCourse = false - private var shouldHideTableViewHeader:Bool = false let refreshController = PullRefreshController() - private var courseBlockID: CourseBlockID? + + private var isResumeCourse = false + private var shouldHideTableViewHeader: Bool = false + + weak var scrollableDelegate: ScrollableDelegate? + private var scrollByDragging = false var isSectionOutline = false { didSet { @@ -620,6 +625,22 @@ extension CourseOutlineTableController: BlockCompletionDelegate { } } +extension CourseOutlineTableController { + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollByDragging = true + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollByDragging { + scrollableDelegate?.scrollViewDidScroll(scrollView: scrollView) + } + } + + override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + scrollByDragging = false + } +} + extension UITableView { //set the tableHeaderView so that the required height can be determined, update the header's frame and set it again func setAndLayoutTableHeaderView(header: UIView) { diff --git a/Source/CourseOutlineViewController.swift b/Source/CourseOutlineViewController.swift index d455ad5b3d..f9c2a8880c 100644 --- a/Source/CourseOutlineViewController.swift +++ b/Source/CourseOutlineViewController.swift @@ -20,7 +20,8 @@ public class CourseOutlineViewController : CourseContentPageViewControllerDelegate, PullRefreshControllerDelegate, LoadStateViewReloadSupport, - InterfaceOrientationOverriding + InterfaceOrientationOverriding, + ScrollableDelegateProvider { public typealias Environment = OEXAnalyticsProvider & DataManagerProvider & OEXInterfaceProvider & NetworkManagerProvider & ReachabilityProvider & OEXRouterProvider & OEXConfigProvider & OEXStylesProvider & ServerConfigProvider @@ -67,6 +68,12 @@ public class CourseOutlineViewController : return environment.dataManager.enrollmentManager.enrolledCourseWithID(courseID: courseID)?.course } + public weak var scrollableDelegate: ScrollableDelegate? { + didSet { + tableController.scrollableDelegate = scrollableDelegate + } + } + public init(environment: Environment, courseID : String, rootID : CourseBlockID?, forMode mode: CourseOutlineMode?) { self.rootID = rootID self.environment = environment diff --git a/Source/DiscussionTopicsViewController.swift b/Source/DiscussionTopicsViewController.swift index 11cf1b5f36..9b8614c61a 100644 --- a/Source/DiscussionTopicsViewController.swift +++ b/Source/DiscussionTopicsViewController.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -public class DiscussionTopicsViewController: OfflineSupportViewController, UITableViewDataSource, UITableViewDelegate, InterfaceOrientationOverriding, LoadStateViewReloadSupport { +public class DiscussionTopicsViewController: OfflineSupportViewController, UITableViewDataSource, UITableViewDelegate, InterfaceOrientationOverriding, LoadStateViewReloadSupport, ScrollableDelegateProvider { public typealias Environment = DataManagerProvider & OEXRouterProvider & OEXAnalyticsProvider & ReachabilityProvider & NetworkManagerProvider @@ -31,6 +31,9 @@ public class DiscussionTopicsViewController: OfflineSupportViewController, UITab private let tableView = UITableView() private let searchBarSeparator = UIView() + public weak var scrollableDelegate: ScrollableDelegate? + private var scrollByDragging = false + public init(environment: Environment, courseID: String) { self.environment = environment self.courseID = courseID @@ -155,7 +158,6 @@ public class DiscussionTopicsViewController: OfflineSupportViewController, UITab self.environment.analytics.trackScreen(withName: OEXAnalyticsScreenViewTopics, courseID: self.courseID, value: nil) refreshTopics() - self.navigationController?.setNavigationBarHidden(false, animated: animated) } override func reloadViewData() { @@ -238,6 +240,22 @@ public class DiscussionTopicsViewController: OfflineSupportViewController, UITab } } +extension DiscussionTopicsViewController: UIScrollViewDelegate { + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollByDragging = true + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollByDragging { + scrollableDelegate?.scrollViewDidScroll(scrollView: scrollView) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + scrollByDragging = false + } +} + extension DiscussionTopicsViewController { public func t_topicsLoaded() -> OEXStream<[DiscussionTopic]> { return topics diff --git a/Source/NewCourseDashboardViewController.swift b/Source/NewCourseDashboardViewController.swift index 752ba0c3c5..d970f0e204 100644 --- a/Source/NewCourseDashboardViewController.swift +++ b/Source/NewCourseDashboardViewController.swift @@ -19,19 +19,21 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv return view }() - private lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.accessibilityIdentifier = "NewCourseDashboardViewController:table-view" - tableView.register(CourseDashboardErrorViewCell.self, forCellReuseIdentifier: CourseDashboardErrorViewCell.identifier) - tableView.register(CourseDashboardAccessErrorCell.self, forCellReuseIdentifier: CourseDashboardAccessErrorCell.identifier) - tableView.register(NewDashboardContentCell.self, forCellReuseIdentifier: NewDashboardContentCell.identifier) - tableView.estimatedRowHeight = 100 - tableView.rowHeight = UITableView.automaticDimension - tableView.delegate = self - tableView.dataSource = self - return tableView + private lazy var contentView: UIView = { + let view = UIView() + view.accessibilityIdentifier = "NewCourseDashboardViewController:contentView-view" + return view + }() + + private lazy var container: UIView = { + let view = UIView() + view.accessibilityIdentifier = "NewCourseDashboardViewController:container-view" + return view }() + private var collapsed = false + private var isAnimating = false + private lazy var courseUpgradeHelper = CourseUpgradeHelper.shared private var pacing: String { @@ -43,6 +45,7 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv private var error: NSError? private var courseAccessError: CourseAccessErrorHelper? private var selectedTabbarItem: TabBarItem? + private var headerViewState: CourseDashboardHeaderViewState = .expanded private var isModalDismissable = true private let courseStream: BackedStream @@ -67,6 +70,7 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv override func viewDidLoad() { super.viewDidLoad() + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) addSubviews() loadCourseStream() } @@ -75,48 +79,56 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv super.viewWillAppear(animated) navigationItem.setHidesBackButton(true, animated: true) + navigationController?.setNavigationBarHidden(true, animated: true) environment.analytics.trackScreen(withName: OEXAnalyticsScreenCourseDashboard, courseID: courseID, value: nil) } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setNavigationBarHidden(false, animated: true) + } + private func addSubviews() { view.backgroundColor = environment.styles.neutralWhiteT() - view.addSubview(tableView) - - loadStateController.setupInController(controller: self, contentView: tableView) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - updateViewConstraints() - } - - override func updateViewConstraints() { - tableView.snp.remakeConstraints { make in + view.addSubview(contentView) + contentView.snp.remakeConstraints { make in make.edges.equalTo(safeEdges) } - configureHeaderView() - super.updateViewConstraints() + loadStateController.setupInController(controller: self, contentView: contentView) } - private func configureHeaderView() { - tableView.tableHeaderView = headerView + private func setupConstraints() { + container.removeFromSuperview() + headerView.removeFromSuperview() + + contentView.addSubview(container) + contentView.addSubview(headerView) + headerView.snp.remakeConstraints { make in - make.leading.equalTo(safeLeading) - make.trailing.equalTo(safeTrailing) + make.top.equalTo(contentView) + make.leading.equalTo(contentView) + make.trailing.equalTo(contentView) + make.height.lessThanOrEqualTo(StandardVerticalMargin * 29) + } + + container.snp.remakeConstraints { make in + make.leading.equalTo(contentView) + make.trailing.equalTo(contentView) + make.top.equalTo(headerView.snp.bottom) + make.bottom.equalTo(contentView) } - tableView.setAndLayoutTableHeaderView(header: headerView) } private func loadCourseStream() { courseStream.backWithStream(environment.dataManager.enrollmentManager.streamForCourseWithID(courseID: courseID)) - courseStream.listen(self) { [weak self] result in + courseStream.listenOnce(self) { [weak self] result in self?.resultLoaded(result: result) } } private func loadedCourse(withCourse course: OEXCourse) { verifyAccess(forCourse: course) - configureHeaderView() + setupConstraints() } private func resultLoaded(result: Result) { @@ -124,12 +136,13 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv case .success(let enrollment): course = enrollment.course loadedCourse(withCourse: enrollment.course) + setupConstraints() case .failure(let error): if !courseStream.active { loadStateController.state = .Loaded self.error = error headerView.showTabbarView(show: false) - tableView.reloadData() + setupContentView() } } } @@ -145,7 +158,42 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv } else { loadStateController.state = .Loaded headerView.showTabbarView(show: true) - tableView.reloadData() + } + setupContentView() + } + + private func setupContentView() { + container.subviews.forEach { $0.removeFromSuperview() } + + if showCourseAccessError { + let view = CourseDashboardAccessErrorView() + view.delegate = self + view.handleCourseAccessError(environment: environment, course: course, error: courseAccessError) + container.addSubview(view) + view.snp.remakeConstraints { make in + make.edges.equalTo(container) + } + } else if showContentNotLoadedError { + let view = CourseDashboardErrorView() + view.myCoursesAction = { [weak self] in + self?.dismiss(animated: true) + } + container.addSubview(view) + view.snp.remakeConstraints { make in + make.edges.equalTo(container) + } + } else if let tabBarItem = selectedTabbarItem { + let contentController = tabBarItem.viewController + if var controller = contentController as? ScrollableDelegateProvider { + controller.scrollableDelegate = self + } + addChild(contentController) + container.addSubview(contentController.view) + contentController.view.snp.remakeConstraints { make in + make.edges.equalTo(container) + } + contentController.didMove(toParent: self) + contentController.view.layoutIfNeeded() } } @@ -176,71 +224,7 @@ class NewCourseDashboardViewController: UIViewController, InterfaceOrientationOv } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - tableView.reloadData() - } - - private func reloadCoursePriceIfNecessary() { - if let _ = tableView.visibleCells.first(where: { $0 is CourseDashboardAccessErrorCell }) { - tableView.reloadData() - } - } -} - -extension NewCourseDashboardViewController: UITableViewDelegate { - -} - -extension NewCourseDashboardViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let visibleCells = tableView.visibleCells as? [NewDashboardContentCell] { - visibleCells.forEach { cell in - cell.viewController?.willMove(toParent: nil) - cell.viewController?.view.removeFromSuperview() - cell.viewController?.removeFromParent() - } - } - - if showCourseAccessError { - let cell = tableView.dequeueReusableCell(withIdentifier: CourseDashboardAccessErrorCell.identifier, for: indexPath) as! CourseDashboardAccessErrorCell - - cell.delegate = self - cell.handleCourseAccessError(environment: environment, course: course, error: courseAccessError) - - return cell - } else if showContentNotLoadedError { - let cell = tableView.dequeueReusableCell(withIdentifier: CourseDashboardErrorViewCell.identifier, for: indexPath) as! CourseDashboardErrorViewCell - cell.myCoursesAction = { [weak self] in - self?.dismiss(animated: true) - } - } else if let tabBarItem = selectedTabbarItem { - let cell = tableView.dequeueReusableCell(withIdentifier: NewDashboardContentCell.identifier, for: indexPath) as! NewDashboardContentCell - let contentController = tabBarItem.viewController - addChild(contentController) - cell.contentView.addSubview(contentController.view) - - let height = tableView.frame.height - headerView.frame.height - contentController.view.snp.makeConstraints { make in - make.edges.equalTo(cell.contentView) - make.height.equalTo(height) - } - - contentController.didMove(toParent: self) - contentController.view.layoutIfNeeded() - - cell.viewController = contentController - - return cell - } - - return UITableViewCell() + setupContentView() } } @@ -274,18 +258,19 @@ extension NewCourseDashboardViewController: CourseDashboardHeaderViewDelegate { func didTapTabbarItem(at position: Int, tabbarItem: TabBarItem) { if courseAccessError == nil && selectedTabbarItem != tabbarItem { + selectedTabbarItem?.viewController.removeFromParent() selectedTabbarItem = tabbarItem - tableView.reloadData() + setupContentView() } } } -extension NewCourseDashboardViewController: CourseDashboardAccessErrorCellDelegate { +extension NewCourseDashboardViewController: CourseDashboardAccessErrorViewDelegate { func findCourseAction() { redirectToDiscovery() } - func coursePrice(cell: CourseDashboardAccessErrorCell, price: String?, elapsedTime: Int) { + func coursePrice(cell: CourseDashboardAccessErrorView, price: String?, elapsedTime: Int) { if let price = price { trackPriceLoadDuration(price: price, elapsedTime: elapsedTime) } @@ -357,13 +342,13 @@ extension NewCourseDashboardViewController { environment.analytics.trackCourseUpgradeTimeToLoadPrice(courseID: courseID, pacing: pacing, coursePrice: price, screen: screen, elapsedTime: elapsedTime) } - private func trackPriceLoadError(cell: CourseDashboardAccessErrorCell) { + private func trackPriceLoadError(cell: CourseDashboardAccessErrorView) { guard let course = course, let courseID = course.course_id else { return } environment.analytics.trackCourseUpgradeLoadError(courseID: courseID, pacing: pacing, screen: screen) showCoursePriceErrorAlert(cell: cell) } - private func showCoursePriceErrorAlert(cell: CourseDashboardAccessErrorCell) { + private func showCoursePriceErrorAlert(cell: CourseDashboardAccessErrorView) { guard let topController = UIApplication.shared.topMostController() else { return } let alertController = UIAlertController().showAlert(withTitle: Strings.CourseUpgrade.FailureAlert.alertTitle, message: Strings.CourseUpgrade.FailureAlert.priceFetchErrorMessage, cancelButtonTitle: nil, onViewController: topController) { _, _, _ in } @@ -392,3 +377,61 @@ extension NewCourseDashboardViewController: CourseUpgradeHelperDelegate { dismiss(animated: true, completion: nil) } } + +extension NewCourseDashboardViewController: ScrollableDelegate { + func scrollViewDidScroll(scrollView: UIScrollView) { + guard headerViewState != .animating else { return } + + if scrollView.contentOffset.y <= 0 { + if headerViewState == .collapsed { + headerViewState = .animating + expandHeaderView() + } + } else if headerViewState == .expanded { + headerViewState = .animating + collapseHeaderView() + } + } +} + +extension NewCourseDashboardViewController { + private func expandHeaderView() { + headerView.snp.remakeConstraints { make in + make.top.equalTo(contentView) + make.leading.equalTo(contentView) + make.trailing.equalTo(contentView) + make.height.lessThanOrEqualTo(StandardVerticalMargin * 29) + } + + UIView.animateKeyframes(withDuration: 0.4, delay: 0, options: .calculationModeLinear) { [weak self] in + UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { + self?.headerView.updateTabbarConstraints(collapse: false) + self?.headerView.showCourseTitleHeaderLabel(show: false) + self?.view.layoutIfNeeded() + } + UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) { + self?.headerView.updateHeader(collapse: false) + } + } completion: { [weak self] _ in + self?.headerViewState = .expanded + } + } + + private func collapseHeaderView() { + headerView.updateHeader(collapse: true) + + headerView.snp.remakeConstraints { make in + make.top.equalTo(contentView) + make.leading.equalTo(contentView) + make.trailing.equalTo(contentView) + make.height.equalTo(StandardVerticalMargin * 12) + } + + UIView.animate(withDuration: 0.3) { [weak self] in + self?.headerView.showCourseTitleHeaderLabel(show: true) + self?.view.layoutIfNeeded() + } completion: { [weak self] _ in + self?.headerViewState = .collapsed + } + } +} diff --git a/Source/OEXRouter+Swift.swift b/Source/OEXRouter+Swift.swift index a951273081..261f5fe2a1 100644 --- a/Source/OEXRouter+Swift.swift +++ b/Source/OEXRouter+Swift.swift @@ -168,9 +168,10 @@ extension OEXRouter { @objc(showMyCoursesAnimated:pushingCourseWithID:) func showMyCourses(animated: Bool = true, pushingCourseWithID courseID: String? = nil) { let controller = EnrolledTabBarViewController(environment: environment) + let learnController = controller.children.flatMap { $0.children }.compactMap { $0 as? LearnContainerViewController } .first showContentStack(withRootController: controller, animated: animated) - if let courseID = courseID { - showCourseWithID(courseID: courseID, fromController: controller, animated: false) + if let courseID = courseID, let learnController = learnController { + showCourseWithID(courseID: courseID, fromController: learnController, animated: false) } } @@ -469,7 +470,8 @@ extension OEXRouter { func showCourseWithID(courseID: String, fromController: UIViewController, animated: Bool = true, completion: ((UIViewController) -> Void)? = nil) { if environment.config.isNewDashboardEnabled { - let controller = NewCourseDashboardViewController(environment: environment, courseID: courseID) + let controller = ForwardingNavigationController(rootViewController: NewCourseDashboardViewController(environment: environment, courseID: courseID)) + controller.navigationController?.setNavigationBarHidden(true, animated: false) controller.modalPresentationStyle = .fullScreen fromController.navigationController?.present(controller, animated: true) } else { diff --git a/Source/ScrollableDelegate.swift b/Source/ScrollableDelegate.swift new file mode 100644 index 0000000000..00f74aa7d4 --- /dev/null +++ b/Source/ScrollableDelegate.swift @@ -0,0 +1,17 @@ +// +// ScrollableDelegate.swift +// edX +// +// Created by MuhammadUmer on 01/02/2023. +// Copyright © 2023 edX. All rights reserved. +// + +import Foundation + +public protocol ScrollableDelegateProvider { + var scrollableDelegate: ScrollableDelegate? { get set } +} + +@objc public protocol ScrollableDelegate: AnyObject { + func scrollViewDidScroll(scrollView: UIScrollView) +} diff --git a/edX.xcodeproj/project.pbxproj b/edX.xcodeproj/project.pbxproj index 97c781a235..4f40784783 100644 --- a/edX.xcodeproj/project.pbxproj +++ b/edX.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 5F0248C624AC9ED8000AF1FF /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F0248C524AC9ED8000AF1FF /* CourseDates.swift */; }; 5F0248C824AC9F09000AF1FF /* CourseDatesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F0248C724AC9F09000AF1FF /* CourseDatesAPI.swift */; }; 5F08321427018D810022971F /* BannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F08321327018D810022971F /* BannerViewController.swift */; }; + 5F11143A298A9C7F00964F02 /* ScrollableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F111439298A9C7F00964F02 /* ScrollableDelegate.swift */; }; 5F1E03FC26CA64B9004F8139 /* VideoDownloadQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1E03FB26CA64B9004F8139 /* VideoDownloadQuality.swift */; }; 5F28980A25074D5A00BF76DF /* CourseDateBannerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F28980925074D5A00BF76DF /* CourseDateBannerModel.swift */; }; 5F28980C2507B19400BF76DF /* CourseDateBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F28980B2507B19400BF76DF /* CourseDateBannerView.swift */; }; @@ -869,8 +870,8 @@ E0EC115C221AE70900F0574A /* ListenableObject.m in Sources */ = {isa = PBXBuildFile; fileRef = E0EC115B221AE70900F0574A /* ListenableObject.m */; }; E0EC12AD2216A6910090EEF6 /* NSString+OEXFormatting.h in Headers */ = {isa = PBXBuildFile; fileRef = 77E647C51C90C70600B6740D /* NSString+OEXFormatting.h */; settings = {ATTRIBUTES = (Public, ); }; }; E0EEC6E71F1CD279006C8D62 /* whats_new.json in Resources */ = {isa = PBXBuildFile; fileRef = E0EEC6E91F1CD279006C8D62 /* whats_new.json */; }; - E0F9C02F29362806003D96DF /* CourseDashboardErrorViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F9C02E29362806003D96DF /* CourseDashboardErrorViewCell.swift */; }; - E0F9C0312939C497003D96DF /* CourseDashboardAccessErrorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorCell.swift */; }; + E0F9C02F29362806003D96DF /* CourseDashboardErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F9C02E29362806003D96DF /* CourseDashboardErrorView.swift */; }; + E0F9C0312939C497003D96DF /* CourseDashboardAccessErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorView.swift */; }; E0FC64C31C85B492004E3E92 /* DiscussionDataParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FC64C11C85B46C004E3E92 /* DiscussionDataParsingTests.swift */; }; E0FCFCC91EC59DB2000B969C /* WhatsNewObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FCFCC81EC59DB2000B969C /* WhatsNewObjectTests.swift */; }; E0FF457920FDD24400109662 /* BlockCompletionApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FF457820FDD24400109662 /* BlockCompletionApi.swift */; }; @@ -1169,6 +1170,7 @@ 5F0248C524AC9ED8000AF1FF /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; 5F0248C724AC9F09000AF1FF /* CourseDatesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesAPI.swift; sourceTree = ""; }; 5F08321327018D810022971F /* BannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerViewController.swift; sourceTree = ""; }; + 5F111439298A9C7F00964F02 /* ScrollableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableDelegate.swift; sourceTree = ""; }; 5F1E03FB26CA64B9004F8139 /* VideoDownloadQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQuality.swift; sourceTree = ""; }; 5F28980925074D5A00BF76DF /* CourseDateBannerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateBannerModel.swift; sourceTree = ""; }; 5F28980B2507B19400BF76DF /* CourseDateBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateBannerView.swift; sourceTree = ""; }; @@ -2027,8 +2029,8 @@ E0EC115A221AE70900F0574A /* ListenableObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ListenableObject.h; sourceTree = ""; }; E0EC115B221AE70900F0574A /* ListenableObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ListenableObject.m; sourceTree = ""; }; E0EEC6E81F1CD279006C8D62 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = en; path = en.lproj/whats_new.json; sourceTree = ""; }; - E0F9C02E29362806003D96DF /* CourseDashboardErrorViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDashboardErrorViewCell.swift; sourceTree = ""; }; - E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDashboardAccessErrorCell.swift; sourceTree = ""; }; + E0F9C02E29362806003D96DF /* CourseDashboardErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDashboardErrorView.swift; sourceTree = ""; }; + E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDashboardAccessErrorView.swift; sourceTree = ""; }; E0FC64C11C85B46C004E3E92 /* DiscussionDataParsingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscussionDataParsingTests.swift; sourceTree = ""; }; E0FCFCC81EC59DB2000B969C /* WhatsNewObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewObjectTests.swift; sourceTree = ""; }; E0FF457820FDD24400109662 /* BlockCompletionApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockCompletionApi.swift; sourceTree = ""; }; @@ -3684,8 +3686,8 @@ 5F08321327018D810022971F /* BannerViewController.swift */, 5F2DFCAF29262D8E00BDA40A /* CourseDashboardHeaderView.swift */, 5F2DFCB129278F0200BDA40A /* NewCourseDashboardViewController.swift */, - E0F9C02E29362806003D96DF /* CourseDashboardErrorViewCell.swift */, - E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorCell.swift */, + E0F9C02E29362806003D96DF /* CourseDashboardErrorView.swift */, + E0F9C0302939C497003D96DF /* CourseDashboardAccessErrorView.swift */, 5FA4266E2966B1110013BBA8 /* CourseAccessErrorHelper.swift */, 5F6C8A51292DFCBB00E5FA7F /* NewDashboardContentCell.swift */, 5F5239BE293DB9560046FF07 /* CourseDashboardTabbarView.swift */, @@ -4070,6 +4072,7 @@ 77D6A9A51C28F98A00E67CCF /* EnrolledCoursesViewController.swift */, 223972E01FE92BB500B2BBEC /* EnrolledTabBarViewController.swift */, 2240FD551FF266E4001D6589 /* TabBarItem.swift */, + 5F111439298A9C7F00964F02 /* ScrollableDelegate.swift */, E0D029F527043CCF001F83B1 /* EnrolledCoursesViewController+Banner.swift */, E0B58CDB2818F7DD0047D78F /* EnrolledCoursesViewController+CourseUpgrade.swift */, 5FC5AE102847944E007E5917 /* LearnContainerHeaderView.swift */, @@ -4954,6 +4957,7 @@ 773A04801AF2E6DA0076532C /* CourseOutlineTableSource.swift in Sources */, B70BD00920B57E8F005F0D19 /* OEXCourseDetailTableViewCell.m in Sources */, 191A002B19405E1B004F7902 /* OEXCourse.m in Sources */, + 5F11143A298A9C7F00964F02 /* ScrollableDelegate.swift in Sources */, 2240FD561FF266E4001D6589 /* TabBarItem.swift in Sources */, E0A2461F1D5DA12A0066C766 /* AppStoreConfig.swift in Sources */, 7778F0981ABB1A6C00B4CDA0 /* NSError+OEXKnownErrors.m in Sources */, @@ -5046,7 +5050,7 @@ 778F17781C0D123F0099BF93 /* CourseCatalogViewController.swift in Sources */, 1AB539E41BFA24DC0065501F /* CertificateViewController.swift in Sources */, 69E1CD011D7D7BA300531449 /* OEXRegistrationViewController+Swift.swift in Sources */, - E0F9C0312939C497003D96DF /* CourseDashboardAccessErrorCell.swift in Sources */, + E0F9C0312939C497003D96DF /* CourseDashboardAccessErrorView.swift in Sources */, 19BB622A1A9B28F1007DBF47 /* OEXRegistrationFieldWrapperView.m in Sources */, 9E1081081B8B7EEC00888746 /* PaginatedFeed.swift in Sources */, B7CCC720209B16B100A66923 /* ConstraintMakerPriortizable.swift in Sources */, @@ -5242,7 +5246,7 @@ 779D1CE11B8E6FE000FCC847 /* PullRefreshController.swift in Sources */, B4D31D351AB1904200C8D45C /* NSJSONSerialization+OEXSafeAccess.m in Sources */, 223895CF1F25CF76005B9C15 /* SwipeableCell.swift in Sources */, - E0F9C02F29362806003D96DF /* CourseDashboardErrorViewCell.swift in Sources */, + E0F9C02F29362806003D96DF /* CourseDashboardErrorView.swift in Sources */, 5F2C5A19242C99EE00FBF986 /* CollectionPaginationManipulator.swift in Sources */, 777DE7141C1630110068E280 /* CourseMediaInfo.swift in Sources */, E01591F21D533F3B00201B15 /* UIViewController+CommonAdditions.swift in Sources */,