From 8164d4400d82ea5b1a3944757e9b81f7aed2f6e7 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 21 Nov 2022 14:48:59 +1100 Subject: [PATCH] feat: make the carousel looping infinitely --- Session.xcodeproj/project.pbxproj | 8 +- .../ConversationTitleView.swift | 107 ++++++------------ ...w.swift => SessionLabelCarouselView.swift} | 107 +++++++++++++----- 3 files changed, 122 insertions(+), 100 deletions(-) rename Session/Conversations/Views & Modals/{PagedScrollView.swift => SessionLabelCarouselView.swift} (53%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 302687811..fe8c576f1 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -113,7 +113,7 @@ 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; - 7B5233C42900E90F00F8F375 /* PagedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* PagedScrollView.swift */; }; + 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; }; 7B5233C6290636D700F8F375 /* _011_DisappearingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _011_DisappearingMessage.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; @@ -1185,7 +1185,7 @@ 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; - 7B5233C32900E90F00F8F375 /* PagedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedScrollView.swift; sourceTree = ""; }; + 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = ""; }; 7B5233C5290636D700F8F375 /* _011_DisappearingMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_DisappearingMessage.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; @@ -2386,7 +2386,7 @@ C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */, FD4B200D283492210034334B /* InsetLockableTableView.swift */, 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */, - 7B5233C32900E90F00F8F375 /* PagedScrollView.swift */, + 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */, ); path = "Views & Modals"; sourceTree = ""; @@ -5763,7 +5763,7 @@ B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */, B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */, B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */, - 7B5233C42900E90F00F8F375 /* PagedScrollView.swift in Sources */, + 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */, 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */, 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, diff --git a/Session/Conversations/Views & Modals/ConversationTitleView.swift b/Session/Conversations/Views & Modals/ConversationTitleView.swift index 7b27558db..8b50d9dee 100644 --- a/Session/Conversations/Views & Modals/ConversationTitleView.swift +++ b/Session/Conversations/Views & Modals/ConversationTitleView.swift @@ -15,7 +15,7 @@ final class ConversationTitleView: UIView { return UIView.layoutFittingExpandedSize } - private lazy var pagedScrollViewWidth = pagedScrollView.set(.width, to: 200) + private lazy var labelCarouselViewWidth = labelCarouselView.set(.width, to: 200) // MARK: - UI Components @@ -31,49 +31,13 @@ final class ConversationTitleView: UIView { return result }() - private lazy var pagedScrollView: PagedScrollView = { - let result = PagedScrollView() - return result - }() - - private lazy var subtitleLabel: UILabel = { - let result: UILabel = UILabel() - result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.themeTextColor = .textPrimary - result.lineBreakMode = .byTruncatingTail - - return result - }() - - private lazy var userCountLabel: UILabel = { - let result: UILabel = UILabel() - result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.themeTextColor = .textPrimary - result.lineBreakMode = .byTruncatingTail - - return result - }() - - private lazy var notificationSettingsLabel: UILabel = { - let result: UILabel = UILabel() - result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.themeTextColor = .textPrimary - result.lineBreakMode = .byTruncatingTail - - return result - }() - - private lazy var disappearingMessageSettingLabel: UILabel = { - let result: UILabel = UILabel() - result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.themeTextColor = .textPrimary - result.lineBreakMode = .byTruncatingTail - + private lazy var labelCarouselView: SessionLabelCarouselView = { + let result = SessionLabelCarouselView() return result }() private lazy var stackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ titleLabel, pagedScrollView ]) + let result = UIStackView(arrangedSubviews: [ titleLabel, labelCarouselView ]) result.axis = .vertical result.alignment = .center @@ -169,13 +133,13 @@ final class ConversationTitleView: UIView { ) ) - ThemeManager.onThemeChange(observer: self.subtitleLabel) { [weak self] theme, _ in + ThemeManager.onThemeChange(observer: self.labelCarouselView) { [weak self] theme, _ in guard let textPrimary: UIColor = theme.color(for: .textPrimary) else { return } - var slides: [UIView?] = [] + var labelStrings: [NSAttributedString] = [] if Date().timeIntervalSince1970 <= (mutedUntilTimestamp ?? 0) { - self?.notificationSettingsLabel.attributedText = NSAttributedString( + let notificationSettingsLabelString = NSAttributedString( string: "\u{e067} ", attributes: [ .font: UIFont.ows_elegantIconsFont(10), @@ -183,8 +147,8 @@ final class ConversationTitleView: UIView { ] ) .appending(string: "Muted") - self?.notificationSettingsLabel.isHidden = false - slides.append(self?.notificationSettingsLabel) + + labelStrings.append(notificationSettingsLabelString) } else if onlyNotifyForMentions{ let imageAttachment = NSTextAttachment() imageAttachment.image = UIImage(named: "NotifyMentions.png")?.withTint(textPrimary) @@ -195,28 +159,31 @@ final class ConversationTitleView: UIView { height: Values.verySmallFontSize ) - self?.notificationSettingsLabel.attributedText = NSAttributedString(attachment: imageAttachment) + let notificationSettingsLabelString = NSAttributedString(attachment: imageAttachment) .appending(string: " ") .appending(string: "view_conversation_title_notify_for_mentions_only".localized()) - self?.notificationSettingsLabel.isHidden = false - slides.append(self?.notificationSettingsLabel) + + labelStrings.append(notificationSettingsLabelString) } if let userCount: Int = userCount { - switch threadVariant { - case .contact: break - - case .closedGroup: - self?.userCountLabel.attributedText = NSAttributedString( - string: "\(userCount) member\(userCount == 1 ? "" : "s")" - ) - - case .openGroup: - self?.userCountLabel.attributedText = NSAttributedString( - string: "\(userCount) active member\(userCount == 1 ? "" : "s")" - ) - } - slides.append(self?.userCountLabel) + let userCountLabelString: NSAttributedString = { + switch threadVariant { + case .contact: return NSAttributedString(string: "") // Should not happen + + case .closedGroup: + return NSAttributedString( + string: "\(userCount) member\(userCount == 1 ? "" : "s")" + ) + + case .openGroup: + return NSAttributedString( + string: "\(userCount) active member\(userCount == 1 ? "" : "s")" + ) + } + }() + + labelStrings.append(userCountLabelString) } if let config = disappearingMessagesConfig, config.isEnabled == true { @@ -229,25 +196,25 @@ final class ConversationTitleView: UIView { height: Values.verySmallFontSize ) - self?.disappearingMessageSettingLabel.attributedText = NSAttributedString(attachment: imageAttachment) + let disappearingMessageSettingLabelString = NSAttributedString(attachment: imageAttachment) .appending(string: " ") .appending(string: config.type == .disappearAfterRead ? "DISAPPERING_MESSAGES_TYPE_AFTER_READ_TITLE".localized() : "DISAPPERING_MESSAGES_TYPE_AFTER_SEND_TITLE".localized()) .appending(string: " - ") .appending(string: floor(config.durationSeconds).formatted(format: .short)) - self?.disappearingMessageSettingLabel.isHidden = false - slides.append(self?.disappearingMessageSettingLabel) + + labelStrings.append(disappearingMessageSettingLabelString) } - self?.pagedScrollView.update( - with: slides.compactMap{ $0 }, - slideSize: CGSize( - width: self?.pagedScrollViewWidth.constant ?? 0, + self?.labelCarouselView.update( + with: labelStrings, + labelSize: CGSize( + width: self?.labelCarouselViewWidth.constant ?? 0, height: 20 ), shouldAutoScroll: false ) - self?.pagedScrollView.isHidden = (slides.count == 0) + self?.labelCarouselView.isHidden = (labelStrings.count == 0) } // Contact threads also have the call button to compensate for diff --git a/Session/Conversations/Views & Modals/PagedScrollView.swift b/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift similarity index 53% rename from Session/Conversations/Views & Modals/PagedScrollView.swift rename to Session/Conversations/Views & Modals/SessionLabelCarouselView.swift index 1a04e2408..118c24de5 100644 --- a/Session/Conversations/Views & Modals/PagedScrollView.swift +++ b/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift @@ -3,20 +3,21 @@ import UIKit import SessionUIKit -final class PagedScrollView: UIView, UIScrollViewDelegate { +final class SessionLabelCarouselView: UIView, UIScrollViewDelegate { private static let autoScrollingTimeInterval: TimeInterval = 10 - private var slides: [UIView] = [] - private var slideSize: CGSize = .zero + private var labelStrings: [NSAttributedString] = [] + private var labelSize: CGSize = .zero private var shouldAutoScroll: Bool = false private var timer: Timer? private lazy var contentWidth = stackView.set(.width, to: 0) private lazy var contentHeight = stackView.set(.height, to: 0) - private var shouldArrowsShow: Bool = false { + private var shouldScroll: Bool = false { didSet { - arrowLeft.isHidden = !shouldArrowsShow - arrowRight.isHidden = !shouldArrowsShow + arrowLeft.isHidden = !shouldScroll + arrowRight.isHidden = !shouldScroll + pageControl.isHidden = !shouldScroll } } @@ -67,10 +68,10 @@ final class PagedScrollView: UIView, UIScrollViewDelegate { // MARK: - Initialization - init(slides: [UIView] = [], slideSize: CGSize = .zero, shouldAutoScroll: Bool = false) { + init(labelStrings: [NSAttributedString] = [], labelSize: CGSize = .zero, shouldAutoScroll: Bool = false) { super.init(frame: .zero) setUpViewHierarchy() - self.update(with: slides, slideSize: slideSize, shouldAutoScroll: shouldAutoScroll) + self.update(with: labelStrings, labelSize: labelSize, shouldAutoScroll: shouldAutoScroll) } required init?(coder: NSCoder) { @@ -79,29 +80,47 @@ final class PagedScrollView: UIView, UIScrollViewDelegate { // MARK: - Content - public func update(with slides: [UIView] = [], slideSize: CGSize = .zero, shouldAutoScroll: Bool = false) { - self.slides = slides - self.slideSize = slideSize + public func update(with labelStrings: [NSAttributedString] = [], labelSize: CGSize = .zero, shouldAutoScroll: Bool = false) { + self.labelStrings = labelStrings + self.labelSize = labelSize self.shouldAutoScroll = shouldAutoScroll - self.shouldArrowsShow = slides.count > 1 + self.shouldScroll = labelStrings.count > 1 - pageControl.numberOfPages = slides.count + if self.shouldScroll { + let first: NSAttributedString = labelStrings.first! + let last: NSAttributedString = labelStrings.last! + self.labelStrings.append(first) + self.labelStrings.insert(last, at: 0) + } + + pageControl.numberOfPages = labelStrings.count pageControl.currentPage = 0 - pageControl.isHidden = (slides.count == 1) - let contentSize = CGSize(width: slideSize.width * CGFloat(slides.count), height: slideSize.height) + let contentSize = CGSize(width: labelSize.width * CGFloat(self.labelStrings.count), height: labelSize.height) scrollView.contentSize = contentSize contentWidth.constant = contentSize.width contentHeight.constant = contentSize.height + self.scrollView.setContentOffset( + CGPoint( + x: Int(self.labelSize.width) * (self.shouldScroll ? 1 : 0), + y: 0 + ), + animated: false + ) stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - self.slides.forEach { + self.labelStrings.forEach { let wrapper: UIView = UIView() - wrapper.set(.width, to: slideSize.width) - wrapper.set(.height, to: slideSize.height) - wrapper.addSubview($0) - $0.center(in: wrapper) + wrapper.set(.width, to: labelSize.width) + wrapper.set(.height, to: labelSize.height) + let label: UILabel = UILabel() + label.font = .systemFont(ofSize: Values.verySmallFontSize) + label.themeTextColor = .textPrimary + label.lineBreakMode = .byTruncatingTail + label.attributedText = $0 + wrapper.addSubview(label) + label.center(in: wrapper) stackView.addArrangedSubview(wrapper) } @@ -132,15 +151,15 @@ final class PagedScrollView: UIView, UIScrollViewDelegate { private func startScrolling() { timer?.invalidate() timer = Timer.scheduledTimerOnMainThread(withTimeInterval: Self.autoScrollingTimeInterval, repeats: true) { _ in - guard self.slides.count != 0 else { return } - let targetPage = (self.pageControl.currentPage + 1) % self.slides.count + guard self.labelStrings.count != 0 else { return } + let targetPage = (self.pageControl.currentPage + 1) % self.labelStrings.count self.scrollView.scrollRectToVisible( CGRect( origin: CGPoint( - x: Int(self.slideSize.width) * targetPage, + x: Int(self.labelSize.width) * targetPage, y: 0 ), - size: self.slideSize + size: self.labelSize ), animated: true ) @@ -153,7 +172,43 @@ final class PagedScrollView: UIView, UIScrollViewDelegate { } func scrollViewDidScroll(_ scrollView: UIScrollView) { - let pageIndex = round(scrollView.contentOffset.x/slideSize.width) - pageControl.currentPage = Int(pageIndex) + let pageIndex: Int = { + let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/labelSize.width)) + if self.shouldScroll { + if maybeCurrentPageIndex == 0 { + return pageControl.numberOfPages - 1 + } + if maybeCurrentPageIndex == self.labelStrings.count - 1 { + return 0 + } + return maybeCurrentPageIndex - 1 + } + return maybeCurrentPageIndex + }() + + pageControl.currentPage = pageIndex + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + if pageControl.currentPage == 0 { + scrollView.setContentOffset( + CGPoint( + x: Int(self.labelSize.width) * 1, + y: 0 + ), + animated: false + ) + } + + if pageControl.currentPage == pageControl.numberOfPages - 1 { + let realLastIndex: Int = self.labelStrings.count - 2 + scrollView.setContentOffset( + CGPoint( + x: Int(self.labelSize.width) * realLastIndex, + y: 0 + ), + animated: false + ) + } } }