SwiftUI - using variable count within ForEach - swift

I have a view created through a ForEach loop which needs to take a variable count within the ForEach itself i.e. I need the app to react to a dynamic count and change the UI accoridngly.
Here is the view I am trying to modify:
struct AnimatedTabSelector: View {
let buttonDimensions: CGFloat
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
HStack {
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
ForEach(1..<tabBarViewModel.activeFormIndex + 1) { _ in
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.green)
}
Circle().frame(
width: buttonDimensions,
height: buttonDimensions)
.foregroundColor(
tabBarViewModel.activeForm.loginFormViewModel.colorScheme
)
ForEach(1..<tabBarViewModel.loginForms.count - tabBarViewModel.activeFormIndex) { _ in
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
}
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.gray)
}
}
}
And the viewModel I am observing:
class TabBarViewModel: ObservableObject, TabBarCompatible {
var loginForms: [LoginForm]
#Published var activeForm: LoginForm
#Published var activeFormIndex = 0
private var cancellables = Set<AnyCancellable>()
init(loginForms: [LoginForm]) {
self.loginForms = loginForms
self.activeForm = loginForms[0] /// First form is always active to begin
setUpPublisher()
}
func setUpPublisher() {
for i in 0..<loginForms.count {
loginForms[i].loginFormViewModel.$isActive.sink { isActive in
if isActive {
self.activeForm = self.loginForms[i]
self.activeFormIndex = i
}
}
.store(in: &cancellables)
}
}
}
And finally the loginFormViewModel:
class LoginFormViewModel: ObservableObject {
#Published var isActive: Bool
let name: String
let icon: Image
let colorScheme: Color
init(isActive: Bool = false, name: String, icon: Image, colorScheme: Color) {
self.isActive = isActive
self.name = name
self.icon = icon
self.colorScheme = colorScheme
}
}
Basically, a button on the login form itself sets its viewModel's isActive property to true. We listen for this in TabBarViewModel and set the activeFormIndex accordingly. This index is then used in the ForEach loop. Essentially, depending on the index selected, I need to generate more or less spacers in the AnimatedTabSelector view.
However, whilst the activeIndex variable is being correctly updated, the ForEach does not seem to react.
Update:
The AnimatedTabSelector is declared as part of this overall view:
struct TabIconsView: View {
struct Constants {
static let buttonDimensions: CGFloat = 50
static let buttonIconSize: CGFloat = 25
static let activeButtonColot = Color.white
static let disabledButtonColor = Color.init(white: 0.8)
struct Animation {
static let stiffness: CGFloat = 330
static let damping: CGFloat = 22
static let velocity: CGFloat = 7
}
}
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
ZStack {
AnimatedTabSelector(
buttonDimensions: Constants.buttonDimensions,
tabBarViewModel: tabBarViewModel)
HStack {
Spacer()
ForEach(tabBarViewModel.loginForms) { loginForm in
Button(action: {
loginForm.loginFormViewModel.isActive = true
}) {
loginForm.loginFormViewModel.icon
.font(.system(size: Constants.buttonIconSize))
.foregroundColor(
tabBarViewModel.activeForm.id == loginForm.id ? Constants.activeButtonColot : Constants.disabledButtonColor
)
}
.frame(width: Constants.buttonDimensions, height: Constants.buttonDimensions)
Spacer()
}
}
}
.animation(Animation.interpolatingSpring(
stiffness: Constants.Animation.stiffness,
damping: Constants.Animation.damping,
initialVelocity: Constants.Animation.velocity)
)
}
}
UPDATE:
I tried another way by adding another published to the AnimatedTabSelector itself to check that values are indeed being updated accordingly. So at the end of the HStack in this view I added:
.onAppear {
tabBarViewModel.$activeFormIndex.sink { index in
self.preCircleSpacers = index + 1
self.postCircleSpacers = tabBarViewModel.loginForms.count - index
}
.store(in: &cancellables)
}
And of course I added the following variables to this view:
#State var preCircleSpacers = 1
#State var postCircleSpacers = 6
#State var cancellables = Set<AnyCancellable>()
Then in the ForEach loops I changed to:
ForEach(1..<preCircleSpacers)
and
ForEach(1..<postCircleSpacers)
respectively.
I added a break point in the new publisher declaration and it is indeed being updated with the expected figures. But the view is still failing to reflect the change in values

OK so I seem to have found a solution - what I am presuming is that ForEach containing a range does not update dynamically in the same way that ForEach containing an array of objects does.
So rather than:
ForEach(0..<tabBarViewModel.loginForms.count)
etc.
I changed it to:
ForEach(tabBarViewModel.loginForms.prefix(tabBarViewModel.activeFormIndex))
This way I still iterate the required number of times but the ForEach updates dynamically with the correct number of items in the array

