Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use DebounceButton for the crop button in the CropToolbar #425

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Mantis.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
F24DCFEC29AF2A6000D8E8C1 /* CropAuxiliaryIndicatorView+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24DCFEB29AF2A6000D8E8C1 /* CropAuxiliaryIndicatorView+Accessibility.swift */; };
F251B36E299B9A52006325B0 /* CropWorkbenchViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F251B36D299B9A52006325B0 /* CropWorkbenchViewTests.swift */; };
F28FF64A2C4A492A002F5F06 /* CropAuxiliaryIndicatorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28FF6492C4A492A002F5F06 /* CropAuxiliaryIndicatorConfig.swift */; };
F2AB98582CDC147F003C7CC2 /* DebounceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB98572CDC147F003C7CC2 /* DebounceButton.swift */; };
F2AD53412A46DB8B00AE9C87 /* ImageAutoAdjustHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AD53402A46DB8B00AE9C87 /* ImageAutoAdjustHelper.swift */; };
F2B4FDA42A3B661B00667F22 /* SlideDial.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B4FDA32A3B661B00667F22 /* SlideDial.swift */; };
F2B4FDA72A3B69B600667F22 /* SlideRulerPositionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B4FDA52A3B69B600667F22 /* SlideRulerPositionHelper.swift */; };
Expand Down Expand Up @@ -213,6 +214,7 @@
F24DCFEB29AF2A6000D8E8C1 /* CropAuxiliaryIndicatorView+Accessibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CropAuxiliaryIndicatorView+Accessibility.swift"; sourceTree = "<group>"; };
F251B36D299B9A52006325B0 /* CropWorkbenchViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropWorkbenchViewTests.swift; sourceTree = "<group>"; };
F28FF6492C4A492A002F5F06 /* CropAuxiliaryIndicatorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropAuxiliaryIndicatorConfig.swift; sourceTree = "<group>"; };
F2AB98572CDC147F003C7CC2 /* DebounceButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceButton.swift; sourceTree = "<group>"; };
F2AD53402A46DB8B00AE9C87 /* ImageAutoAdjustHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAutoAdjustHelper.swift; sourceTree = "<group>"; };
F2B4FDA32A3B661B00667F22 /* SlideDial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideDial.swift; sourceTree = "<group>"; };
F2B4FDA52A3B69B600667F22 /* SlideRulerPositionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideRulerPositionHelper.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -380,6 +382,7 @@
OBJ_23 /* CropViewController */ = {
isa = PBXGroup;
children = (
F2AB98572CDC147F003C7CC2 /* DebounceButton.swift */,
OBJ_24 /* CropToolbar.swift */,
OBJ_25 /* CropViewController.swift */,
OBJ_26 /* FixedRatioManager.swift */,
Expand Down Expand Up @@ -756,6 +759,7 @@
OBJ_75 /* CropView+UIScrollViewDelegate.swift in Sources */,
664A7E9F2884606800DF7C79 /* Global.swift in Sources */,
F24DCFEC29AF2A6000D8E8C1 /* CropAuxiliaryIndicatorView+Accessibility.swift in Sources */,
F2AB98582CDC147F003C7CC2 /* DebounceButton.swift in Sources */,
66F4398E2978F6E400728B52 /* UIViewExtensions.swift in Sources */,
OBJ_76 /* CropView.swift in Sources */,
660D4F57298CDA8C00C4E11A /* TestHelper.swift in Sources */,
Expand Down
21 changes: 15 additions & 6 deletions Sources/Mantis/CropViewController/CropToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,21 @@ public final class CropToolbar: UIView, CropToolbarProtocol {
return button
}()

private lazy var cropButton: UIButton = {
private lazy var cropButton: DebounceButton = {
if let icon = iconProvider?.getCropIcon() {
let button = createOptionButton(withTitle: nil, andAction: #selector(crop))
let button: DebounceButton = createOptionButton(withTitle: nil)
button.setImage(icon, for: .normal)
button.setCropOperation { [weak self] in
self?.crop(button)
}
return button
}

let doneText = LocalizedHelper.getString("Mantis.Done", value: "Done")
let button = createOptionButton(withTitle: doneText, andAction: #selector(crop))
let button: DebounceButton = createOptionButton(withTitle: doneText)
button.setCropOperation { [weak self] in
self?.crop(button)
}
button.accessibilityIdentifier = "DoneButton"
button.accessibilityLabel = doneText
return button
Expand Down Expand Up @@ -282,13 +288,13 @@ extension CropToolbar {

// private functions
extension CropToolbar {
private func createOptionButton(withTitle title: String?, andAction action: Selector) -> UIButton {
private func createOptionButton<T: UIButton>(withTitle title: String?, andAction action: Selector? = nil) -> T {
let buttonColor = config.foregroundColor
let buttonFont: UIFont = .preferredFont(forTextStyle: .body)
let fontMetrics = UIFontMetrics(forTextStyle: .body)
let maxSize = UIFont.systemFontSize * 1.5

let button = UIButton(type: .system)
let button = T(type: .system)
button.tintColor = config.foregroundColor
button.titleLabel?.font = fontMetrics.scaledFont(for: buttonFont, maximumPointSize: maxSize)
button.titleLabel?.adjustsFontForContentSizeCategory = true
Expand Down Expand Up @@ -320,7 +326,10 @@ extension CropToolbar {
button.setContentCompressionResistancePriority(UILayoutPriority(rawValue: compressionPriority), for: .horizontal)
button.setContentCompressionResistancePriority(UILayoutPriority(rawValue: compressionPriority), for: .vertical)

button.addTarget(self, action: action, for: .touchUpInside)
if let action = action {
button.addTarget(self, action: action, for: .touchUpInside)
}

button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 10, bottom: 4, right: 10)

button.isAccessibilityElement = true
Expand Down
92 changes: 92 additions & 0 deletions Sources/Mantis/CropViewController/DebounceButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// DebounceButton.swift
// Mantis
//
// Created by Yingtao Guo on 11/6/24.
//

import UIKit

/// A custom UIButton subclass that prevents multiple rapid taps and provides debouncing functionality
class DebounceButton: UIButton {
// Default debounce interval in seconds
private var debounceInterval: TimeInterval = 0.5

// Timestamp of the last tap
private var lastTapTimestamp: TimeInterval = 0

// Flag to track if a crop operation is in progress
private var isProcessing: Bool = false

// Closure to store the actual crop operation
private var cropOperation: (() -> Void)?

// MARK: - Initialization

override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupButton()
}

/// Sets up the initial button configuration
private func setupButton() {
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}

// MARK: - Public Methods

/// Sets the minimum time interval between valid taps
/// - Parameter interval: The time interval in seconds
func setDebounceInterval(_ interval: TimeInterval) {
debounceInterval = interval
}

/// Sets the crop operation to be performed when the button is tapped
/// - Parameter operation: A closure containing the crop logic
func setCropOperation(_ operation: @escaping () -> Void) {
cropOperation = operation
}

// MARK: - Private Methods

/// Handles the button tap event with debouncing logic
@objc private func handleTap() {
let currentTime = Date().timeIntervalSince1970

// If a crop operation is already in progress, ignore the tap
guard !isProcessing else {
return
}

// Check if enough time has passed since the last tap
if currentTime - lastTapTimestamp >= debounceInterval {
lastTapTimestamp = currentTime
isProcessing = true

// Perform the crop operation
performCropOperation()
}
}

/// Executes the stored crop operation with proper state management
private func performCropOperation() {
guard let cropOperation = cropOperation else {
isProcessing = false
return
}

isEnabled = false
cropOperation()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
// Re-enable the button and reset processing state
self?.isEnabled = true
self?.isProcessing = false
}
}
}