SwiftUI DataStructure for ForEach.. (how to delete) - swiftui

I'm working on an app which show images when tapping buttons.
I have a 4 buttons, and Each buttons has a action that add a image.
The problem I have is that when I remove by ontapGesture, The deleting order is not my want.
Ex) button 1 -> button 2 -> button 3 -> button 4 : Now I can see 4 images in vstack, plus, minus, multiply, divide.
But If I tap button 1 now, the plus image still there.
I think the problem arises because of array. What dataStructure should I use instead?
`
import SwiftUI
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
array.append(0)
} label: {
Text("Button 0")
}
Button {
array.append(1)
} label: {
Text("Button 1")
}
Button {
array.append(2)
} label: {
Text("Button 2")
}
Button {
array.append(3)
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[index]
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
`
I found out when I tap button 1 again, it delete the data in array[0].
But there is still array[0], because array always has index.
I understand! but I can not come up with data structure for images.

Using your current setup, you could try this approach, using the content of the array array[index] for the index into systemDictionary, and to limit the set of images to 4 only, use if !array.contains(0) { array.append(0) } as shown in the code:
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
if !array.contains(0) { array.append(0) } // <-- here
} label: {
Text("Button 0")
}
Button {
if !array.contains(1) { array.append(1) }
} label: {
Text("Button 1")
}
Button {
if !array.contains(2) { array.append(2) }
} label: {
Text("Button 2")
}
Button {
if !array.contains(3) { array.append(3) }
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[array[index]] // <-- here
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
EDIT-1: using a specific structure for the images:
struct Picture: Identifiable {
let id = UUID()
var image: Image
var label: String
var isVisible: Bool = false
}
struct ContentView: View {
#State var pictures: [Picture] = [
Picture(image: Image(systemName: "plus.circle"), label: "Button 0"),
Picture(image: Image(systemName: "minus.circle"), label: "Button 1"),
Picture(image: Image(systemName: "multiply.circle"), label: "Button 2"),
Picture(image: Image(systemName: "divide.circle"), label: "Button 3")
]
var body: some View {
VStack {
HStack {
ForEach($pictures) { $picture in
Button {
picture.isVisible = true
} label: {
Text(picture.label)
}
}
}
ForEach($pictures) { $picture in
if picture.isVisible {
picture.image
.font(.system(size: 30))
.padding()
.onTapGesture {
picture.isVisible = false
}
}
}
}
}
}

Related

Animate text position simultaneously with text changing in SwiftUI

I want to animate the position of Expand button, but instead it appears right at the final position. There's a strange hack .transition(.scale) that fixes my problem, but I hope to see a better not hacky solution.
struct TextView: View {
#State var isExpanded = false
var body: some View {
VStack(spacing: 10) {
Text("This is the most recent test comment with a picture. Lörem ipsum berölogi kjolprotest. Trist gigade. Sms-livräddare grönt elcertifikat.")
.lineLimit(isExpanded ? nil : 2)
HStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text(isExpanded ? "Less" : "Expand")
}
// .transition(.scale)
}
}
.padding(.all)
.background(Color.yellow)
.cornerRadius(13)
.padding(.horizontal)
}
}
One option is to use a ZStack containing Buttons in both states, and use the .opacity modifier to hide or show them…
struct ContentView: View {
#State var isExpanded = false
var body: some View {
VStack(spacing: 10) {
Text("This is the most recent test comment with a picture. Lörem ipsum berölogi kjolprotest. Trist gigade. Sms-livräddare grönt elcertifikat.")
.lineLimit(isExpanded ? nil : 2)
HStack {
ZStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Less")
}
.opacity(isExpanded ? 1 : 0)
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Expand")
}
.opacity(isExpanded ? 0 : 1)
}
}
}
.padding(.all)
.background(Color.yellow)
.cornerRadius(13)
.padding(.horizontal)
}
}
Or alternatively, use .matchedGeometryEffect
if isExpanded {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Less")
}
.matchedGeometryEffect(id: "button", in: namespace)
} else {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Spacer()
Text("Expand")
}
.matchedGeometryEffect(id: "button", in: namespace)
}
Obviously there's some duplication so you would pull the Button out into a func or its own View struct.

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

Result of 'View' initializer is unused