Related

Conditional view modifier sometimes doesn't want to update

As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.
I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.
Here is my code:
The view that houses the card views with the conditional view modifier
extension View {
#ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition == true {
transform(self).offset(x: 0, y: 0)
} else {
self
}
}
}
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
.resetCards(studyCards.isDone) { view in
view
}
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
}
}
HomeViewModel
class HomeViewModel: ObservableObject {
#Published var studySet: StudyModel = StudyModel()
#Published var allSets: [StudyModel] = [StudyModel()]
}
I initialize allSets with one StudyModel() to see it in the preview
StudyListViewModel
class StudyListViewModel: ObservableObject {
#Published var cardsSortedThrough: Int = 0
#Published var isDone: Bool = false
}
StudyModel
import SwiftUI
struct StudyModel: Hashable{
var title: String = ""
var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
var studyGoals = "One day"
var studyItem: [StudyItemModel] = []
}
Lastly, StudyItemModel
struct StudyItemModel: Hashable, Identifiable{
let id = UUID()
var itemTitle: String = ""
var itemDescription: String = ""
}
Once again, any help would be appreciated, thanks in advance!
I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.
I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.
The updated code:
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
.onChange(of: currentCard.isDone, perform: {_ in
offset = .zero
})
}
}
StudyListView
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count:
currentStudySet.allSets[index].studyItem.count)
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
Hope this helps anyone in the future!

Dynamically sized #State var

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}

Turn Button into Picker

