How to use ScrollViewReader to scroll to proper BarMark - swift

Im trying to resolve the animated transition to the correct one BarMark using Charts library.
The first idea was to add to each element id and next using .scrollTo(id) from ScrollViewReader.
Unfortunately, it doesn't work.
import SwiftUI
import UIKit
import Charts
struct PartyItem: Identifiable, Codable {
var type: String
var count: Double
var color: Color
var id = UUID()
static var shared: [Self] = [
.init(type: "logo_1", count: 1, color: .blue),
.init(type: "logo_2", count: 1, color: .orange),
.init(type: "logo_3", count: 1, color: .purple),
.init(type: "logo_4", count: 1, color: .yellow),
.init(type: "logo_5", count: 1, color: .red),
.init(type: "logo_6", count: 1, color: .green),
.init(type: "logo_7", count: 1, color: .cyan),
.init(type: "logo_8", count: 1, color: .indigo)
]
}
extension PartyItem: Comparable {
static func < (lhs: PartyItem, rhs: PartyItem) -> Bool {
lhs.count < rhs.count
}
static func == (lhs: PartyItem, rhs: PartyItem) -> Bool {
lhs.count == rhs.count
}
}
enum OperationType {
case subtract, add
}
struct MainVoteView: View {
#AppStorage(.partyStats) private var partyStats: [PartyItem] = []
typealias ChartLocation = (location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy)?
#State private var chartLocation: ChartLocation = nil
#State private var showingPopup = false
#State private var scaleValue: CGFloat = 1
var body: some View {
ScrollView(.horizontal) {
VStack {
Chart {
ForEach(partyStats.sorted(by: >)) { shape in
BarMark(
x: .value("Party Type", shape.type),
y: .value("Vote Count", shape.count)
)
.annotation(position: .bottom, alignment: .center, spacing: .spacingMedium) {
Image(shape.type)
.resizable()
.scaledToFit()
.frame(width: .sizeMediumMediumSmall, height: .sizeMediumMediumSmall)
}
.annotation(position: .top, alignment: .center, spacing: .spacingSmall, content: { value in
Text("\(Int(shape.count))")
.bold()
.font(.subheadline)
.foregroundColor(.white)
})
.foregroundStyle(shape.color)
}
}
.scaleEffect(scaleValue)
.animation(.easeInOut(duration: 0.5), value: scaleValue)
.frame(width: .sizeExtraExtraLarge)
.chartYAxis(.hidden)
.chartXAxis(.hidden)
.chartLegend(.hidden)
.padding()
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle().fill(.clear).contentShape(Rectangle()).onTapGesture { location in
scaleValue = 1.1
showingPopup = true
chartLocation = (location, proxy, geometry)
}
}
}
}
}
.scrollIndicators(.hidden)
.padding()
.background(Color.darkBlue)
.navigationBarBackButtonHidden()
.popup(isPresented: $showingPopup) {
PopUpView {
scaleValue = 1
updateSelected(at: chartLocation, operation: .add)
} nayAction: {
scaleValue = 1
updateSelected(at: chartLocation, operation: .subtract)
} abstainAction: {
scaleValue = 1
}
}
}
}
extension MainVoteView {
private func updateSelected(at chartLocation: ChartLocation, operation: OperationType) {
guard let location = chartLocation?.location,
let proxy = chartLocation?.proxy,
let geometry = chartLocation?.geometry else {
return
}
let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
guard let selected: String = proxy.value(atX: xPosition) else {
return
}
guard let index = partyStats.enumerated().first(where: { $0.element.type == selected })?.offset else {
return
}
switch operation {
case .subtract:
if partyStats[index].count > 0 {
partyStats[index].count -= 1
}
case .add:
partyStats[index].count += 1
}
}
}
And when it updates the partyStats value, I'd like to center the screen where it's placed on the screen. For example:
When i want update value of blue mark bar I would like to scroll to where it will go.
The data is sorted from largest to smallest,

Related

How to dynamically change GridItems in LazyVGrid with MagnificationGesture [Zoom In, Out] in SwiftUI?

