Digital ACT-191 ONEAPP-7013 story: refactored code
This commit is contained in:
parent
668b20f77b
commit
8619c64109
@ -183,13 +183,11 @@ open class Carousel: View {
|
|||||||
|
|
||||||
/// If provided, will set the alignment for slot content when the slots has different heights.
|
/// If provided, will set the alignment for slot content when the slots has different heights.
|
||||||
open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } }
|
open var slotAlignment: [CarouselSlotAlignmentModel] = [] { didSet { setNeedsUpdate() } }
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Private Properties
|
// MARK: - Private Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// Sizes are from InVision design specs.
|
internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) }
|
||||||
internal var containerSize: CGSize { CGSize(width: 320, height: 44) }
|
|
||||||
|
|
||||||
private let contentStackView = UIStackView().with {
|
private let contentStackView = UIStackView().with {
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
$0.axis = .vertical
|
$0.axis = .vertical
|
||||||
@ -260,7 +258,7 @@ open class Carousel: View {
|
|||||||
private var prevButtonLeadingConstraint: NSLayoutConstraint?
|
private var prevButtonLeadingConstraint: NSLayoutConstraint?
|
||||||
private var nextButtonTrailingConstraint: NSLayoutConstraint?
|
private var nextButtonTrailingConstraint: NSLayoutConstraint?
|
||||||
private var containerLeadingConstraint: NSLayoutConstraint?
|
private var containerLeadingConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile.
|
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile.
|
||||||
let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
|
let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
|
||||||
|
|
||||||
@ -268,18 +266,21 @@ open class Carousel: View {
|
|||||||
var peekMinimum = 24.0
|
var peekMinimum = 24.0
|
||||||
var minimumSlotWidth = 0.0
|
var minimumSlotWidth = 0.0
|
||||||
var carouselScrollbarMinWidth = 96.0
|
var carouselScrollbarMinWidth = 96.0
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
/// Executed on initialization for this View.
|
||||||
open override func initialSetup() {
|
open override func initialSetup() {
|
||||||
super.initialSetup()
|
super.initialSetup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||||
open override func setup() {
|
open override func setup() {
|
||||||
super.setup()
|
super.setup()
|
||||||
isAccessibilityElement = false
|
isAccessibilityElement = false
|
||||||
|
|
||||||
// add containerView
|
// Add containerView
|
||||||
addSubview(containerView)
|
addSubview(containerView)
|
||||||
containerView
|
containerView
|
||||||
.pinTop()
|
.pinTop()
|
||||||
@ -287,18 +288,19 @@ open class Carousel: View {
|
|||||||
.pinLeadingGreaterThanOrEqualTo()
|
.pinLeadingGreaterThanOrEqualTo()
|
||||||
.pinTrailing()
|
.pinTrailing()
|
||||||
.heightGreaterThanEqualTo(containerSize.height)
|
.heightGreaterThanEqualTo(containerSize.height)
|
||||||
|
|
||||||
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
|
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
|
||||||
containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0)
|
containerLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 0)
|
||||||
containerLeadingConstraint?.activate()
|
containerLeadingConstraint?.activate()
|
||||||
|
|
||||||
// add content stackview
|
// Add content stackview
|
||||||
containerView.addSubview(contentStackView)
|
containerView.addSubview(contentStackView)
|
||||||
|
|
||||||
// add scrollview
|
// Add scrollview
|
||||||
scrollContainerView.addSubview(scrollView)
|
scrollContainerView.addSubview(scrollView)
|
||||||
scrollView.pinToSuperView()
|
scrollView.pinToSuperView()
|
||||||
|
|
||||||
// add pagination button icons
|
// Add pagination button icons
|
||||||
scrollContainerView.addSubview(previousButton)
|
scrollContainerView.addSubview(previousButton)
|
||||||
previousButton
|
previousButton
|
||||||
.pinLeadingGreaterThanOrEqualTo()
|
.pinLeadingGreaterThanOrEqualTo()
|
||||||
@ -309,7 +311,7 @@ open class Carousel: View {
|
|||||||
.pinTrailingLessThanOrEqualTo()
|
.pinTrailingLessThanOrEqualTo()
|
||||||
.pinCenterY()
|
.pinCenterY()
|
||||||
|
|
||||||
// add scroll container view & carousel scrollbar
|
// Add scroll container view & carousel scrollbar
|
||||||
contentStackView.addArrangedSubview(scrollContainerView)
|
contentStackView.addArrangedSubview(scrollContainerView)
|
||||||
contentStackView.addArrangedSubview(carouselScrollBar)
|
contentStackView.addArrangedSubview(carouselScrollBar)
|
||||||
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
|
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
|
||||||
@ -322,7 +324,8 @@ open class Carousel: View {
|
|||||||
|
|
||||||
addlisteners()
|
addlisteners()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used to make changes to the View based off a change events or from local properties.
|
||||||
open override func updateView() {
|
open override func updateView() {
|
||||||
super.updateView()
|
super.updateView()
|
||||||
|
|
||||||
@ -353,7 +356,7 @@ open class Carousel: View {
|
|||||||
if peek == .none {
|
if peek == .none {
|
||||||
paginationDisplay = .persistent
|
paginationDisplay = .persistent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
|
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
|
||||||
if UIDevice.isIPad && peek == .minimum {
|
if UIDevice.isIPad && peek == .minimum {
|
||||||
peek = .standard
|
peek = .standard
|
||||||
@ -363,11 +366,12 @@ open class Carousel: View {
|
|||||||
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
|
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
|
||||||
peek = .minimum
|
peek = .minimum
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePaginationControls()
|
updatePaginationControls()
|
||||||
addCarouselSlots()
|
addCarouselSlots()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets to default settings.
|
||||||
open override func reset() {
|
open override func reset() {
|
||||||
super.reset()
|
super.reset()
|
||||||
shouldUpdateView = false
|
shouldUpdateView = false
|
||||||
@ -384,19 +388,69 @@ open class Carousel: View {
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
private func addlisteners() {
|
||||||
|
nextButton.onClick = { _ in self.nextButtonClick() }
|
||||||
|
previousButton.onClick = { _ in self.previousButtonClick() }
|
||||||
|
|
||||||
|
/// Will be called when the thumb move forward.
|
||||||
|
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
|
||||||
|
guard let self else { return }
|
||||||
|
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be called when the thumb move backward.
|
||||||
|
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
|
||||||
|
guard let self else { return }
|
||||||
|
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be called when the thumb touch start.
|
||||||
|
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
|
||||||
|
guard let self else { return }
|
||||||
|
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Will be called when the thumb touch end.
|
||||||
|
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
|
||||||
|
guard let self else { return }
|
||||||
|
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePaginationControls() {
|
||||||
|
containerView.surface = surface
|
||||||
|
showPaginationControls()
|
||||||
|
previousButton.kind = pagination.kind
|
||||||
|
previousButton.floating = pagination.floating
|
||||||
|
nextButton.kind = pagination.kind
|
||||||
|
nextButton.floating = pagination.floating
|
||||||
|
previousButton.surface = surface
|
||||||
|
nextButton.surface = surface
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showPaginationControls() {
|
||||||
|
if carouselScrollBar.numberOfSlides == _layout.value {
|
||||||
|
previousButton.isHidden = true
|
||||||
|
nextButton.isHidden = true
|
||||||
|
} else {
|
||||||
|
previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none)
|
||||||
|
nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func addCarouselSlots() {
|
private func addCarouselSlots() {
|
||||||
getSlotWidth()
|
getSlotWidth()
|
||||||
if containerView.frame.size.width > 0 {
|
if containerView.frame.size.width > 0 {
|
||||||
containerViewHeightConstraint?.isActive = false
|
containerViewHeightConstraint?.isActive = false
|
||||||
containerStackHeightConstraint?.isActive = false
|
containerStackHeightConstraint?.isActive = false
|
||||||
|
|
||||||
// perform a loop to iterate each subView
|
// Perform a loop to iterate each subView
|
||||||
scrollView.subviews.forEach { subView in
|
scrollView.subviews.forEach { subView in
|
||||||
// removing subView from its parent view
|
// Removing subView from its parent view
|
||||||
subView.removeFromSuperview()
|
subView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
// add carousel items
|
// Add carousel items
|
||||||
if data.count > 0 {
|
if data.count > 0 {
|
||||||
var xPos = 0.0
|
var xPos = 0.0
|
||||||
for x in 0...data.count - 1 {
|
for x in 0...data.count - 1 {
|
||||||
@ -433,86 +487,7 @@ open class Carousel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ratioSize(for width: CGFloat) -> CGSize {
|
private func getSlotWidth() {
|
||||||
var height: CGFloat = width
|
|
||||||
|
|
||||||
switch aspectRatio {
|
|
||||||
case .ratio1x1:
|
|
||||||
break;
|
|
||||||
case .ratio3x4:
|
|
||||||
height = (4 / 3) * width
|
|
||||||
case .ratio4x3:
|
|
||||||
height = (3 / 4) * width
|
|
||||||
case .ratio2x3:
|
|
||||||
height = (3 / 2) * width
|
|
||||||
case .ratio3x2:
|
|
||||||
height = (2 / 3) * width
|
|
||||||
case .ratio9x16:
|
|
||||||
height = (16 / 9) * width
|
|
||||||
case .ratio16x9:
|
|
||||||
height = (9 / 16) * width
|
|
||||||
case .ratio1x2:
|
|
||||||
height = (2 / 1) * width
|
|
||||||
case .ratio2x1:
|
|
||||||
height = (1 / 2) * width
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return CGSize(width: width, height: height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addlisteners() {
|
|
||||||
nextButton.onClick = { _ in self.nextButtonClick() }
|
|
||||||
previousButton.onClick = { _ in self.previousButtonClick() }
|
|
||||||
|
|
||||||
/// will be called when the thumb move forward.
|
|
||||||
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
|
|
||||||
guard let self else { return }
|
|
||||||
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// will be called when the thumb move backward.
|
|
||||||
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
|
|
||||||
guard let self else { return }
|
|
||||||
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// will be called when the thumb touch start.
|
|
||||||
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
|
|
||||||
guard let self else { return }
|
|
||||||
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// will be called when the thumb touch end.
|
|
||||||
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
|
|
||||||
guard let self else { return }
|
|
||||||
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePaginationControls() {
|
|
||||||
containerView.surface = surface
|
|
||||||
showPaginationControls()
|
|
||||||
previousButton.kind = pagination.kind
|
|
||||||
previousButton.floating = pagination.floating
|
|
||||||
nextButton.kind = pagination.kind
|
|
||||||
nextButton.floating = pagination.floating
|
|
||||||
previousButton.surface = surface
|
|
||||||
nextButton.surface = surface
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePaginationInset() {
|
|
||||||
prevButtonLeadingConstraint?.isActive = false
|
|
||||||
nextButtonTrailingConstraint?.isActive = false
|
|
||||||
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
|
|
||||||
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
|
|
||||||
prevButtonLeadingConstraint?.isActive = true
|
|
||||||
nextButtonTrailingConstraint?.isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSlotWidth() {
|
|
||||||
let actualWidth = containerView.frame.size.width
|
let actualWidth = containerView.frame.size.width
|
||||||
let isScrollbarSuppressed = data.count > 0 && layout.value == data.count
|
let isScrollbarSuppressed = data.count > 0 && layout.value == data.count
|
||||||
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
|
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
|
||||||
@ -538,19 +513,69 @@ open class Carousel: View {
|
|||||||
minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value)
|
minimumSlotWidth = minimumSlotWidth / CGFloat(layout.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextButtonClick() {
|
private func nextButtonClick() {
|
||||||
carouselScrollBar.position = carouselScrollBar.position+1
|
carouselScrollBar.position = carouselScrollBar.position+1
|
||||||
showPaginationControls()
|
showPaginationControls()
|
||||||
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
||||||
}
|
}
|
||||||
|
|
||||||
func previousButtonClick() {
|
private func previousButtonClick() {
|
||||||
carouselScrollBar.position = carouselScrollBar.position-1
|
carouselScrollBar.position = carouselScrollBar.position-1
|
||||||
showPaginationControls()
|
showPaginationControls()
|
||||||
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateScrollPosition(position: Int, callbackText: String) {
|
private func ratioSize(for width: CGFloat) -> CGSize {
|
||||||
|
var height: CGFloat = width
|
||||||
|
|
||||||
|
switch aspectRatio {
|
||||||
|
case .ratio1x1:
|
||||||
|
break;
|
||||||
|
case .ratio3x4:
|
||||||
|
height = (4 / 3) * width
|
||||||
|
case .ratio4x3:
|
||||||
|
height = (3 / 4) * width
|
||||||
|
case .ratio2x3:
|
||||||
|
height = (3 / 2) * width
|
||||||
|
case .ratio3x2:
|
||||||
|
height = (2 / 3) * width
|
||||||
|
case .ratio9x16:
|
||||||
|
height = (16 / 9) * width
|
||||||
|
case .ratio16x9:
|
||||||
|
height = (9 / 16) * width
|
||||||
|
case .ratio1x2:
|
||||||
|
height = (2 / 1) * width
|
||||||
|
case .ratio2x1:
|
||||||
|
height = (1 / 2) * width
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePaginationInset() {
|
||||||
|
prevButtonLeadingConstraint?.isActive = false
|
||||||
|
nextButtonTrailingConstraint?.isActive = false
|
||||||
|
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
|
||||||
|
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
|
||||||
|
prevButtonLeadingConstraint?.isActive = true
|
||||||
|
nextButtonTrailingConstraint?.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
|
||||||
|
let scrollContentSizeWidth = scrollView.contentSize.width
|
||||||
|
let totalPositions = totalPositions()
|
||||||
|
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
|
||||||
|
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
|
||||||
|
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
|
||||||
|
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
|
||||||
|
carouselScrollBar.position = contentPos
|
||||||
|
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScrollPosition(position: Int, callbackText: String) {
|
||||||
if carouselScrollBar.numberOfSlides > 0 {
|
if carouselScrollBar.numberOfSlides > 0 {
|
||||||
let scrollContentSizeWidth = scrollView.contentSize.width
|
let scrollContentSizeWidth = scrollView.contentSize.width
|
||||||
let totalPositions = totalPositions()
|
let totalPositions = totalPositions()
|
||||||
@ -586,30 +611,9 @@ open class Carousel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
|
|
||||||
let scrollContentSizeWidth = scrollView.contentSize.width
|
|
||||||
let totalPositions = totalPositions()
|
|
||||||
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
|
|
||||||
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
|
|
||||||
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
|
|
||||||
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
|
|
||||||
carouselScrollBar.position = contentPos
|
|
||||||
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func totalPositions() -> Int {
|
private func totalPositions() -> Int {
|
||||||
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value)))
|
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(_layout.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func showPaginationControls() {
|
|
||||||
if carouselScrollBar.numberOfSlides == _layout.value {
|
|
||||||
previousButton.isHidden = true
|
|
||||||
nextButton.isHidden = true
|
|
||||||
} else {
|
|
||||||
previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none)
|
|
||||||
nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Carousel: UIScrollViewDelegate {
|
extension Carousel: UIScrollViewDelegate {
|
||||||
@ -619,6 +623,7 @@ extension Carousel: UIScrollViewDelegate {
|
|||||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
|
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
var visibleRect = CGRect()
|
var visibleRect = CGRect()
|
||||||
visibleRect.origin = scrollView.contentOffset
|
visibleRect.origin = scrollView.contentOffset
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user