From 2773dadb055e989e2c9937906442ab1299f3d166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bednorz?= Date: Sat, 14 Mar 2026 04:12:05 +0100 Subject: [PATCH] feat: vertical slider --- .swiftlint.yml | 5 ++ Package.resolved | 24 +++++++ Sources/SlidableImage/SlidableImage.swift | 64 ++++++++++++++----- .../SlidableImageTests.swift | 39 ++++++++--- 4 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 Package.resolved diff --git a/.swiftlint.yml b/.swiftlint.yml index 2ef1ccf..bbbf71e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -61,3 +61,8 @@ function_body_length: cyclomatic_complexity: warning: 10 error: 20 + +identifier_name: + excluded: + - x + - y diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..bfc89e0 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "07f6d732b85637fac54dd49fb5698bf06e0b2d8e830420f1d6c1e2ac277684dc", + "pins" : [ + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "e977f65879f82b375a044c8837597f690c067da6", + "version" : "1.4.6" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + } + ], + "version" : 3 +} diff --git a/Sources/SlidableImage/SlidableImage.swift b/Sources/SlidableImage/SlidableImage.swift index 38fb40f..60c93a5 100644 --- a/Sources/SlidableImage/SlidableImage.swift +++ b/Sources/SlidableImage/SlidableImage.swift @@ -7,13 +7,16 @@ import SwiftUI public struct SlidableImage: View { @State private var location: CGPoint? + private let axis: Axis private let arrows: () -> ArrowsIcon private let leftView: () -> LeftView private let rightView: () -> RightView - public init(@ViewBuilder arrows: @escaping () -> ArrowsIcon, + public init(axis: Axis = .horizontal, + @ViewBuilder arrows: @escaping () -> ArrowsIcon, @ViewBuilder leftView: @escaping () -> LeftView, @ViewBuilder rightView: @escaping () -> RightView) { + self.axis = axis self.arrows = arrows self.leftView = leftView self.rightView = rightView @@ -21,42 +24,71 @@ public struct SlidableImage: public var body: some View { GeometryReader { geometry in - ZStack(alignment: .leading) { + ZStack(alignment: axis == .horizontal ? .leading : .top) { rightView() leftView() .mask { - HStack { - Color.black - Spacer(minLength: maskSize(width: geometry.size.width)) + if axis == .horizontal { + HStack { + Color.black + Spacer(minLength: maskSize(total: geometry.size.width)) + } + } else { + VStack { + Color.black + Spacer(minLength: maskSize(total: geometry.size.height)) + } } } arrows() .frame(width: Constants.arrowSize, height: Constants.arrowSize) - .padding(.leading, location?.x ?? geometry.size.width / 2 - Constants.arrowSize / 2) + .padding(axis == .horizontal ? .leading : .top, + locationValue ?? defaultOffset(geometry: geometry)) .gesture( DragGesture() .onChanged { value in - guard value.location.x <= geometry.size.width - Constants.arrowSize else { return } - - location = CGPoint(x: value.location.x, y: geometry.size.height / 2) + if axis == .horizontal { + let x = min(max(value.location.x, 0), geometry.size.width - Constants.arrowSize) + location = CGPoint(x: x, y: geometry.size.height / 2) + } else { + let y = min(max(value.location.y, 0), geometry.size.height - Constants.arrowSize) + location = CGPoint(x: geometry.size.width / 2, y: y) + } } ) } } } - package func maskSize(width: CGFloat, locationX: CGFloat? = nil) -> CGFloat { - guard let locationX = locationX ?? location?.x else { - return width / 2 + private var locationValue: CGFloat? { + axis == .horizontal ? location?.x : location?.y + } + + private func defaultOffset(geometry: GeometryProxy) -> CGFloat { + axis == .horizontal + ? geometry.size.width / 2 - Constants.arrowSize / 2 + : geometry.size.height / 2 - Constants.arrowSize / 2 + } + + package func maskSize(total: CGFloat, locationValue: CGFloat? = nil) -> CGFloat { + guard let value = locationValue ?? self.locationValue else { + return total / 2 } - return width - locationX - Constants.arrowSize / 2 + return total - value - Constants.arrowSize / 2 } } #Preview { - SlidableImage(arrows: { Arrows() }, - leftView: { Color.red }, - rightView: { Color.green }) + VStack { + SlidableImage(arrows: { Arrows() }, + leftView: { Color.red }, + rightView: { Color.green }) + + SlidableImage(axis: .vertical, + arrows: { Arrows() }, + leftView: { Color.blue }, + rightView: { Color.orange }) + } } diff --git a/Tests/SlidableImageTests/SlidableImageTests.swift b/Tests/SlidableImageTests/SlidableImageTests.swift index 36dd168..2a29b7b 100644 --- a/Tests/SlidableImageTests/SlidableImageTests.swift +++ b/Tests/SlidableImageTests/SlidableImageTests.swift @@ -5,27 +5,46 @@ import SwiftUI @MainActor struct SlidableImageTests { - private func makeSUT() -> SlidableImage { - SlidableImage(arrows: { Arrows() }, leftView: { Color.red }, rightView: { Color.green }) + private func makeSUT(axis: Axis = .horizontal) -> SlidableImage { + SlidableImage(axis: axis, arrows: { Arrows() }, leftView: { Color.red }, rightView: { Color.green }) } - @Test func maskSizeWithoutLocation() { - #expect(makeSUT().maskSize(width: 200) == 100) + // MARK: - Horizontal + + @Test func maskSizeHorizontalWithoutLocation() { + #expect(makeSUT().maskSize(total: 200) == 100) } - @Test func maskSizeWithLocationAtStart() { - let result = makeSUT().maskSize(width: 200, locationX: 0) + @Test func maskSizeHorizontalWithLocationAtStart() { + let result = makeSUT().maskSize(total: 200, locationValue: 0) #expect(result == 200 - Constants.arrowSize / 2) } - @Test func maskSizeWithLocationAtCenter() { - let result = makeSUT().maskSize(width: 200, locationX: 100) + @Test func maskSizeHorizontalWithLocationAtCenter() { + let result = makeSUT().maskSize(total: 200, locationValue: 100) #expect(result == 100 - Constants.arrowSize / 2) } - @Test func maskSizeWithLocationAtBoundary() { + @Test func maskSizeHorizontalWithLocationAtBoundary() { let locationX = 200 - Constants.arrowSize - let result = makeSUT().maskSize(width: 200, locationX: locationX) + let result = makeSUT().maskSize(total: 200, locationValue: locationX) + #expect(result == Constants.arrowSize / 2) + } + + // MARK: - Vertical + + @Test func maskSizeVerticalWithoutLocation() { + #expect(makeSUT(axis: .vertical).maskSize(total: 200) == 100) + } + + @Test func maskSizeVerticalWithLocationAtStart() { + let result = makeSUT(axis: .vertical).maskSize(total: 200, locationValue: 0) + #expect(result == 200 - Constants.arrowSize / 2) + } + + @Test func maskSizeVerticalWithLocationAtBoundary() { + let locationY = 200 - Constants.arrowSize + let result = makeSUT(axis: .vertical).maskSize(total: 200, locationValue: locationY) #expect(result == Constants.arrowSize / 2) } }