The idea is to recreate the same photo layout behaviour like in Apple Photo Library when I can zoom in and out with 1, 3 or 5 photos in a row. I'm stack in a half way. For that I use a MagnificationGesture() and based on gesture value I update number of GridItems() in LazyVGrid().
Please let me know how to achieve it. Thanks a lot ๐Ÿ™
Here's code:
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var colums = Array(repeating: GridItem(), count: 1)
// #GestureState var magnifyBy: CGFloat = 1.0
#State var magnifyBy: CGFloat = 1.0
#State var lastMagnifyBy: CGFloat = 1.0
let minMagnifyBy = 1.0
let maxMagnifyBy = 5.0
var magnification: some Gesture {
MagnificationGesture()
// .updating($magnifyBy) { (currentState, pastState, trans) in
// pastState = currentState.magnitude
// }
.onChanged { state in
adjustMagnification(from: state)
print("Current State \(state)")
}
.onEnded { state in
adjustMagnification(from: state)
// withAnimation(.spring()) {
// validateMagnificationLimits()
// }
lastMagnifyBy = 1.0
}
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: colums) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.title2.bold()).foregroundColor(.white))
.frame(height: 100)
}
}
.scaleEffect(magnifyBy)
.gesture(magnification)
.navigationTitle("๐Ÿงจ Grid")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring(response: 0.8)) {
colums = Array(repeating: .init(), count: colums.count == 5 ? 1 : colums.count % 5 + 2)
}
} label: {
Image(systemName: "square.grid.3x3")
.font(.title2)
.foregroundColor(.primary)
}
}
}
}
}
}
private func adjustMagnification(from state: MagnificationGesture.Value) {
let stepCount = Int(min(max(1, state), 5))
// let delta = state / lastMagnifyBy
// magnifyBy *= delta
withAnimation(.linear) {
colums = Array(repeating: GridItem(), count: stepCount)
}
lastMagnifyBy = state
}
private func getMinMagnificationAllowed() -> CGFloat {
max(magnifyBy, minMagnifyBy)
}
private func getMaxMagnificationAllowed() -> CGFloat {
min(magnifyBy, maxMagnifyBy)
}
private func validateMagnificationLimits() {
magnifyBy = getMinMagnificationAllowed()
magnifyBy = getMaxMagnificationAllowed()
}
}
Here you go. This uses a TrackableScrollView (git link in the code).
I implemented an array of possible zoomStages (cols per row), to make switching between them easier.
Next to dos would be scrolling back to the magnification center, so the same item stays in focus. And maybe an opacity transition in stead of rearranging the Grid. Have fun ;)
import SwiftUI
// https://github.com/maxnatchanon/trackable-scroll-view.git
import SwiftUITrackableScrollView
struct ContentView: View {
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
let zoomStages = [1, 3, 5, 9, 15]
#State private var zoomStageIndex = 0
var colums: [GridItem] { Array(repeating: GridItem(spacing: 0), count: zoomStages[zoomStageIndex]) }
#State var magnifyBy: CGFloat = 1.0
#State private var scrollViewOffset = CGFloat.zero // SwiftUITrackableScrollView: Content offset available to use
var body: some View {
NavigationView {
TrackableScrollView(.vertical, showIndicators: false, contentOffset: $scrollViewOffset) {
LazyVGrid(columns: colums, spacing: 0) {
ForEach(0..<500) { number in
colors[number % colors.count]
.overlay(
Text("\(number)").font(.title2.bold()).foregroundColor(.white)
.minimumScaleFactor(0.1)
)
.aspectRatio(1, contentMode: .fit) // always squares
.id(number)
}
}
.scaleEffect(magnifyBy, anchor: .top)
// offset to correct magnify "center" point
.offset(x: 0, y: (scrollViewOffset + UIScreen.main.bounds.midY) * (1 - magnifyBy) )
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring(response: 0.8)) {
if zoomStageIndex < zoomStages.count-1 {
zoomStageIndex += 1
} else {
zoomStageIndex = 0
}
}
} label: {
Image(systemName: "square.grid.3x3")
.font(.title2)
.foregroundColor(.primary)
}
}
}
.gesture(magnification)
}
.ignoresSafeArea()
}
}
var magnification: some Gesture {
MagnificationGesture()
.onChanged { state in
magnifyBy = state
}
.onEnded { state in
// find predefined zoom(index) that is closest to actual pinch zoom value
let newZoom = Double(zoomStages[zoomStageIndex]) * 1 / state
let newZoomIndex = findClosestZoomIndex(value: newZoom)
// print("***", zoomStages[zoomStageIndex], state, newZoom, newZoomIndex)
withAnimation(.spring(response: 0.8)) {
magnifyBy = 1 // reset scaleEffect
zoomStageIndex = newZoomIndex // set new zoom level
}
}
}
func findClosestZoomIndex(value: Double) -> Int {
let distanceArray = zoomStages.map { abs(Double($0) - value) } // absolute difference between zoom stages and actual pinch zoom
// print("dist:", distanceArray)
return distanceArray.indices.min(by: {distanceArray[$0] < distanceArray[$1]}) ?? 0 // return index of element that is "closest"
}
}