My Custom button does not tap and passes to next view called AddCreditCardView.
I have tested the button action with print statement and it won't work too.
I copied my code below in separate.
This is my ContentView
import SwiftUI
struct ContentView: View {
let membershipRows = MembershipData.listData()
let corporateRows = CorporateData.listData()
let otherOperationRows = OtherOperationsData.listData()
#State var selectedCard = CreditCard(id: "", cardOwnerName: "", cardNumber: "", cardExpMonth: "", cardExpYear: "", ccv: "")
#State var shown: Bool = false
var body: some View {
NavigationView {
VStack {
List {
Section(header: Text("Bireysel")) {
ForEach(membershipRows) { row in
NavigationLink(destination: CreditCardView()) {
RowElementView(row: row)
}
}
}
if self.corporateRows.count == 0
{
Rectangle()
.background(Color(.white))
.edgesIgnoringSafeArea(.all)
.foregroundColor(.white)
.padding(.vertical,32)
}
else {
Section(header: Text("Kurumsal")) {
ForEach(corporateRows) { row in
RowElementView(row: row)
}
}
}
Section(header: Text("Diger Islemler")) {
ForEach(otherOperationRows) { row in
RowElementView(row: row)
}
}
Rectangle()
.foregroundColor(.clear)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height )
}
.navigationBarTitle("Odeme Yontemleri", displayMode: .inline)
.font(Font.custom("SFCompactDisplay", size: 16))
Button(action: {
AddCreditCardView(item: self.selectedCard)
}, label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
})
}
}
}
This is my AddCreditCardView
import SwiftUI
struct AddCreditCardView: View {
var item: CreditCard
var body: some View {
NavigationView {
VStack {
TopBar()
Spacer()
CardInfo()
Spacer()
}
.navigationBarTitle("Odeme Yontemi", displayMode: .inline)
}
}
}
struct TopBar : View {
var body: some View {
VStack {
HStack() {
Image("addcreditcard")
Image("line")
Image("locationBar")
Image("line")
Image("check-circle")
}
.padding(.horizontal,62)
VStack {
Text("Kredi Karti Ekle")
.font(Font.custom("SFCompactDisplay-Bold", size: 14))
Text("1. Adim")
.font(Font.custom("SFCompactDisplay", size: 14))
.fontWeight(.regular)
.foregroundColor(.gray)
}
}
.padding()
}
}
struct CardInfo : View {
var body: some View {
VStack {
CustomTextField(tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
CustomTextField(tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
HStack {
CreditCardDateTextField(tFtext: "", tFImage: "date")
.textContentType(.creditCardNumber)
Spacer()
Text("|")
.foregroundColor(.black)
.overlay(
Rectangle()
.frame(width: 60, height: 53))
CustomTextField(tFtext: "CCV", tFImage: "")
.textContentType(.creditCardNumber)
}
.foregroundColor(Color(#colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9882352941, alpha: 1)))
CustomTextField(tFtext: "Kart Ismi", tFImage: "cardEdit")
Spacer()
}
}
}
And Finally, this is my CreditCard Model
import SwiftUI
struct CreditCard: Identifiable {
var id: String = UUID().uuidString
var cardOwnerName : String
var cardNumber: String
var cardExpMonth: String
var cardExpYear: String
var ccv: String
Seems like you are trying to navigate to AddCreditCardView on the button press. The action closure can not present a view automatically like that! You should change that code to something like this:
#State var navigated = false
,,,
NavigationLink("AddCreditCardView", destination: AddCreditCardView(), isActive: $navigated)
Button(action: { self.navigated.toggle() },
label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable) })
changing the navigated state will show the next page as it seems you wished.

How do I insert a row of buttons with a staggered, scale animation?

I'm attempting to recreate the following animation from the Castro app...
(The GIF is slowed down so you can see the effect)
As you can see in the GIF above, you have a row of buttons that appear when the cell is tapped. Each button has a zoom-in and zoom-out effect. The animations are staggered, such that the first button finishes first and the last button finishes last.
What I've tried...
struct SwiftUIView: View {
#State var show: Bool = false
var body: some View {
VStack {
Button(action: {
withAnimation {
show.toggle()
}
}, label: {
Text("Button")
})
HStack {
if show {
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
Button(action: {}, label: { Image(systemName: "circle") })
.transition(.scale)
}
}
}
}
}
As you can see in the image above... Each button does zoom in, but only as the view is removed. Also, I don't know how to stagger the animation.
Try the following:
struct ContentView: View {
#State var show = false
#State var showArray = Array(repeating: false, count: 3)
var body: some View {
VStack {
Button(action: toggleButtons) {
Text("Button")
}
HStack {
ForEach(showArray.indices, id: \.self) { index in
self.circleView(for: index)
}
}
}
}
#ViewBuilder
func circleView(for index: Int) -> some View {
if show {
ZStack {
Image(systemName: "circle")
.opacity(.leastNonzeroMagnitude)
.animation(nil)
if showArray[index] {
Image(systemName: "circle")
.transition(.scale)
}
}
}
}
func toggleButtons() {
showArray.indices.forEach { index in
withAnimation {
self.show = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(index)) {
withAnimation {
self.showArray[index].toggle()
if index == self.showArray.count - 1, self.showArray[index] == false {
self.show = false
}
}
}
}
}
}
It uses a little hack to align the views correctly - in ZStack there is a fake Image with almost no opacity.

ForEach(0...5,id: \.self) { n in --what is the scope of "n"?

See the following TestView. When I clicked "Button 5", I see the alert shows "Button 0 clicked".
struct TestView: View {
#State var showAlert = false
var body: some View {
VStack {
ForEach(0...5, id: \.self) { n in
Button(action: {
self.showAlert.toggle()
}) {
Text("Button \(n)")
}
.padding(20)
.border(Color.green, width: 4)
.padding(8)
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Button \(n) clicked"))
}
}
}
}
}
Regardless of which button I click, the alert always show "Button 0 clicked". I was expecting each button should show its own button index. Wondering why is that?
I suspect this is happening because everything in your TestView re-renders while state variable changes. So your alert is displaying only for the first loop iteration. The decision is to change other variable, which should contain clicked button index:
struct TextView: View {
#State var showAlert = false
#State var clickedButtonIndex = 0
var body: some View {
VStack {
ForEach(0...5, id: \.self) { n in
Button(action: {
self.showAlert.toggle()
self.clickedButtonIndex = n
}) {
Text("Button \(n)")
}
.padding(20)
.border(Color.green, width: 4)
.padding(8)
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Button \(self.clickedButtonIndex) clicked"))
}
}
}
}
}
And you'll have this result: