Skip to content
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
2 changes: 1 addition & 1 deletion ACarouselDemo/ACarouselDemo iOS/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ContentView: View {

@State var spacing: CGFloat = 10
@State var headspace: CGFloat = 10
@State var sidesScaling: CGFloat = 0.8
@State var sidesScaling: ScaleMode = .uniform(factor: 0.8)
@State var isWrap: Bool = false
@State var autoScroll: Bool = false
@State var time: TimeInterval = 1
Expand Down
2 changes: 1 addition & 1 deletion ACarouselDemo/ACarouselDemo macOS/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct ContentView: View {

@State var spacing: CGFloat = 10
@State var headspace: CGFloat = 10
@State var sidesScaling: CGFloat = 0.8
@State var sidesScaling: ScaleMode = .uniform(factor: 0.8)
@State var isWrap: Bool = false
@State var autoScroll: Bool = false
@State var time: TimeInterval = 1
Expand Down
36 changes: 28 additions & 8 deletions Sources/ACarousel/ACarousel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,28 @@ public struct ACarousel<Data, ID, Content> : View where Data : RandomAccessColle

private func generateContent(proxy: GeometryProxy) -> some View {
HStack(spacing: viewModel.spacing) {
ForEach(viewModel.data, id: viewModel.dataId) {
content($0)
ForEach(viewModel.data, id: viewModel.dataId) { data in
switch viewModel.itemScaling(data) {
case .uniform(let factor):
content(data)
.frame(width: viewModel.itemWidth)
.scaleEffect(x: 1, y: viewModel.itemScaling($0), anchor: .center)
.scaleEffect(x: factor, y: factor, anchor: .center)
.onTapGesture(count: 1) {
if viewModel.tapToSelect {
viewModel.selectItem(data)
}
}
case .nonUniform(let horizontal, let vertical):
content(data)
.frame(width: viewModel.itemWidth)
.scaleEffect(x: horizontal, y: vertical, anchor: .center)
.onTapGesture(count: 1) {
if viewModel.tapToSelect {
viewModel.selectItem(data)
}
}

}
}
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .leading)
Expand Down Expand Up @@ -74,9 +92,9 @@ extension ACarousel {
/// - autoScroll: A enum that define view to scroll automatically. See
/// ``ACarouselAutoScroll``. default is `inactive`.
/// - content: The view builder that creates views dynamically.
public init(_ data: Data, id: KeyPath<Data.Element, ID>, index: Binding<Int> = .constant(0), spacing: CGFloat = 10, headspace: CGFloat = 10, sidesScaling: CGFloat = 0.8, isWrap: Bool = false, autoScroll: ACarouselAutoScroll = .inactive, canMove: Bool = true, @ViewBuilder content: @escaping (Data.Element) -> Content) {
public init(_ data: Data, id: KeyPath<Data.Element, ID>, index: Binding<Int> = .constant(0), spacing: CGFloat = 10, headspace: CGFloat = 10, sidesScaling: ScaleMode = .uniform(factor: 0.8), isWrap: Bool = false, autoScroll: ACarouselAutoScroll = .inactive, canMove: Bool = true, tapToSelect: Bool = false, @ViewBuilder content: @escaping (Data.Element) -> Content) {

self.viewModel = ACarouselViewModel(data, id: id, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove)
self.viewModel = ACarouselViewModel(data, id: id, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove, tapToSelect: tapToSelect)
self.content = content
}

Expand All @@ -99,10 +117,12 @@ extension ACarousel where ID == Data.Element.ID, Data.Element : Identifiable {
/// - isWrap: Define views to scroll through in a loop, default is false.
/// - autoScroll: A enum that define view to scroll automatically. See
/// ``ACarouselAutoScroll``. default is `inactive`.
/// - tapToSelect: Allows tapping on other visible items in the carousel to
/// select them
/// - content: The view builder that creates views dynamically.
public init(_ data: Data, index: Binding<Int> = .constant(0), spacing: CGFloat = 10, headspace: CGFloat = 10, sidesScaling: CGFloat = 0.8, isWrap: Bool = false, autoScroll: ACarouselAutoScroll = .inactive, canMove: Bool = true, @ViewBuilder content: @escaping (Data.Element) -> Content) {
public init(_ data: Data, index: Binding<Int> = .constant(0), spacing: CGFloat = 10, headspace: CGFloat = 10, sidesScaling: ScaleMode = .uniform(factor:0.8), isWrap: Bool = false, autoScroll: ACarouselAutoScroll = .inactive, canMove: Bool = true, tapToSelect: Bool = false, @ViewBuilder content: @escaping (Data.Element) -> Content) {

self.viewModel = ACarouselViewModel(data, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove)
self.viewModel = ACarouselViewModel(data, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove, tapToSelect: tapToSelect)
self.content = content
}

Expand All @@ -115,7 +135,7 @@ struct ACarousel_LibraryContent: LibraryContentProvider {
@LibraryContentBuilder
var views: [LibraryItem] {
LibraryItem(ACarousel(Datas) { _ in }, title: "ACarousel", category: .control)
LibraryItem(ACarousel(Datas, index: .constant(0), spacing: 10, headspace: 10, sidesScaling: 0.8, isWrap: false, autoScroll: .inactive) { _ in }, title: "ACarousel full parameters", category: .control)
LibraryItem(ACarousel(Datas, index: .constant(0), spacing: 10, headspace: 10, sidesScaling: .uniform(factor: 0.8), isWrap: false, autoScroll: .inactive, tapToSelect: false) { _ in }, title: "ACarousel full parameters", category: .control)
}

struct _Item: Identifiable {
Expand Down
52 changes: 42 additions & 10 deletions Sources/ACarousel/ACarouselViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import SwiftUI
import Combine

@available(iOS 13.0, OSX 10.15, *)
public enum ScaleMode {
case nonUniform(horizontal: CGFloat, vertical: CGFloat)
case uniform(factor: CGFloat)
}

class ACarouselViewModel<Data, ID>: ObservableObject where Data : RandomAccessCollection, ID : Hashable {

/// external index
Expand All @@ -33,11 +38,12 @@ class ACarouselViewModel<Data, ID>: ObservableObject where Data : RandomAccessCo
private let _spacing: CGFloat
private let _headspace: CGFloat
private let _isWrap: Bool
private let _sidesScaling: CGFloat
private let _sidesScaling: ScaleMode
private let _autoScroll: ACarouselAutoScroll
private let _canMove: Bool
@Published var tapToSelect: Bool

init(_ data: Data, id: KeyPath<Data.Element, ID>, index: Binding<Int>, spacing: CGFloat, headspace: CGFloat, sidesScaling: CGFloat, isWrap: Bool, autoScroll: ACarouselAutoScroll, canMove: Bool) {
init(_ data: Data, id: KeyPath<Data.Element, ID>, index: Binding<Int>, spacing: CGFloat, headspace: CGFloat, sidesScaling: ScaleMode, isWrap: Bool, autoScroll: ACarouselAutoScroll, canMove: Bool, tapToSelect: Bool) {

guard index.wrappedValue < data.count else {
fatalError("The index should be less than the count of data ")
Expand All @@ -51,6 +57,7 @@ class ACarouselViewModel<Data, ID>: ObservableObject where Data : RandomAccessCo
self._sidesScaling = sidesScaling
self._autoScroll = autoScroll
self._canMove = canMove
self.tapToSelect = tapToSelect

if data.count > 1 && isWrap {
activeIndex = index.wrappedValue + 1
Expand Down Expand Up @@ -105,8 +112,8 @@ class ACarouselViewModel<Data, ID>: ObservableObject where Data : RandomAccessCo

extension ACarouselViewModel where ID == Data.Element.ID, Data.Element : Identifiable {

convenience init(_ data: Data, index: Binding<Int>, spacing: CGFloat, headspace: CGFloat, sidesScaling: CGFloat, isWrap: Bool, autoScroll: ACarouselAutoScroll, canMove: Bool) {
self.init(data, id: \.id, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove)
convenience init(_ data: Data, index: Binding<Int>, spacing: CGFloat, headspace: CGFloat, sidesScaling: ScaleMode, isWrap: Bool, autoScroll: ACarouselAutoScroll, canMove: Bool, tapToSelect: Bool) {
self.init(data, id: \.id, index: index, spacing: spacing, headspace: headspace, sidesScaling: sidesScaling, isWrap: isWrap, autoScroll: autoScroll, canMove: canMove, tapToSelect: tapToSelect)
}
}

Expand Down Expand Up @@ -155,12 +162,32 @@ extension ACarouselViewModel {
/// Defines the scaling based on whether the item is currently active or not.
/// - Parameter item: The incoming item
/// - Returns: scaling
func itemScaling(_ item: Data.Element) -> CGFloat {
guard activeIndex < data.count else {
return 0
func itemScaling(_ item: Data.Element) -> ScaleMode {
guard activeIndex < data.count, activeIndex >= 0 else {
return .uniform(factor: 1.0)
}
let activeItem = data[activeIndex as! Data.Index]
return activeItem[keyPath: _dataId] == item[keyPath: _dataId] ? 1 : sidesScaling
switch _sidesScaling {
case .uniform:
return activeItem[keyPath: _dataId] == item[keyPath: _dataId] ? .uniform(factor: 1) : sidesScaling
case .nonUniform:
return activeItem[keyPath: _dataId] == item[keyPath: _dataId] ? .nonUniform(horizontal: 1, vertical: 1) : sidesScaling
}
}

func selectItem(_ item: Data.Element) {
guard activeIndex < data.count && activeIndex >= 0 else {
return
}
if let index = data.firstIndex(where: { $0[keyPath: _dataId] == item[keyPath: _dataId] }) {
let newActiveIndex = activeIndex + data.distance(from: activeIndex as! Data.Index, to: index)
if newActiveIndex != activeIndex && newActiveIndex >= 0 && newActiveIndex < data.count {
DispatchQueue.main.async {
self.activeIndex = newActiveIndex
self.isAnimatedOffset = false
}
}
}
}
}

Expand All @@ -185,8 +212,13 @@ extension ACarouselViewModel {
itemWidth + spacing
}

private var sidesScaling: CGFloat {
return max(min(_sidesScaling, 1), 0)
private var sidesScaling: ScaleMode {
switch _sidesScaling {
case .nonUniform(let horizontal, let vertical):
return .nonUniform(horizontal: max(min(horizontal, 1), 0), vertical: max(min(vertical, 1), 0))
case .uniform(let factor):
return .uniform(factor: max(min(factor, 1), 0))
}
}

/// Is animated when view is in offset
Expand Down