Want to "deal" cards in Swift: how to stop cards from changing sizes as more cards are dealt

I need help with this animation. Basically, I want to deal 12 cards when I tap on the "deck." However, I want to deal it so that the sizes of each card is already set; I don't want the card size to change as more cards come onto screen. Put differently, to put 12 cards on the screen, at the end, we have 3 rows of 4 cards. But the Swift system tries to put 3 cards per row, and then moves to 4 cards per row. This changing of card size looks very weird, so I was hoping to put in some code that makes it so the card size doesn't change.
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: GameViewModel
#Namespace private var dealingNamespace
var body: some View {
VStack {
newGameButton
gameBody
HStack {
deckBody
discardedDeckBody
}
shuffleButton
}
}
var newGameButton: some View {
Button(action: { viewModel.newGame() }) { Text("New Game")
.font(.caption) }
}
var gameBody: some View {
AspectVGrid(items: viewModel.playingCards, aspectRatio: 2/3) { card in
CardView(viewModel: viewModel, card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.padding(4)
// .transition(AnyTransition.asymmetric(insertion: .identity, removal: .scale))
.onTapGesture {
withAnimation {
viewModel.choose(card)
}
}
.foregroundColor(viewModel.borderColor(for: card))
}
.padding(.horizontal)
}
private func dealAnimation(for cardNumber: Int, _ totalCardsToDeal: Int) -> Animation {
let delay = Double(cardNumber) * (CardConstants.totalDealDuration / Double(totalCardsToDeal))
return Animation.easeInOut(duration: CardConstants.dealDuration).delay(delay)
}
var deckBody: some View {
ZStack {
ForEach(viewModel.undealtCards) { card in
CardView(viewModel: viewModel, card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
// .transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
}
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.foregroundColor(CardConstants.color)
.onTapGesture {
for cardNumber in Array(0..<viewModel.numberOfCardsToDeal()) {
withAnimation(dealAnimation(for: cardNumber, viewModel.numberOfCardsToDeal())) {
viewModel.deal()
}
}
}
}
var discardedDeckBody: some View {
ZStack {
ForEach(viewModel.discardedCards()) { card in
CardView(viewModel: viewModel, card: card)
}
}
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.foregroundColor(CardConstants.color)
}
private struct CardConstants {
static let color = Color.red
static let aspectRatio: CGFloat = 2/3
static let dealDuration: Double = 0.5
static let totalDealDuration: Double = 2
static let undealtHeight: CGFloat = 90
static let undealtWidth = undealtHeight * aspectRatio
}
var shuffleButton: some View {
Button("Shuffle") {
withAnimation {
viewModel.shuffle()
}
}
}
}
struct CardView: View {
#ObservedObject var viewModel: GameViewModel
let card: GameModel.Card
var body: some View {
GeometryReader { geometry in
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
shape.fill().foregroundColor(.white)
shape.stroke(lineWidth: DrawingConstants.lineWidth)
viewModel.symbol(for: card)
.padding(.all)
shape.foregroundColor(viewModel.cover(for: card))
}
}
}
}
struct DrawingConstants {
static let cornerRadius: CGFloat = 10
static let lineWidth: CGFloat = 3
static let fontScale: CGFloat = 0.7
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = GameViewModel()
ContentView(viewModel: game)
.previewInterfaceOrientation(.portrait)
}
}
AspectVGrid
import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
var items: [Item]
var aspectRatio: CGFloat
var content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, #ViewBuilder content: #escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
let computedWidth: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
let width: CGFloat = computedWidth > 65 ? computedWidth : 65
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) { item in
content(item).aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = (itemCount + (columnCount - 1)) / columnCount
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
Model
import Foundation
import SwiftUI
struct GameModel {
private(set) var cards: Array<Card>
var undealtCards: Array<Card>
var discardedCards: Array<Card> = []
private(set) var playingCards: Array<Card>
private var selectedCards: Array<Card> = []
private static var colors = [ContentColor.red, ContentColor.green, ContentColor.purple]
private static var shapes = [ContentShape.squiggly, ContentShape.circle, ContentShape.diamond]
private static var fillings = [ContentFilling.empty, ContentFilling.full, ContentFilling.striped]
private static var numbers = [1, 2, 3]
func numberOfUndealtCards() -> Int {
return undealtCards.count
}
mutating func deal() {
playingCards.append(undealtCards[0])
undealtCards.remove(at: 0)
}
func numberOfCardsToDeal() -> Int {
if numberOfUndealtCards() == 81 {
return 12
} else {
return 3
}
}
mutating func resetCards() {
if !playingCards.first(where: { $0 == selectedCards.first })!.isMatched! {
selectedCards.forEach { card in
let unmatchedIndex = playingCards.firstIndex(of: card)!
playingCards[unmatchedIndex].isMatched = nil
}
selectedCards = []
}
}
mutating func shuffle() {
playingCards = playingCards.shuffled()
}
mutating func choose(_ card: Card) {
if let chosenIndex = playingCards.firstIndex(where: { $0.id == card.id }) {
if selectedCards.count < 3 || selectedCards.count == 3 && !selectedCards.contains(playingCards[chosenIndex]) {
if selectedCards.count == 3 { resetCards() }
if !playingCards[chosenIndex].isSelected {
playingCards[chosenIndex].isSelected = true
selectedCards.append(playingCards[chosenIndex])
if selectedCards.count == 4 {
let chosenCard = playingCards[chosenIndex]
for card in selectedCards {
if card != chosenCard {
discardedCards.append(card)
let matchedIndex = playingCards.firstIndex(of: card)!
playingCards.remove(at: matchedIndex)
}
}
selectedCards = [chosenCard]
}
if selectedCards.count == 3 {
if formSet(by: selectedCards) {
selectedCards.forEach { card in
let index = playingCards.firstIndex(of: card)!
playingCards[index].isMatched = true
playingCards[index].isSelected = false
}
} else {
selectedCards.forEach { card in
let index = playingCards.firstIndex(of: card)!
playingCards[index].isMatched = false
playingCards[index].isSelected = false
}
}
}
}
else {
playingCards[chosenIndex].isSelected = false
selectedCards.removeAll(where: { $0 == playingCards[chosenIndex] })
}
}
}
}
private func formSet(by cards: [Card]) -> Bool {
return feature(\Card.color, isValidFor: cards) && feature(\Card.shape, isValidFor: cards) && feature(\Card.filling, isValidFor: cards) && feature(\Card.number, isValidFor: cards)
}
private func feature<F: Hashable>(_ keyPath: KeyPath<Card, F>, isValidFor cards: [Card]) -> Bool {
let count = cards.reduce(into: Set<F>()) { $0.insert($1[keyPath: keyPath]) }.count
return count == 1 || count == 3
}
init() {
cards = []
var counter = 0
for id1 in GameModel.colors.indices {
for id2 in GameModel.shapes.indices {
for id3 in GameModel.fillings.indices {
for id4 in GameModel.numbers.indices {
cards.append(Card(color: GameModel.colors[id1], shape: GameModel.shapes[id2], filling: GameModel.fillings[id3], number: GameModel.numbers[id4], id: counter))
counter += 1
}
}
}
}
playingCards = []
undealtCards = cards
}
enum ContentColor {
case red
case green
case purple
}
enum ContentShape {
case squiggly
case circle
case diamond
}
enum ContentFilling {
case empty
case full
case striped
}
struct Card: Identifiable, Equatable {
var color: ContentColor
var shape: ContentShape
var filling: ContentFilling
var number: Int
var id: Int
var isSelected = false
var isUndealt = false
var isMatched: Bool?
static func == (lhs: Card, rhs: Card) -> Bool {
return lhs.id == rhs.id
}
}
}
Code is untestable and unclear due to missed many components, but the issue is because use use withAnimation, like
.onTapGesture {
withAnimation {
viewModel.choose(card)
}
}
which animates everything animatable.
There are next directions to fix observed behavior
disable animations everywhere they are unneeded, for example animating of frames
.frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
.animation(nil, value: viewModel.undealtCards) // << like this !!
.foregroundColor(CardConstants.color)
remove all explicit generic withAnimation and use animation(_, value) specifically in those places which are going to be animatable, like
VStack {
newGameButton
gameBody
HStack {
deckBody
discardedDeckBody
}
shuffleButton
}
.animation(.default, value: viewModel.playingCards) // << here !!
probably combine 1-2

SwiftUI - Fatal error: index out of range on deleting element from an array

I have an array on appstorage. I'm displaying its elements with foreach method. It has swipe to delete on each element of the array. But when i delete one, app is crashing. Here is my first view;
struct View1: View {
#Binding var storedElements: [myElements]
var body: some View{
GeometryReader {
geometry in
VStack{
ForEach(storedElements.indices, id: \.self){i in
View2(storedElements: $storedElements[i], pic: $storedWElements[i].pic, allElements: $storedElements, index: i)
}
}.frame(width: geometry.size.width, height: geometry.size.height / 2, alignment: .top).padding(.top, 25)
}
}
}
And View 2;
struct View2: View {
#Binding var storedElements: myElements
#Binding var pic: String
#Binding var allElements: [myElements]
var index: Int
#State var offset: CGFloat = 0.0
#State var isSwiped: Bool = false
#AppStorage("pics", store: UserDefaults(suiteName: "group.com.some.id"))
var arrayData: Data = Data()
var body : some View {
ZStack{
Color.red
HStack{
Spacer()
Button(action: {
withAnimation(.easeIn){
delete()}}) {
Image(systemName: "trash").font(.title).foregroundColor(.white).frame(width: 90, height: 50)
}
}
View3(storedElements: $storedElements).background(Color.red).contentShape(Rectangle()).offset(x: self.offset).gesture(DragGesture().onChanged(onChanged(value:)).onEnded(onEnd(value:)))
}.frame(width: 300, height: 175).cornerRadius(30)
}
}
func onChanged(value: DragGesture.Value) {
if value.translation.width < 0 {
if self.isSwiped {
self.offset = value.translation.width - 90
}else {
self.offset = value.translation.width
}
}
}
func onEnd(value: DragGesture.Value) {
withAnimation(.easeOut) {
if value.translation.width < 0 {
if -value.translation.width > UIScreen.main.bounds.width / 2 {
self.offset = -100
delete()
}else if -self.offset > 50 {
self.isSwiped = true
self.offset = -90
}else {
self.isSwiped = false
self.offset = 0
}
}else {
self.isSwiped = false
self.offset = 0
}
}
}
func delete() {
//self.allElements.remove(at: self.index)
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
}
}
OnChange and onEnd functions are for swiping. I think its the foreach method that is causing crash. Also tried the commented line on delete function but no help.
And I know its a long code for a question. I'm trying for days and tried every answer here but none of them solved my problem here.
In your myElements class/struct, ensure it has a unique property. If not add one and upon init set a unique ID
public class myElements {
var uuid: String
init() {
self.uuid = NSUUID().uuidString
}
}
Then when deleting the element, instead of
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
Use
self.allElements.removeAll(where: { a in a.uuid == storedElements.uuid })
This is array bound safe as it does not use an index

