Skip to content

Commit

Permalink
v1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tkarlz committed Dec 3, 2023
0 parents commit 58e9904
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
26 changes: 26 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "BriefPagingControl",
platforms: [
.iOS(.v14)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "BriefPagingControl",
targets: ["BriefPagingControl"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "BriefPagingControl"),
.testTarget(
name: "BriefPagingControlTests",
dependencies: ["BriefPagingControl"]),
]
)
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# BriefPagingControl

BriefPageControl is similar to Instagram's PageControl, and SwiftUI is used.

You can use touch and drag of UIPageControl. You can also set the size, color and animation.

![Examples1](_Images/Examples1.png)
![Examples2](_Images/Examples2.png)
![Examples3](_Images/Examples3.png)
![Examples4](_Images/Examples4.png)

## Installation

Supports iOS14 or later.

### Swift Package Manager

1. In Xcode, select “File” → “Add Packages...”
1. Enter https://github.com/tkarlz/BriefPagingControl.git

or you can add the following dependency to your `Package.swift`:
```swift
.package(url: "https://github.com/tkarlz/BriefPagingControl.git", .upToNextMajor(from: "1.0")),
```

## Usage

### Simply

```swift
BriefPagingControl(numberOfPages: pages.count, currentPage: $currentPage)
```

### You Want

```swift
BriefPagingControl(numberOfPages: pages.count, currentPage: $currentPage) { config in
config.indicatorSize = 10
config.spacing = 10
config.currentIndicatorColor = .red
config.indicatorColor = .orange
config.numberOfMainIndicators = .five
config.hidesForSinglePage = true
config.animation = .snappy
}
```

### Default Confing

```swift
indicatorSize = 8
spacing = 8
currentIndicatorColor = .primary
indicatorColor = .gray.opacity(0.6)
numberOfMainIndicators = .three
hidesForSinglePage = false
animation = .default
```
223 changes: 223 additions & 0 deletions Sources/BriefPagingControl/BriefPagingControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//
// BriefPagingControl
//
// Created by Youngkyu Seo on 12/3/23.
//

import SwiftUI

public struct BriefPagingControl: View {

private let numberOfPages: Int
@Binding private var currentPage: Int

private let indicatorSize: CGFloat
private let spacing: CGFloat
private let currentIndicatorColor: Color
private let indicatorColor: Color
private let numberOfMainIndicators: IndicatorType
private let hidesForSinglePage: Bool
private let animation: Animation

@State private var activePage: Int
@State private var displayedPosition: Int
@State private var adjustedOffset: CGFloat


public init(numberOfPages: Int, currentPage: Binding<Int>, setup: ((inout PagingControlConfig) -> Void)? = nil) {

self.numberOfPages = numberOfPages
self._currentPage = currentPage

var config = PagingControlConfig()
setup?(&config)

self.indicatorSize = config.indicatorSize
self.spacing = config.spacing
self.currentIndicatorColor = config.currentIndicatorColor
self.indicatorColor = config.indicatorColor
self.numberOfMainIndicators = config.numberOfMainIndicators
self.hidesForSinglePage = config.hidesForSinglePage
self.animation = config.animation

let activePage = currentPage.wrappedValue
let displayedPosition = numberOfMainIndicators.initialPosition(of: activePage, numberOfPages: numberOfPages)
let basicOffset = CGFloat(numberOfPages - numberOfMainIndicators.rawValue) * (indicatorSize + spacing) / 2
let adjustedOffset = basicOffset - CGFloat(activePage - displayedPosition - numberOfMainIndicators.halfValue) * (indicatorSize + spacing)

self.activePage = activePage
self.displayedPosition = displayedPosition
self.adjustedOffset = adjustedOffset
}

public var body: some View {

VStack {
if numberOfPages > numberOfMainIndicators.rawValue {

HStack(spacing: spacing) {
ForEach(0..<numberOfPages, id: \.self) { page in
let size = calculateSize(page)

Circle()
.fill(activePage == page ? currentIndicatorColor : indicatorColor)
.frame(width: size, height: size)
.frame(width: indicatorSize, height: indicatorSize, alignment: .center)
}
}
.offset(x: adjustedOffset)
.frame(width: (indicatorSize + spacing) * CGFloat(numberOfMainIndicators.rawValue + 4 + 1))
.onChange(of: currentPage) { [previousPage = currentPage] nextPage in
withAnimation(animation) {
if previousPage < nextPage {
if displayedPosition + 1 > numberOfMainIndicators.halfValue {
activePage += 1
adjustedOffset -= (indicatorSize + spacing)
} else {
activePage += 1
displayedPosition += 1
}
} else {
if displayedPosition - 1 < -numberOfMainIndicators.halfValue {
activePage -= 1
adjustedOffset += (indicatorSize + spacing)
} else {
activePage -= 1
displayedPosition -= 1
}
}
}
}
} else {

HStack(spacing: spacing) {
ForEach(0..<numberOfPages, id: \.self) { page in
Circle()
.fill(activePage == page ? currentIndicatorColor : indicatorColor)
.frame(width: indicatorSize, height: indicatorSize)
}
}
.padding(.horizontal, (indicatorSize + spacing) / 2)
.onChange(of: currentPage) { [previousPage = currentPage] nextPage in
withAnimation(animation) {
if previousPage < nextPage {
activePage += 1
} else {
activePage -= 1
}
}
}
}
}
.opacity(hidesForSinglePage && numberOfPages == 1 ? 0 : 1)
.padding(.vertical, indicatorSize / 2)
.overlay (
PagingController(numberOfPages: numberOfPages, activePage: $currentPage)
)
}

private func calculateSize(_ value: Int) -> CGFloat {
let base = UInt(numberOfMainIndicators.halfValue)

switch (activePage - displayedPosition - value).magnitude {
case ...base:
return indicatorSize
case base + 1:
return indicatorSize * 2 / 3
case base + 2:
return indicatorSize / 3
default:
return 0
}
}
}

public enum IndicatorType: Int {

case three = 3
case five = 5

fileprivate var halfValue: Int {
rawValue / 2
}

fileprivate func initialPosition(of value: Int, numberOfPages: Int) -> Int {

switch self {
case .three:
switch value {
case 0: -1
case numberOfPages - 1: 1
default: 0
}
case .five:
switch value {
case 0: -2
case 1: -1
case numberOfPages - 2: 1
case numberOfPages - 1: 2
default: 0
}
}
}
}

public struct PagingControlConfig {

public var indicatorSize: CGFloat
public var spacing: CGFloat
public var currentIndicatorColor: Color
public var indicatorColor: Color
public var numberOfMainIndicators: IndicatorType
public var hidesForSinglePage: Bool
public var animation: Animation

fileprivate init() {
self.indicatorSize = 8
self.spacing = 8
self.currentIndicatorColor = .primary
self.indicatorColor = .gray.opacity(0.6)
self.numberOfMainIndicators = .three
self.hidesForSinglePage = false
self.animation = .default
}
}

private struct PagingController: UIViewRepresentable {

let numberOfPages: Int
@Binding var activePage: Int

func makeUIView(context: Context) -> UIPageControl {
let view = UIPageControl()
view.numberOfPages = numberOfPages
view.currentPage = activePage
view.backgroundStyle = .minimal
view.currentPageIndicatorTintColor = .clear
view.pageIndicatorTintColor = .clear
view.addTarget(context.coordinator, action: #selector(Coordinator.onPageUpdate(control:)), for: .valueChanged)
return view
}

func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.numberOfPages = numberOfPages
uiView.currentPage = activePage
}

func makeCoordinator() -> Coordinator {
.init(activePage: $activePage)
}

class Coordinator: NSObject {
@Binding var activePage: Int

init(activePage: Binding<Int>) {
self._activePage = activePage
}

@objc
func onPageUpdate(control: UIPageControl) {
activePage = control.currentPage
}
}
}
10 changes: 10 additions & 0 deletions Tests/BriefPagingControlTests/PagingControlTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import XCTest
@testable import BriefPagingControl

final class PagingControlTests: XCTestCase {

func testExample() throws {
let app = XCUIApplication()
app.launch()
}
}
Binary file added _Images/Examples1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added _Images/Examples2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added _Images/Examples3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added _Images/Examples4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 58e9904

Please sign in to comment.