-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6b39649
commit 8007941
Showing
8 changed files
with
468 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
Sources/PageControl/Classes/Drawers/Implementations/BaseDrawer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// | ||
// page-control | ||
// Copyright © 2025 Space Code. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
public class BaseDrawer: IDrawer { | ||
// MARK: Properties | ||
|
||
/// The size of the item, defined by its width and height. | ||
public var size: CGSize | ||
/// The index of the currently selected item, represented as a fractional value to support animations or transitions. | ||
public var currentItem: CGFloat | ||
/// The total number of items (pages) in the drawer. | ||
public var numberOfPages: Int | ||
|
||
/// The space between individual items in the drawer. | ||
let space: CGFloat | ||
/// The default color of the items, used for unselected items. | ||
let itemColor: UIColor | ||
/// The color of the currently selected item, often more prominent to indicate focus. | ||
let selectedItemColor: UIColor | ||
/// The corner radius of the items, used to give them rounded edges. | ||
let radius: CGFloat | ||
|
||
public var contentSize: CGSize { | ||
CGSize( | ||
width: CGFloat(numberOfPages - 1) * size.width + CGFloat(numberOfPages - 1) * space, | ||
height: size.height + 16.0 | ||
) | ||
} | ||
|
||
// MARK: Initialization | ||
|
||
/// Initializes a new instance of `BaseDrawer` with customizable properties. | ||
/// - Parameters: | ||
/// - currentItem: The initial index of the selected item. Defaults to `0.0`. | ||
/// - numberOfPages: The total number of items (pages). Defaults to `5`. | ||
/// - space: The space between individual items in the drawer. Defaults to `4.0`. | ||
/// - width: The width of each item. Defaults to `16.0`. | ||
/// - height: The height of each item. Defaults to `3.0`. | ||
/// - itemColor: The default color for unselected items. Defaults to `UIColor.lightGray`. | ||
/// - selectedItemColor: The color for the selected item. Defaults to a semi-transparent blue. | ||
/// - radius: The corner radius of the items for rounded edges. Defaults to `2`. | ||
public init( | ||
currentItem: CGFloat = .zero, | ||
numberOfPages: Int = .zero, | ||
space: CGFloat = 4.0, | ||
width: CGFloat = 16.0, | ||
height: CGFloat = 3.0, | ||
itemColor: UIColor = .lightGray, | ||
selectedItemColor: UIColor = .blue.withAlphaComponent(0.8), | ||
radius: CGFloat = 2 | ||
) { | ||
size = CGSize(width: width, height: height) | ||
self.currentItem = currentItem | ||
self.numberOfPages = numberOfPages | ||
self.space = space | ||
self.itemColor = itemColor | ||
self.selectedItemColor = selectedItemColor | ||
self.radius = radius | ||
} | ||
|
||
// MARK: IDrawer | ||
|
||
public func draw(_: CGRect) {} | ||
|
||
// MARK: Internal | ||
|
||
/// Calculates the horizontal center position for an item in a layout. | ||
/// | ||
/// - Parameters: | ||
/// - rect: The bounding rectangle of the container. | ||
/// - position: The item's position index (e.g., 0 for the first item). | ||
/// - size: The width of the item. | ||
/// - space: The space between items. | ||
/// - numberOfPages: The total number of items (pages) in the layout. | ||
/// | ||
/// - Returns: The `x` coordinate for the center of the item. | ||
func centerX( | ||
_ rect: CGRect, | ||
position: CGFloat, | ||
size: CGFloat, | ||
space: CGFloat, | ||
numberOfPages: Int | ||
) -> CGFloat { | ||
let dotPosition = (position * (size + space)) | ||
let midX = rect.size.width / 2.0 | ||
let midXWithSpaces = ((CGFloat(numberOfPages) * (size + (space - 1))) / 2.0) | ||
|
||
return dotPosition - midXWithSpaces + midX | ||
} | ||
|
||
/// Calculates the vertical center position for an item in a layout. | ||
/// | ||
/// - Parameters: | ||
/// - rect: The bounding rectangle of the container. | ||
/// - size: The height of the item. | ||
/// | ||
/// - Returns: The `y` coordinate for the center of the item. | ||
func centerY(_ rect: CGRect, size: CGFloat) -> CGFloat { | ||
let midY = rect.size.height / 2.0 | ||
let midDotY = size / 2.0 | ||
let centerY = midY - midDotY | ||
|
||
return centerY | ||
} | ||
|
||
/// Draws a rounded rectangular item with optional border and fill color. | ||
/// | ||
/// - Parameters: | ||
/// - rect: The rectangle defining the item's position and size. | ||
/// - radius: The corner radius for the item's rounded edges. | ||
/// - color: The fill color for the item. | ||
/// - borderWidth: The width of the item's border. Defaults to `0`. | ||
/// - borderColor: The color of the item's border. Defaults to `.clear`. | ||
func drawItem( | ||
_ rect: CGRect, | ||
radius: CGFloat, | ||
color: UIColor, | ||
borderWidth: CGFloat = .zero, | ||
borderColor: UIColor = .clear | ||
) { | ||
let path = UIBezierPath(roundedRect: rect, cornerRadius: radius) | ||
path.lineWidth = borderWidth | ||
borderColor.setStroke() | ||
path.stroke() | ||
color.setFill() | ||
path.fill() | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
Sources/PageControl/Classes/Drawers/Implementations/ExtendedLineDrawer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// | ||
// page-control | ||
// Copyright © 2025 Space Code. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
// MARK: - ExtendedLineDrawer | ||
|
||
public final class ExtendedLineDrawer: BaseDrawer { | ||
// MARK: Override | ||
|
||
override public func draw(_ rect: CGRect) { | ||
drawIndicators(rect) | ||
drawCurrentItem(rect) | ||
} | ||
|
||
override public var contentSize: CGSize { | ||
CGSize( | ||
width: selectedItemWidth + CGFloat(numberOfPages - 1) * size.width + CGFloat(numberOfPages - 1) * space, | ||
height: size.height + .extraSpace | ||
) | ||
} | ||
|
||
// MARK: Private | ||
|
||
// swiftlint:disable:next function_body_length | ||
private func drawIndicators(_ rect: CGRect) { | ||
let step = (space + size.width) | ||
|
||
for index in 0 ... numberOfPages { | ||
if index != Int(currentItem + 1), index != Int(currentItem) { | ||
var newX: CGFloat = .zero | ||
var newY: CGFloat = .zero | ||
var newHeight: CGFloat = .zero | ||
var newWidth: CGFloat = .zero | ||
|
||
let progress = currentItem - floor(currentItem) | ||
|
||
var itemColor = itemColor | ||
|
||
if index == Int(currentItem + 2) { | ||
itemColor = (self.itemColor * Double(1 - progress)) + (selectedItemColor * Double(progress)) | ||
|
||
let centerY = centerY(rect, size: size.height) | ||
|
||
let currentProgress = currentItem - floor(currentItem) | ||
let currentPosition = floor(currentItem + 2) - currentProgress | ||
|
||
let x = centerX( | ||
rect, | ||
position: currentPosition, | ||
size: size.width, | ||
space: space, | ||
numberOfPages: numberOfPages + 1 | ||
) | ||
|
||
let ratio = 1 - currentProgress | ||
let scale = step - (ratio * step) | ||
|
||
newX = rect.origin.x + x | ||
newY = rect.origin.y + centerY | ||
newWidth = size.width + scale | ||
newHeight = size.height | ||
} else { | ||
let centerY = centerY(rect, size: size.height) | ||
|
||
let x = centerX( | ||
rect, | ||
position: CGFloat(index), | ||
size: size.width, | ||
space: space, | ||
numberOfPages: numberOfPages + 1 | ||
) | ||
|
||
newX = rect.origin.x + x | ||
newY = rect.origin.y + centerY | ||
newWidth = size.width | ||
newHeight = size.height | ||
} | ||
|
||
drawItem( | ||
CGRect( | ||
x: newX, | ||
y: newY, | ||
width: newWidth, | ||
height: newHeight | ||
), | ||
radius: radius, | ||
color: itemColor | ||
) | ||
} | ||
} | ||
} | ||
|
||
private func drawCurrentItem(_ rect: CGRect) { | ||
let progress = currentItem - floor(currentItem) | ||
let color = (itemColor * Double(progress)) + (selectedItemColor * Double(1 - progress)) | ||
|
||
if currentItem >= 0 { | ||
let step = (space + size.width) | ||
let centerY = centerY(rect, size: size.height) | ||
let position = floor(currentItem) | ||
|
||
let centerX = centerX( | ||
rect, | ||
position: position, | ||
size: size.width, | ||
space: space, | ||
numberOfPages: numberOfPages + 1 | ||
) | ||
|
||
let rect = CGRect( | ||
x: rect.origin.x + centerX, | ||
y: rect.origin.y + centerY, | ||
width: selectedItemWidth, | ||
height: size.height | ||
) | ||
|
||
drawItem(rect, radius: radius, color: color) | ||
} | ||
} | ||
|
||
private var selectedItemWidth: CGFloat { | ||
let step = (space + size.width) | ||
let currentProgress = currentItem - floor(currentItem) | ||
let ratio = 1 - currentProgress | ||
let desiredWidth = size.width + ratio * step | ||
|
||
return desiredWidth | ||
} | ||
} | ||
|
||
// MARK: Constants | ||
|
||
private extension CGFloat { | ||
static let extraSpace: CGFloat = 16.0 | ||
} |
30 changes: 30 additions & 0 deletions
30
Sources/PageControl/Classes/Drawers/Interfaces/IDrawer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// | ||
// page-control | ||
// Copyright © 2025 Space Code. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
/// A protocol that defines the behavior for drawing a custom UI component, such as a pager or a carousel. | ||
public protocol IDrawer { | ||
/// The index or position of the current item being displayed. | ||
/// This property determines which item is currently in focus. | ||
var currentItem: CGFloat { get set } | ||
|
||
/// The size of each item in the drawer. | ||
/// This could represent the width or height, depending on the layout. | ||
var size: CGSize { get set } | ||
|
||
/// The total number of pages or items in the drawer. | ||
/// Used to calculate the range of drawable items or manage pagination logic. | ||
var numberOfPages: Int { get set } | ||
|
||
/// The content size of the element. | ||
var contentSize: CGSize { get } | ||
|
||
/// A method responsible for drawing the content within the specified rectangle. | ||
/// | ||
/// - Parameter rect: The area in which the content should be drawn. | ||
/// This is typically provided by the rendering system (e.g., a `UIView` or `CALayer`). | ||
func draw(_ rect: CGRect) | ||
} |
58 changes: 58 additions & 0 deletions
58
Sources/PageControl/Classes/Helpers/Extensions/UIColor+.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// | ||
// page-control | ||
// Copyright © 2025 Space Code. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
extension UIColor { | ||
/// Adds two `UIColor` objects together. | ||
/// | ||
/// - Parameters: | ||
/// - color1: The first `UIColor`. | ||
/// - color2: The second `UIColor`. | ||
/// | ||
/// - Returns: A new `UIColor` representing the sum of the two colors. | ||
/// The resulting color's components are clamped to a maximum of 1.0. | ||
/// If either color cannot be decomposed into RGBA components, `.clear` is returned. | ||
static func + (color1: UIColor, color2: UIColor) -> UIColor { | ||
var (r1, g1, b1, a1) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0)) | ||
var (r2, g2, b2, a2) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0)) | ||
|
||
guard color1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1), | ||
color2.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) | ||
else { | ||
return .clear | ||
} | ||
|
||
return UIColor( | ||
red: min(r1 + r2, 1.0), | ||
green: min(g1 + g2, 1.0), | ||
blue: min(b1 + b2, 1.0), | ||
alpha: (a1 + a2) / 2 | ||
) | ||
} | ||
|
||
/// Multiplies a `UIColor`'s RGB components by a scalar multiplier. | ||
/// | ||
/// - Parameters: | ||
/// - color: The `UIColor` to modify. | ||
/// - multiplier: The scalar multiplier applied to the RGB components. Values are clamped between 0 and 1. | ||
/// | ||
/// - Returns: A new `UIColor` with modified RGB components. The alpha remains unchanged. | ||
/// If the color cannot be decomposed into RGBA components, `.clear` is returned. | ||
static func * (color: UIColor, multiplier: CGFloat) -> UIColor { | ||
var (r, g, b, a) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0)) | ||
|
||
guard color.getRed(&r, green: &g, blue: &b, alpha: &a) else { | ||
return .clear | ||
} | ||
|
||
return UIColor( | ||
red: min(max(r * multiplier, 0), 1.0), | ||
green: min(max(g * multiplier, 0), 1.0), | ||
blue: min(max(b * multiplier, 0), 1.0), | ||
alpha: a | ||
) | ||
} | ||
} |
Oops, something went wrong.