State variable not update in SwiftUI

I have struct DBScrollViewCellWrapper. that display contain
struct DBScrollViewCellWrapper: View, Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
#State var showSelectionLine: Bool = false
var body: some View {
VStack(spacing: 0){
view
if self.showSelectionLine{
Rectangle()
.frame(width: 10, height: 1)
.foregroundColor(.red)
}
}
}
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id }
}
then generate number of DBScrollViewCellWrapper cell. when tap on cell, display tapped cell selected with line.
struct DBScrollView: View {
let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [DBScrollViewCellWrapper],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { $0 } //DBScrollViewCellWrapper(view: $0)
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(spacing: itemSpacing){
ForEach(self.views) { item in
item
.padding(5)
.border(Color.black)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views, id: \.id) { item in
item
.padding(5)
.border(Color.clear)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
value.showSelectionLine = true
completion(value,index)
}
}
Preview Code:
struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{DBScrollViewCellWrapper(view: AnyView(Text("\($0)")))}
DBScrollView(views: arrView, isHorizontal: false) { (cell, inx) in
cell.showSelectionLine = true
}
}
}
Problem
when tapped on cell, changed the value of cell but that not update.
Redesigned code for selection
struct DBScrollView: View {
private let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var selectedIndex: Int = -1
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [AnyView],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { DBScrollViewCellWrapper(view: AnyView($0))}
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(alignment: .center, spacing: itemSpacing){
ForEach(self.views.indices, id:\.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.foregroundColor(self.selectedIndex == index ? Color.yellow : Color.white)
.padding(5)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views.indices, id: \.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.padding(5)
.border(Color.white)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
completion(value,index)
}
}
struct DBScrollViewCellWrapper: Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id } } struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{AnyView(Text("\($0)"))}
DBScrollView(views: arrView, isHorizontal: true) { (cell, inx) in
}
.background(Color.black)
}
}

