From 21771ea3a1e2d728d6552665fc6c45633247e8e4 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 16 Feb 2024 09:58:24 -0800 Subject: [PATCH] 1.1.2 --- .../Turbocharger/Sources/View/ForEach.swift | 70 ++++++++++++++++++ .../Sources/View/MarqueeHStack.swift | 71 ++++++++++++++++++- .../Sources/View/ProposedSizeObserver.swift | 26 ------- 3 files changed, 138 insertions(+), 29 deletions(-) create mode 100644 Sources/Turbocharger/Sources/View/ForEach.swift delete mode 100644 Sources/Turbocharger/Sources/View/ProposedSizeObserver.swift diff --git a/Sources/Turbocharger/Sources/View/ForEach.swift b/Sources/Turbocharger/Sources/View/ForEach.swift new file mode 100644 index 0000000..e467c77 --- /dev/null +++ b/Sources/Turbocharger/Sources/View/ForEach.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) Nathan Tannar +// + +import SwiftUI + +extension ForEach { + @_disfavoredOverload + @inlinable + public init<_Data: RandomAccessCollection>( + _ data: _Data, + @ViewBuilder content: @escaping (_Data.Index, _Data.Element) -> Content + ) where Data == Array<(_Data.Index, _Data.Element)>, ID == _Data.Index, Content: View { + let elements = Array(zip(data.indices, data)) + self.init(elements, id: \.0) { index, element in + content(index, element) + } + } + + @inlinable + public init< + _Data: RandomAccessCollection + >( + _ data: _Data, + id: KeyPath<_Data.Element, ID>, + @ViewBuilder content: @escaping (_Data.Index, _Data.Element) -> Content + ) where Data == Array<(_Data.Index, _Data.Element)>, Content: View { + let elements = Array(zip(data.indices, data)) + let elementPath: KeyPath<(_Data.Index, _Data.Element), _Data.Element> = \.1 + self.init(elements, id: elementPath.appending(path: id)) { index, element in + content(index, element) + } + } + + @inlinable + public init< + _Data: RandomAccessCollection + >( + _ data: _Data, + @ViewBuilder content: @escaping (_Data.Index, _Data.Element) -> Content + ) where Data == Array<(_Data.Index, _Data.Element)>, _Data.Element: Identifiable, ID == _Data.Element.ID, Content: View { + let elements = Array(zip(data.indices, data)) + self.init(elements, id: \.1.id) { index, element in + content(index, element) + } + } +} + +// MARK: - ForEach Previews + +struct ForEach_Previews: PreviewProvider { + struct Model: Identifiable { + var id = UUID().uuidString + } + static var previews: some View { + VStack { + ForEach([10, 20, 30]) { index, number in + Text("\(index): \(number)") + } + + ForEach([10, 20, 30], id: \.self) { index, number in + Text("\(index): \(number)") + } + + ForEach([Model()]) { index, model in + Text("\(index): \(model.id)") + } + } + } +} diff --git a/Sources/Turbocharger/Sources/View/MarqueeHStack.swift b/Sources/Turbocharger/Sources/View/MarqueeHStack.swift index 952f9ed..da876da 100644 --- a/Sources/Turbocharger/Sources/View/MarqueeHStack.swift +++ b/Sources/Turbocharger/Sources/View/MarqueeHStack.swift @@ -11,6 +11,7 @@ public struct MarqueeHStack: View { public var spacing: CGFloat public var speed: Double + public var minimumInterval: Double? public var isScrollEnabled: Bool public var content: Content @@ -20,6 +21,7 @@ public struct MarqueeHStack: View { public init( spacing: CGFloat? = nil, speed: Double = 1, + minimumInterval: Double? = nil, delay: TimeInterval = 2, isScrollEnabled: Bool = true, @ViewBuilder content: () -> Content @@ -27,6 +29,7 @@ public struct MarqueeHStack: View { self.selection = nil self.spacing = spacing ?? 4 self.speed = speed + self.minimumInterval = minimumInterval self.isScrollEnabled = isScrollEnabled self.content = content() self._startAt = State(wrappedValue: Date.now.addingTimeInterval(delay)) @@ -37,6 +40,7 @@ public struct MarqueeHStack: View { selection: Binding, spacing: CGFloat? = nil, speed: Double = 1, + minimumInterval: Double? = nil, delay: TimeInterval = 2, isScrollEnabled: Bool = true, @ViewBuilder content: () -> Content @@ -44,6 +48,7 @@ public struct MarqueeHStack: View { self.selection = selection self.spacing = spacing ?? 4 self.speed = speed + self.minimumInterval = minimumInterval self.isScrollEnabled = isScrollEnabled self.content = content() self._startAt = State(wrappedValue: Date.now.addingTimeInterval(delay)) @@ -60,7 +65,7 @@ public struct MarqueeHStack: View { content } content: { source in TimelineView( - .animation(minimumInterval: nil, paused: !isScrollEnabled) + .animation(minimumInterval: minimumInterval, paused: !isScrollEnabled) ) { ctx in MarqueeHStackBody( selection: selection, @@ -180,10 +185,10 @@ private struct MarqueeHStackBody: View { let requiredWidth = resolvedSymbols.map(\.symbol.size.width).reduce(0, +) + spacing * CGFloat(views.count - 1) let timestamp = keyframe / 20 * speed - let dx = (requiredWidth * timestamp).truncatingRemainder(dividingBy: requiredWidth).rounded() + let dx = (requiredWidth * timestamp).truncatingRemainder(dividingBy: requiredWidth) var origin = CGPoint( x: 0, - y: (size.height / 2).rounded() + y: (size.height / 2) ) if requiredWidth > size.width { @@ -217,3 +222,63 @@ private struct MarqueeHStackBody: View { box?.nodes = proxies } } + +// MARK: - Previews + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct MarqueeHStack_Previews: PreviewProvider { + static var previews: some View { + VStack { + MarqueeHStack(speed: 2) { + ForEach(0..<10, id: \.self) { index in + Label { + Text("Index: \(index)") + } icon: { + Image(systemName: "info") + } + .foregroundColor(.white) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background { + Capsule() + .fill(.black) + } + } + } + + MarqueeHStack(speed: -1) { + ForEach(10..<20, id: \.self) { index in + Label { + Text("Index: \(index)") + } icon: { + Image(systemName: "info") + } + .foregroundColor(.white) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background { + Capsule() + .fill(.black) + } + } + } + + MarqueeHStack { + ForEach(20..<30, id: \.self) { index in + Label { + Text("Index: \(index)") + } icon: { + Image(systemName: "info") + } + .foregroundColor(.white) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background { + Capsule() + .fill(.black) + } + } + } + } + } +} diff --git a/Sources/Turbocharger/Sources/View/ProposedSizeObserver.swift b/Sources/Turbocharger/Sources/View/ProposedSizeObserver.swift deleted file mode 100644 index fb01741..0000000 --- a/Sources/Turbocharger/Sources/View/ProposedSizeObserver.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) Nathan Tannar -// - -import SwiftUI - -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) -public struct ProposedSizeObserver: ViewModifier { - @Binding var size: ProposedSize - - public init(size: Binding) { - self._size = size - } - - public func body(content: Content) -> some View { - content - .background( - GeometryReader { proxy in - Color.clear - .hidden() - .onAppearAndChange(of: proxy.size) { size = ProposedSize(size: $0) } - .onDisappear { size = .unspecified } - } - ) - } -}