I have this code, where there are three buttons which have values stored in each of them.
When I press the Add button, the values add up and appear on the list. However, I am not able to check which values are being selected.
How can I make the button look like a picker, so that when the user clicks the button A, B, or C, it will show with a checkmark that it has been selected and when the user taps the Add button, the value of the selected button and "gp" should show up on the list? Also, the checkmark should disappear once the Add button is selected so that the user can select another list.
Such as:
If A and B are selected, the list should look like:
A = 2.0, B = 5.0, gp = 7.0.
If A and C are selected, the list should look like:
A= 2.0, C = 7.0, gp = 9.0.
I have tried using Picker and other methods, however, I couldn't get through. I have found this as the best solution. However, I am not able to put a checkmark on the buttons and not able to show the selected values on the list.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var a = 2.0
#State var b = 5.0
#State var c = 7.0
//var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
#State var name = String()
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(gp)", amount: Double(gp)))
self.gp = 0.0
}
func stepcount() {
count += 1
}
var body: some View {
HStack {
Button(action: { gp += a }) {
Text("A =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += b }) {
Text("B =") .frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += c }) {
Text("C =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
}
HStack(spacing: 15) {
Button(
String(format: ""),
action: {
print("pay for the shift is ")
gp += loadpay
}
)
Button(
action: {
addNewToDo()
stepcount()
},
label: { Text("Add")}
)
}
Form {
ForEach(self.taskStore.tasks) { task in
Text(task.toDoItem)
}
}
}
}
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
The issue is you are trying to turn a Button into something it is not. You can create your own view that responds to a tap and keeps its state so it knows whether it is currently selected or not. An example is this:
struct MultiPickerView: View {
#State private var selectA = false
#State private var selectB = false
#State private var selectC = false
let A = 2.0
let B = 5.0
let C = 7.0
var gp: Double {
(selectA ? A : 0) + (selectB ? B : 0) + (selectC ? C : 0)
}
var body: some View {
VStack {
HStack {
SelectionButton(title: "A", selection: $selectA)
SelectionButton(title: "B", selection: $selectB)
SelectionButton(title: "C", selection: $selectC)
}
.foregroundColor(.blue)
Text(gp.description)
.padding()
}
}
}
struct SelectionButton: View {
let title: String
#Binding var selection: Bool
var body: some View {
HStack {
Image(systemName: selection ? "checkmark.circle" : "circle")
Text(title)
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(Color.blue, lineWidth: 4)
)
.padding()
.onTapGesture {
selection.toggle()
}
}
}
You could make it more adaptable by using an Array that has a struct that keeps the value and selected state, and run it though a ForEach, but this is the basics of the logic.

I create an instance of 3 different view models and assign each to a state object. How do I place this into a dispatch queue?

I have a Swift program that works as desired. I have 3 view models that each call a separate model. Each model calls a function that reads a separate large CSV file, performs some manipulation and returns a data frame. This takes some time and I would like to speed things up.
Swift offers a DispatchQueue that allows one to place code into an asynchronous global queue with QOS and I believe if I ran the creation of the view models in this fashion, I would display the initial view sooner.
The problem is: I have no idea how to incorporate it. Any help to point me in the right direction will be appreciated.
Below is my content view, one view model, and one model. The test dispatch queue code at the end runs successfully in a playground.
struct ContentView: View {
#StateObject var vooVM: VOOViewModel = VOOViewModel()
#StateObject var vfiaxVM: VFIAXViewModel = VFIAXViewModel()
#StateObject var principalVM: PrincipalViewModel = PrincipalViewModel()
#State private var selectedItemId: Int?
var body: some View {
NavigationView {
VStack (alignment: .leading) {
List {
Spacer()
.frame(height: 20)
Group {
Divider()
NavigationLink(destination: Summary(vooVM: vooVM, vfiaxVM: vfiaxVM, prinVM: principalVM), tag: 1, selection: $selectedItemId, label: {
HStack {
Image(systemName: "house")
.padding(.leading, 10)
.padding(.trailing, 0)
.padding(.bottom, 5)
Text("Summary")
.bold()
.padding(.bottom, 2)
} // end h stack
})
} // end group
NavigationLinks(listText: "VOO", dataFrame1: vooVM.timeSeriesDailyDF1, dataFrame5: vooVM.timeSeriesDailyDF5)
NavigationLinks(listText: "VFIAX", dataFrame1: vfiaxVM.timeSeriesDailyDF1, dataFrame5: vfiaxVM.timeSeriesDailyDF5)
NavigationLinks(listText: "Principal", dataFrame1: principalVM.timeSeriesDailyDF1, dataFrame5: principalVM.timeSeriesDailyDF5)
Divider()
Spacer()
} // end list
} // end v stack
} // end navigation view
.onAppear {self.selectedItemId = 1}
.navigationTitle("Stock Data")
.frame(width: 1200, height: 900, alignment: .center)
} // end body view
} // end content view
View Model
class VOOViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
var timeSeriesDailyDF1: DataFrame {
return vooModel.vooDF.0
}
var timeSeriesDailyDF5: DataFrame {
return vooModel.vooDF.1
}
var symbol: String {
return vooModel.symbol
}
var currentShares: Double {
return vooModel.currentShares
}
var currentSharePrice: Double {
let lastRowIndex: Int = vooModel.vooDF.0.shape.rows - 1
let currentPrice: Double = (vooModel.vooDF.0[row: lastRowIndex])[1] as! Double
return currentPrice
}
var percentGain: Double {
let pastValue: Double = (vooModel.vooDF.0[row: 0])[1] as! Double
let numRows: Int = vooModel.vooDF.0.shape.rows - 1
let curValue: Double = (vooModel.vooDF.0[row: numRows])[1] as! Double
let oneYearGain: Double = (100 * (curValue - pastValue)) / pastValue
return oneYearGain
}
}
Model
struct VOOModel {
var vooDF = GetDF(fileName: "FormattedVOO")
let symbol: String = "VOO"
let currentShares: Double = 1
}
Playground Code
let myQue = DispatchQueue.global()
let myGroup = DispatchGroup()
myQue.async(group: myGroup) {
sleep(5)
print("Task 1 complete")
}
myQue.async(group: myGroup) {
sleep(3)
print("Task 2 complete")
}
myGroup.wait()
print("All tasks completed")
I was able to solve my problem by using only 1 viewmodel instead of 3. The viewmodel calls all three models which were modified such that their function call to read a CSV file and place it into a dataframe is contained in a function. This function is in turn called within a function in the viewmodel which is called in the viewmodels init. Below is the updated code. Note that the ContentView was simplified to make testing easy.
New Content View:
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
let printValue1 = (viewModel.dataFrames.0.0[row: 0])[0]
let tempValue = (viewModel.dataFrames.0.0[row: 0])[1] as! Double
let tempValueFormatted: String = String(format: "$%.2f", tempValue)
Text("\(dateToStringFormatter.string(from: printValue1 as! Date))" + " " + tempValueFormatted )
.frame(width: 1200, height: 900, alignment: .center)
}
}
New ViewModel:
class ViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
#Published private var vfiaxModel: VFIAXModel = VFIAXModel()
#Published private var principalModel: PrincipalModel = PrincipalModel()
var dataFrames = ((DataFrame(), DataFrame()), (DataFrame(), DataFrame()), (DataFrame(), DataFrame()))
init() {
self.dataFrames = GetDataFrames()
}
func GetDataFrames() -> ((DataFrame, DataFrame), (DataFrame, DataFrame), (DataFrame, DataFrame)) {
let myQue: DispatchQueue = DispatchQueue.global()
let myGroup: DispatchGroup = DispatchGroup()
var vooDF = (DataFrame(), DataFrame())
var vfiaxDF = (DataFrame(), DataFrame())
var principalDF = (DataFrame(), DataFrame())
myQue.async(group: myGroup) {
vfiaxDF = self.vfiaxModel.GetData()
}
myQue.async(group: myGroup) {
principalDF = self.principalModel.GetData()
}
myQue.async(group: myGroup) {
vooDF = self.vooModel.GetData()
}
myGroup.wait()
return (vooDF, vfiaxDF, principalDF)
}
}
One of the new models. The other 2 are identical except for the CSV file they read.
struct VOOModel {
let symbol: String = "VOO"
let currentShares: Double = 1
func GetData() -> (DataFrame, DataFrame) {
let vooDF = GetDF(fileName: "FormattedVOO")
return vooDF
}
}