Swing passing a #State var as argument does not update view

I am following the Apple Developer Tutorial for SwingUI and I am pretty new. I am working on animations currently and I am confused about #State variables. Let's look at the following code:
import SwiftUI
struct HikeDetail: View {
let hike: Hike
#State var dataToShow = \Hike.Observation.elevation
var buttons = [
("Elevation", \Hike.Observation.elevation),
("Heart Rate", \Hike.Observation.heartRate),
("Pace", \Hike.Observation.pace),
]
var body: some View {
VStack {
HikeGraph(hike: hike, path: self.dataToShow)
.frame(height: 200)
HStack(spacing: 25) {
ForEach(buttons, id: \.0) { value in
Button(action: {
self.dataToShow = value.1
}) {
Text(value.0)
.font(.system(size: 15))
.foregroundColor(value.1 == self.dataToShow
? Color.green
: Color.accentColor)
.animation(nil)
}
}
}
}
}
}
HikeGraph is another SwiftUI View that produces a graph, it has three options:
- elevation => gray color
- heartRate => red color
- pace => purple color
Each of them has a different color and shape, however when I change between these three options, I only see one type of graph which is the initially defined one.
#State var dataToShow = \Hike.Observation.elevation
When I change the path variable of:
HikeGraph(hike: hike, path: dataToShow)
.frame(height: 200)
The graph does not change. However, when I change dataToShow variable by myself, I can observe the color change.
My question is how can I update the graph itself when the state variable is changed.
Edit: HikeGraph View
import SwiftUI
func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double>
where C.Element == Range<Double> {
guard !ranges.isEmpty else { return 0..<0 }
let low = ranges.lazy.map { $0.lowerBound }.min()!
let high = ranges.lazy.map { $0.upperBound }.max()!
return low..<high
}
func magnitude(of range: Range<Double>) -> Double {
return range.upperBound - range.lowerBound
}
extension Animation {
static func ripple(index: Int) -> Animation {
Animation.spring(dampingFraction: 0.5)
.speed(2)
.delay(0.03 * Double(index))
}
}
struct HikeGraph: View {
var hike: Hike
var path: KeyPath<Hike.Observation, Range<Double>>
var color: Color {
switch path {
case \.elevation:
return .gray
case \.heartRate:
return Color(hue: 0, saturation: 0.5, brightness: 0.7)
case \.pace:
return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
default:
return .black
}
}
var body: some View {
let data = hike.observations
let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] })
let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
let heightRatio = (1 - CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2
return GeometryReader { proxy in
HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
ForEach(data.indices) { index in
GraphCapsule(
index: index,
height: proxy.size.height,
range: data[index][keyPath: self.path],
overallRange: overallRange)
.colorMultiply(self.color)
.transition(.slide)
.animation(.ripple(index: index))
}
.offset(x: 0, y: proxy.size.height * heightRatio)
}
}
}
}