diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2a8dc0f3a..9b7220317 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; 7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */; }; + 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */; }; 7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */; }; 7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */; }; 7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */; }; @@ -1179,6 +1180,7 @@ 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = ""; }; + 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = ""; }; 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = ""; }; @@ -2587,6 +2589,7 @@ FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */, FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */, 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */, + 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */, ); path = Shared; sourceTree = ""; @@ -5580,6 +5583,7 @@ buildActionMask = 2147483647; files = ( FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */, + 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */, FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */, FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, diff --git a/Session/Media Viewing & Editing/MediaInfoVC.swift b/Session/Media Viewing & Editing/MediaInfoVC.swift index efcf4574e..044d31844 100644 --- a/Session/Media Viewing & Editing/MediaInfoVC.swift +++ b/Session/Media Viewing & Editing/MediaInfoVC.swift @@ -14,17 +14,27 @@ final class MediaInfoVC: BaseVC { private lazy var mediaInfoView: MediaInfoView = MediaInfoView(attachment: nil) private lazy var mediaCarouselView: SessionCarouselView = { let result: SessionCarouselView = SessionCarouselView( - slices: self.attachments.map { - MediaPreviewView( - attachment: $0, - isOutgoing: self.isOutgoing + info: SessionCarouselView.Info( + slices: self.attachments.map { + MediaPreviewView( + attachment: $0, + isOutgoing: self.isOutgoing + ) + }, + sliceSize: CGSize( + width: Self.mediaSize, + height: Self.mediaSize + ), + shouldShowPageControl: true, + pageControlHeight: 10, + shouldShowArrows: true, + arrowsSize: CGSize( + width: 20, + height: 30 ) - }, - sliceSize: CGSize( - width: Self.mediaSize, - height: Self.mediaSize ) ) + result.set(.height, to: Self.mediaSize) return result }() @@ -63,9 +73,11 @@ final class MediaInfoVC: BaseVC { let stackView: UIStackView = UIStackView(arrangedSubviews: [ mediaCarouselView, mediaInfoView ]) stackView.axis = .vertical + stackView.alignment = .center stackView.spacing = Values.largeSpacing self.view.addSubview(stackView) - stackView.center(in: self.view) + stackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self.view) + stackView.center(.vertical, in: self.view) } } diff --git a/Session/Shared/SessionCarouselView+Info.swift b/Session/Shared/SessionCarouselView+Info.swift new file mode 100644 index 000000000..8aef9cda8 --- /dev/null +++ b/Session/Shared/SessionCarouselView+Info.swift @@ -0,0 +1,39 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +extension SessionCarouselView { + public struct Info { + let slices: [UIView] + let sliceSize: CGSize + let sliceCount: Int + let shouldShowPageControl: Bool + let pageControlHeight: CGFloat + let pageControlScale: CGFloat // This is to control the size of the dots + let shouldShowArrows: Bool + let arrowsSize: CGSize + + // MARK: - Initialization + + init( + slices: [UIView] = [], + sliceSize: CGSize = .zero, + shouldShowPageControl: Bool = true, + pageControlHeight: CGFloat = 0, + pageControlScale: CGFloat = 1, + shouldShowArrows: Bool = true, + arrowsSize: CGSize = .zero + ) { + self.slices = slices + self.sliceSize = sliceSize + self.sliceCount = slices.count + self.shouldShowPageControl = shouldShowPageControl && (self.sliceCount > 1) + self.pageControlHeight = pageControlHeight + self.pageControlScale = pageControlScale + self.shouldShowArrows = shouldShowArrows && (self.sliceCount > 1) + self.arrowsSize = arrowsSize + } + } +} diff --git a/Session/Shared/SessionCarouselView.swift b/Session/Shared/SessionCarouselView.swift index f8c1f9747..39accc837 100644 --- a/Session/Shared/SessionCarouselView.swift +++ b/Session/Shared/SessionCarouselView.swift @@ -1,18 +1,12 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit +import SessionUIKit +import SessionUtilitiesKit final class SessionCarouselView: UIView, UIScrollViewDelegate { private let slicesForLoop: [UIView] - private let sliceSize: CGSize - private let sliceCount: Int - - // MARK: - Settings - public var showPageControl: Bool = true { - didSet { - self.pageControl.isHidden = !showPageControl - } - } + private let info: SessionCarouselView.Info // MARK: - UI private lazy var scrollView: UIScrollView = { @@ -22,8 +16,8 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { result.showsHorizontalScrollIndicator = false result.showsVerticalScrollIndicator = false result.contentSize = CGSize( - width: self.sliceSize.width * CGFloat(self.slicesForLoop.count), - height: self.sliceSize.height + width: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count), + height: self.info.sliceSize.height ) return result @@ -31,23 +25,52 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { private lazy var pageControl: UIPageControl = { let result: UIPageControl = UIPageControl() - result.numberOfPages = self.sliceCount + result.numberOfPages = self.info.sliceCount result.currentPage = 0 + result.isHidden = !self.info.shouldShowPageControl + result.set(.height, to: self.info.pageControlHeight) + result.transform = CGAffineTransform(scaleX: self.info.pageControlScale, y: self.info.pageControlScale) + + return result + }() + + private lazy var arrowLeft: UIButton = { + let result = UIButton(type: .custom) + result.setImage(UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate), for: .normal) + result.addTarget(self, action: #selector(scrollToPreviousSlice), for: .touchUpInside) + result.themeTintColor = .textPrimary + result.set(.width, to: self.info.arrowsSize.width) + result.set(.height, to: self.info.arrowsSize.height) + result.isHidden = !self.info.shouldShowArrows + + return result + }() + + private lazy var arrowRight: UIButton = { + let result = UIButton(type: .custom) + result.setImage(UIImage(systemName: "chevron.right")?.withRenderingMode(.alwaysTemplate), for: .normal) + result.addTarget(self, action: #selector(scrollToNextSlice), for: .touchUpInside) + result.themeTintColor = .textPrimary + result.set(.width, to: self.info.arrowsSize.width) + result.set(.height, to: self.info.arrowsSize.height) + result.isHidden = !self.info.shouldShowArrows return result }() // MARK: - Lifecycle - init(slices: [UIView], sliceSize: CGSize) { - self.sliceCount = slices.count - if self.sliceCount > 1, let copyOfFirstSlice: UIView = slices.first?.copyView(), let copyOfLastSlice: UIView = slices.last?.copyView() { + init(info: SessionCarouselView.Info) { + self.info = info + if self.info.sliceCount > 1, + let copyOfFirstSlice: UIView = self.info.slices.first?.copyView(), + let copyOfLastSlice: UIView = self.info.slices.last?.copyView() + { self.slicesForLoop = [copyOfLastSlice] - .appending(contentsOf: slices) + .appending(contentsOf: self.info.slices) .appending(copyOfFirstSlice) } else { - self.slicesForLoop = slices + self.slicesForLoop = self.info.slices } - self.sliceSize = sliceSize super.init(frame: CGRect.zero) setUpViewHierarchy() @@ -62,19 +85,22 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { } private func setUpViewHierarchy() { + set(.width, to: self.info.sliceSize.width + Values.largeSpacing + 2 * self.info.arrowsSize.width) + set(.height, to: self.info.sliceSize.height) + let stackView: UIStackView = UIStackView(arrangedSubviews: self.slicesForLoop) stackView.axis = .horizontal - stackView.set(.width, to: self.sliceSize.width * CGFloat(self.slicesForLoop.count)) - stackView.set(.height, to: self.sliceSize.height) + stackView.set(.width, to: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count)) + stackView.set(.height, to: self.info.sliceSize.height) addSubview(self.scrollView) - scrollView.pin(to: self) - scrollView.set(.width, to: self.sliceSize.width) - scrollView.set(.height, to: self.sliceSize.height) + scrollView.center(in: self) + scrollView.set(.width, to: self.info.sliceSize.width) + scrollView.set(.height, to: self.info.sliceSize.height) scrollView.addSubview(stackView) scrollView.setContentOffset( CGPoint( - x: Int(self.sliceSize.width) * (self.sliceCount > 1 ? 1 : 0), + x: Int(self.info.sliceSize.width) * (self.info.sliceCount > 1 ? 1 : 0), y: 0 ), animated: false @@ -83,13 +109,21 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { addSubview(self.pageControl) self.pageControl.center(.horizontal, in: self) self.pageControl.pin(.bottom, to: .bottom, of: self) + + addSubview(self.arrowLeft) + self.arrowLeft.pin(.leading, to: .leading, of: self) + self.arrowLeft.center(.vertical, in: self) + + addSubview(self.arrowRight) + self.arrowRight.pin(.trailing, to: .trailing, of: self) + self.arrowRight.center(.vertical, in: self) } // MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { let pageIndex: Int = { - let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/sliceSize.width)) - if self.sliceCount > 1 { + let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/self.info.sliceSize.width)) + if self.info.sliceCount > 1 { if maybeCurrentPageIndex == 0 { return pageControl.numberOfPages - 1 } @@ -108,7 +142,7 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { if pageControl.currentPage == 0 { scrollView.setContentOffset( CGPoint( - x: Int(self.sliceSize.width) * 1, + x: Int(self.info.sliceSize.width) * 1, y: 0 ), animated: false @@ -119,11 +153,32 @@ final class SessionCarouselView: UIView, UIScrollViewDelegate { let realLastIndex: Int = self.slicesForLoop.count - 2 scrollView.setContentOffset( CGPoint( - x: Int(self.sliceSize.width) * realLastIndex, + x: Int(self.info.sliceSize.width) * realLastIndex, y: 0 ), animated: false ) } } + + // MARK: - Interaction + @objc func scrollToNextSlice() { + self.scrollView.setContentOffset( + CGPoint( + x: self.scrollView.contentOffset.x + self.info.sliceSize.width, + y: 0 + ), + animated: true + ) + } + + @objc func scrollToPreviousSlice() { + self.scrollView.setContentOffset( + CGPoint( + x: self.scrollView.contentOffset.x - self.info.sliceSize.width, + y: 0 + ), + animated: true + ) + } }