Animate view on property change SwiftUI

I have a view
struct CellView: View {
#Binding var color: Int
#State var padding : Length = 10
let colors = [Color.yellow, Color.red, Color.blue, Color.green]
var body: some View {
colors[color]
.cornerRadius(20)
.padding(padding)
.animation(.spring())
}
}
And I want it to have padding animation when property color changes. I want to animate padding from 10 to 0.
I've tried to use onAppear
...onAppear {
self.padding = 0
}
But it work only once when view appears(as intended), and I want to do this each time when property color changes. Basically, each time color property changes, I want to animate padding from 10 to 0. Could you please tell if there is a way to do this?
As you noticed in the other answer, you cannot update state from within body. You also cannot use didSet on a #Binding (at least as of Beta 4) the way you can with #State.
The best solution I could come up with was to use a BindableObject and sink/onReceive in order to update padding on each color change. I also needed to add a delay in order for the padding animation to finish.
class IndexBinding: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var index: Int = 0 {
didSet {
self.willChange.send()
}
}
}
struct ParentView: View {
#State var index = IndexBinding()
var body: some View {
CellView(index: self.index)
.gesture(TapGesture().onEnded { _ in
self.index.index += 1
})
}
}
struct CellView: View {
#ObjectBinding var index: IndexBinding
#State private var padding: CGFloat = 0.0
var body: some View {
Color.red
.cornerRadius(20.0)
.padding(self.padding + 20.0)
.animation(.spring())
.onReceive(self.index.willChange) {
self.padding = 10.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
self.padding = 0.0
}
}
}
}
This example doesn't animate in the Xcode canvas on Beta 4. Run it on the simulator or a device.
As of Xcode 12, Swift 5
One way to achieve the desired outcome could be to move the currently selected index into an ObservableObject.
final class CellViewModel: ObservableObject {
#Published var index: Int
init(index: Int = 0) {
self.index = index
}
}
Your CellView can then react to this change in index using the .onReceive(_:) modifier; accessing the Publisher provided by the #Published property wrapper using the $ prefix.
You can then use the closure provided by this modifier to update the padding and animate the change.
struct CellView: View {
#ObservedObject var viewModel: CellViewModel
#State private var padding : CGFloat = 10
let colors: [Color] = [.yellow, .red, .blue, .green]
var body: some View {
colors[viewModel.index]
.cornerRadius(20)
.padding(padding)
.onReceive(viewModel.$index) { _ in
padding = 10
withAnimation(.spring()) {
padding = 0
}
}
}
}
And here's an example parent view for demonstration:
struct ParentView: View {
let viewModel: CellViewModel
var body: some View {
VStack {
CellView(viewModel: viewModel)
.frame(width: 200, height: 200)
HStack {
ForEach(0..<4) { i in
Button(action: { viewModel.index = i }) {
Text("\(i)")
.padding()
.frame(maxWidth: .infinity)
.background(Color(.secondarySystemFill))
}
}
}
}
}
}
Note that the Parent does not need its viewModel property to be #ObservedObject here.
You could use Computed Properties to get this working. The code below is an example how it could be done.
import SwiftUI
struct ColorChanges: View {
#State var color: Float = 0
var body: some View {
VStack {
Slider(value: $color, from: 0, through: 3, by: 1)
CellView(color: Int(color))
}
}
}
struct CellView: View {
var color: Int
#State var colorOld: Int = 0
var padding: CGFloat {
if color != colorOld {
colorOld = color
return 40
} else {
return 0
}
}
let colors = [Color.yellow, Color.red, Color.blue, Color.green]
var body: some View {
colors[color]
.cornerRadius(20)
.padding(padding)
.animation(.spring())
}
}
Whenever there is a single incremental change in the color property this will toggle the padding between 10 and 0
padding = color % 2 == 0 ? 10 : 0