I'm sure my view could use all kinds of refactoring, but what I'm currently stuck on is why updating #State private var savedEntities isn't triggering a view refresh. I'm passing in updated data for the following variables from another view.
var overallSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var yearSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var monthSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var overallMoodCounts: [GroupingKey: [MoodCount]]
var yearMoodCounts: [GroupingKey: [MoodCount]]
var monthMoodCounts: [GroupingKey: [MoodCount]]
Then, in .onAppear(), depending on the picker selection (timeframe), I attempt to assign those values to #State private var savedEntities with the expectation that the view and Chart will update accordingly. It does not. If I use the Picker, the .onChange() method seems to work and my view & Chart data gets updated. Also if I restart the app. Appreciate any help. Thanks!
#available(iOS 16.0, *)
struct SwiftUIViewTemplate: View {
#StateObject private var UIState = UIStateModel()
#State private var timeframe: TimeFrame = .overall
#State private var savedEntities: [GroupingKey: [MoodCount]] = [:]
#State private var sortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element] = []
var overallSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var yearSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var monthSortedKeys: [Dictionary<GroupingKey, [MoodCount]>.Keys.Element]
var overallMoodCounts: [GroupingKey: [MoodCount]]
var yearMoodCounts: [GroupingKey: [MoodCount]]
var monthMoodCounts: [GroupingKey: [MoodCount]]
var body: some View {
let spacing: CGFloat = 16
let widthOfHiddenCards: CGFloat = 32
VStack {
Text("Moods")
.padding(.top, 50)
.foregroundColor(App.Colors.secondary.suColor)
.font(.system(size: 44))
Picker("Timeframe", selection: $timeframe) {
Text("Overall").tag(TimeFrame.overall)
Text("Year").tag(TimeFrame.year)
Text("Month").tag(TimeFrame.month)
}
.onAppear() {
switch timeframe {
case TimeFrame.year:
savedEntities = yearMoodCounts
sortedKeys = yearSortedKeys
case TimeFrame.month:
savedEntities = monthMoodCounts
sortedKeys = monthSortedKeys
default:
savedEntities = overallMoodCounts
sortedKeys = overallSortedKeys
}
}
.onChange(of: timeframe, perform: { timeframe in
switch timeframe {
case TimeFrame.year:
savedEntities = yearMoodCounts
sortedKeys = yearSortedKeys
case TimeFrame.month:
savedEntities = monthMoodCounts
sortedKeys = monthSortedKeys
default:
savedEntities = overallMoodCounts
sortedKeys = overallSortedKeys
}
UIState.activeCard = sortedKeys.count - 1
})
.pickerStyle(.segmented)
.padding(.bottom, 20)
Carousel (
numberOfItems: CGFloat(savedEntities.count),
spacing: spacing,
widthOfHiddenCards: widthOfHiddenCards
) {
ForEach(Array(sortedKeys.enumerated()), id: \.element) { index, groupingKey in
Item(
_id: index,
spacing: spacing,
widthOfHiddenCards: widthOfHiddenCards
){
GroupBox {
let year = String(groupingKey.dateComponents?.year ?? 99)
let month = String(groupingKey.dateComponents?.month ?? 99)
Text("\(year) \(month)")
.foregroundColor(.black)
Chart {
ForEach(savedEntities[groupingKey] ?? [], id: \.self) { moodCount in
BarMark(x: .value("Entries", moodCount.count),
y: .value("Mood", moodCount.mood), width: .ratio(0.6))
}
}
.frame(maxWidth: .infinity)
.chartXAxis(.hidden)
.chartYAxis(content: {
AxisMarks(preset: .extended) { axisValue in
AxisValueLabel(centered: true) {
if let stringValue = axisValue.as(String.self) {
let confettiIcon = RevenueService().getConfettiIcon(identifier: stringValue)
Image(uiImage: confettiIcon)
}
}
}
})
}
}
.equatable()
.shadow(color: .gray, radius: 8, x: -4, y: 4)
.animation(.easeInOut) // Makes the cards slide in & out smoothly
}
}
Spacer()
}
.padding()
.environmentObject(UIState)
}
}
Related
I have a tabview (page style) that I am using to create an automatic slideshow. Each slide is not assigned to the currentIndex for some odd reason. Neither does the timer work or manually switching tabs with the default dot controls given with tabview.
If i set the tag to selectedIndex the tabview will default to the first slide.
No errors to the console.
Any help is greatly appreciated!
struct HomeView: View {
#EnvironmentObject private var hvm: HomeViewModel
#State private var selectedFilter: String = "Popular"
#State private var selectedMedia: MediaModelResult? = nil
#State private var showDetailView: Bool = false
#State private var currentIndex: Int = 0
#State var isFilterSelected: Bool = true
#Namespace var animation
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
ZStack {
Color.theme.background.ignoresSafeArea()
VStack {
header
VStack(alignment: .leading, spacing: 15) {
Text("Upcoming Movies")
.foregroundColor(Color.theme.secondaryAccent)
.font(.subheadline)
.padding(.leading, 15)
TabView(selection: $currentIndex) {
ForEach(hvm.upcomingFilms) { film in
MediaImageView(mediaPath: film, poster: false)
.scaledToFill()
.tag(film)
}
}
.tabViewStyle(PageTabViewStyle())
.clipShape(RoundedRectangle(cornerRadius: 5))
.padding(.horizontal, 15)
.frame(width: UIScreen.main.bounds.width, height: 180)
.onReceive(timer, perform: { _ in
withAnimation(.default) {
currentIndex = currentIndex < hvm.upcomingFilms.count ? currentIndex + 1 : 0
}
})
}
scrollViewContent
}
}
.preferredColorScheme(.dark)
.navigationBarHidden(true)
}
}
}
struct MediaImageView: View {
#StateObject var mivm: MediaImageViewModel
var poster: Bool
init(mediaPath: MediaModelResult, poster: Bool) {
self.poster = poster
_mivm = StateObject(wrappedValue: MediaImageViewModel(mediaPath: mediaPath))
}
var body: some View {
if poster {
ZStack {
if let image = mivm.poster {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if mivm.isLoading {
ProgressView()
}
}
} else {
ZStack {
if let image = mivm.backdrop {
Image(uiImage: image)
.resizable()
} else if mivm.isLoading {
ProgressView()
}
}
}
}
}
class HomeViewModel: ObservableObject {
#Published var tabBarImageNames = ["house", "rectangle.stack", "clock.arrow.circlepath", "magnifyingglass"]
#Published var filterTitles = ["Popular Now", "Top Rated", "New"]
#Published var popularFilms: [MediaModelResult] = []
#Published var topRatedFilms: [MediaModelResult] = []
#Published var upcomingFilms: [MediaModelResult] = []
#Published var popularTV: [MediaModelResult] = []
#Published var topRatedTV: [MediaModelResult] = []
private let dataService = MediaDataService()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscribers()
}
func addSubscribers() {
dataService.$popularFilms
.sink { [weak self] (returnedFilms) in
self?.popularFilms = returnedFilms
}
.store(in: &cancellables)
dataService.$topRatedFilms
.sink { [weak self] (returnedFilms) in
self?.topRatedFilms = returnedFilms
}
.store(in: &cancellables)
dataService.$upcomingFilms
.sink { [weak self] (returnedFilms) in
self?.upcomingFilms = returnedFilms
}
.store(in: &cancellables)
dataService.$popularTV
.sink { [weak self] (returnedFilms) in
self?.popularTV = returnedFilms
}
.store(in: &cancellables)
dataService.$topRatedTV
.sink { [weak self] (returnedFilms) in
self?.topRatedTV = returnedFilms
}
.store(in: &cancellables)
}
}
Solution:
TabView(selection: $currentIndex) {
ForEach(0..<hvm.upcomingFilms.count) { film in
MediaImageView(mediaPath: hvm.upcomingFilms[film], poster: false)
.scaledToFill()
}
}
I have a list of entries each with an attached date. I would like to display the date only if there is a change in date. I first developed this software in iOS 14.4 that resulted in a view immutable error. This was because I was storing and changing a copy of the entry date.
Now in version iOS 14.5 I don't see the immutable error. But my software still doesn't work. If you run the code and look in the console you will note that Xcode is going through my six entries twice: the first time is always true (show the date) and the second time always false (don't show the date). Why?
In my actual code I am using dates of type Date instead of Strings in this example code. In my actual code, operation hangs as it loops endlessly through my function checkDate (Many times more than the number of entries). Does date of type Date include the time causing the compare to fail?
Is there a better way to prevent display of the date if it is the same as the previous entry?
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var oldDate: String = ""
#Published var kitchenItem: [KitchenItem]
init() {
self.kitchenItem = []
let item0 = KitchenItem(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85)
self.kitchenItem.append(item0)
let item1 = KitchenItem(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39)
self.kitchenItem.append(item1)
let item2 = KitchenItem(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35)
self.kitchenItem.append(item2)
let item3 = KitchenItem(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75)
self.kitchenItem.append(item3)
let item4 = KitchenItem(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65)
self.kitchenItem.append(item4)
let item5 = KitchenItem(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95)
self.kitchenItem.append(item5)
}
}
struct ContentView: View {
#ObservedObject var pantry: Pantry = Pantry()
var body: some View {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0..<pantry.kitchenItem.count, id: \.self) { item in
VStack (alignment: .leading) {
showRow(item: item)
}
}
}
}
}
struct showRow: View {
#ObservedObject var pantry: Pantry = Pantry()
var item: Int
var body: some View {
// don't show the date if is the same as the previous entry
let newDate = pantry.kitchenItem[item].itemDate
if checkDate(newDate: newDate) == true {
Text("\n\(newDate)")
.font(.title2)
.padding(.leading, 10)
}
HStack {
Text("\(pantry.kitchenItem[item].item)")
.padding(.leading, 50)
.frame(width: 150, alignment: .leading)
Text("\(pantry.kitchenItem[item].itemCost, specifier: "$%.2f")")
}
}
func checkDate(newDate: String) -> (Bool) {
print(" ")
print("new date = \(newDate)")
if newDate == pantry.oldDate {
print("false: don't show the date")
return false
} else {
pantry.oldDate = newDate
print("old date = \(pantry.oldDate)")
print("true: show the date")
return true
}
}
}
Actual code:
struct ListView: View {
#EnvironmentObject var categories: Categories
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
var money: String = ""
var xchRate: Double = 0.0
var cat: Int = 0
var mny: String = ""
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
GeometryReader { g in
ScrollView {
LazyVStack (alignment: .leading) {
TitleView()
ForEach(currTrans, id: \.self) { item in
showRow(item: item, priorDate: priorDate(forItemIndex: item), g: g)
}
.onDelete(perform: deleteItem)
}
.font(.body)
}
}
}
private func priorDate(forItemIndex item: Int) -> Date? {
guard item > 0 else { return nil }
return currTrans[item - 1].entryDT
}
}
struct showRow: View {
#EnvironmentObject var userData: UserData
var item: CurrTrans
var priorDate: Date?
var g: GeometryProxy
var payType = ["Cash/Debit", "Credit"]
var body: some View {
// don't show the date if is the same as the previous entry
let gotDate = item.entryDT ?? Date()
let newDate = gotDate.getFormattedDate()
Text("\(newDate)")
.opacity(gotDate == priorDate ? 0 : 1)
.font(.title2)
.padding(.leading, 10)
displays entry parameters in HStack...
Thou shalt not mutate thy data inside thy body method, for it is an abomination in the eyes of SwiftUI.
Modifying oldDate inside the body method is wrong. SwiftUI will get confused if you modify the data it is observing while it is rendering your views. Furthermore, SwiftUI doesn't make any guarantees about the order in which it renders the children of a LazyVStack (or any other container).
Is there a better way to prevent display of the date if it is the same as the previous entry?
Yes. Pass the current entry, and the prior entry's date, to the entry view.
Here's your data model and store, without the cruft:
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var kitchenItems: [KitchenItem] = [
.init(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85),
.init(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39),
.init(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35),
.init(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75),
.init(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65),
.init(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95),
]
}
For each KitchenItem, you need to also extract the prior item's date, if there is a prior item. We'll use a helper method, priorDate(forItemIndex:), to do that. Also, you need to use StateObject, not ObservedObject, if you're going to create your store inside the view. Thus:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
ScrollView(.vertical) {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0 ..< pantry.kitchenItems.count) { i in
if i > 0 {
Divider()
}
KitchenItemRow(item: pantry.kitchenItems[i], priorDate: priorDate(forItemIndex: i))
}
}
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
}
Here is KitchenItemRow. You can see that it makes the date Text transparent if the date is the same as the prior item's date. I keep it in place but make it transparent so the row lays out the same:
struct KitchenItemRow: View {
var item: KitchenItem
var priorDate: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(item.item)
Spacer()
Text("\(item.itemCost, specifier: "$%.2f")")
}
Text(item.itemDate)
.opacity(item.itemDate == priorDate ? 0 : 1)
}
.padding([.leading, .trailing], 10)
}
}
And here is TitleView, extracted from ContentView for hygiene:
struct TitleView: View {
var body: some View {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
}
}
Result:
UPDATE
Since your “real code” uses onDelete, it's important to give ForEach an id for each item instead of using the indexes.
Note that onDelete only works inside List, not inside LazyVStack.
So we need to map each item to its index, so we can find the prior item. Here's a revised version of my ContentView that uses onDelete:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
let indexForItem: [UUID: Int] = .init(
uniqueKeysWithValues: pantry.kitchenItems.indices.map {
(pantry.kitchenItems[$0].id, $0) })
List {
TitleView()
ForEach(pantry.kitchenItems, id: \.id) { item in
let i = indexForItem[item.id]!
KitchenItemRow(item: item, priorDate: priorDate(forItemIndex: i))
}
.onDelete(perform: deleteItems(at:))
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
private func deleteItems(at offsets: IndexSet) {
pantry.kitchenItems.remove(atOffsets: offsets)
}
}
In my testing, this works and allows swipe-to-delete. I trust you can adapt it to your “real” code.
I created a custom alert. I want the product to be added to the basket when the Ok button on Alert is clicked on the first screen. When the Ok button is pressed on the second screen, the purchase of the product is requested. I called the same alert on 2 pages and I want it to take different actions. I couldn't do that with #Escaping.
AlertView
struct AlertView: View {
#Binding var openShowAlert: Bool
#State var closeShowAlert: Bool = false
#State var openState: CGFloat = -UIScreen.main.bounds.height
#State var closeState: CGFloat = UIScreen.main.bounds.height
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var cancelButtonText: String = ""
var body: some View {
VStack {
Text(title)
.michromaFont(size: 20)
.padding(.top)
Spacer()
Text(message)
.michromaFont(size: 18)
Spacer()
HStack {
Button(action: {
self.openShowAlert = false
openState = -UIScreen.main.bounds.height
closeState = UIScreen.main.bounds.height
}) {
Text(cancelButtonText)
.foregroundColor(.red)
}
Spacer()
Button(action: {}) {
Text(okButtonText)
}
}
.michromaFont(size: 18)
.padding([.horizontal, .bottom])
}
.neumorphisimBackground(width: 300, height: 200)
.offset(y: self.openShowAlert ? self.openState : self.closeState)
.animation(.easeInOut)
.onChange(of: self.openShowAlert, perform: { value in
if value {
self.openState = .zero
}
})
}
}
DetailView
On this screen, click Alert presentation to add the product to the cart.
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
var device = UIDevice.current.userInterfaceIdiom
#State var width: CGFloat = 300
#State var height: CGFloat = 450
#Binding var text: String
#State var showAlert: Bool = false
var body: some View {
ZStack() {
......
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
CartView Click I am providing an alert on this screen to purchase the product.
struct CartView: View {
#State var cartList = [1,2,3,4,5,6,7,8,9]
#Environment(\.presentationMode) var presentationMode
#State var showAlert: Bool = false
var body: some View {
ZStack(alignment: .top) {
.....
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
How can I send two different actions in the same alert.
Hmm, I don't see why it shouldn't work with a closure. Have you tried passing over a closure like so?
struct AlertView: View {
...
var okButtonAction: () -> ()
var body: some View {
...
Button(action: okButtonAction) {
Text(okButtonText)
}
}
}
Usage
AlertView(openShowAlert: self.$showAlert) {
// Your custom code
}
Alternative Idea
You could work with Combine and create a publisher with a specific key to identify the sender screen. Then you can put your custom code inside .onReceive().
I have a form the User completes which includes an image upload, as this takes a little time to upload to the server I want to show a progress view of the upload.
I have created the view and connected it to the upload progress data which all works fine, and I can have it as a permanent overlay, but I would like to overlay it only when the upload button is pressed.
How do I do that?
my progress view
struct ProgressIndicator: View {
#State var imageUploadAmount = 0.0
var progress: Progress
var body: some View {
VStack {
ProgressView("Uploading…", value: imageUploadAmount, total: 1)
}.onReceive(progress.$upload, perform: { _ in
imageUploadAmount = progress.upload
})
}
}
the form view
struct AddAClimb: View {
#EnvironmentObject var screenCoordinator: ScreenCoordinator
#ObservedObject var progress = Progress()
#State private var showingImagePicker = false
#State private var selectedTypeIndex: Int = 0
#State private var name = ""
#State private var grade = ""
#State private var description = ""
#State private var country = ""
#State private var region = ""
#State private var crag = ""
#State private var section = ""
#State private var height = ""
#State private var long = "0.0"
#State private var lat = "0.0"
#State private var stars = 0
#State private var image: Image? //= "default"
#State private var imageUrl = "default"
#State private var inputImage: UIImage?
#State private var icons = [""]
#State var pitchCount = 1
#State private var pitch = ["",""]
let cloudinary = CLDCloudinary(configuration: ImageController().config)
private var climbTypeOptions = ["Trad", "Sport", "Boulder", "Solo", "Aid"]
var body: some View {
Form{
Section(header: Text("Climb Detial")) {
HStack{
TextField("Climb Name", text: $name)
}
Picker("Climb Type", selection: $selectedTypeIndex){
ForEach(0..<climbTypeOptions.count){
Text(self.climbTypeOptions[$0])
}
}.pickerStyle(SegmentedPickerStyle())
HStack{
Text("Grade:")
TextField("enter the grade", text: $grade)
}
HStack{
Text("Height:")
TextField("enter the height", text: $height)
}
Text("Description:")
TextEditor(text: $description)
HStack{
Text("Pitches:")
Spacer()
Stepper(value: $pitchCount,
in: 1...100,
label: {
Text(" \(self.pitchCount)")
})
}
ForEach(0..<pitchCount){ x in
HStack{
Text("\(x + 1):")
TextField("Pitch Description", text: $pitch[x] )
}
}
}
Section(header: Text("Location")) {
TextField("Country", text: $country )
TextField("Region", text: $region )
TextField("Crag", text: $crag)
TextField("Sector", text: $section)
HStack{
Text("Longitude:")
TextField("Longitude", text: $long).keyboardType(.numbersAndPunctuation)
}
HStack{
Text("Latitude:")
TextField("Latitude", text: $lat).keyboardType(.numbersAndPunctuation)
}
}
Section(header: Text("Images/Videos")) {
image?.resizable().aspectRatio(contentMode: .fit)
HStack{
Spacer()
Button(action: {
self.showingImagePicker = true
}, label: {
Text("Select an Image")
})
Spacer()
}
}
Section(header: Text("Save")) {
HStack{
Spacer()
Button(action:{
ClimbController().addNewClimbToDB(name: name, desc: description, country: country, region: region, crag: crag, sector: section, long: Double(long) ?? 0.0, lat: Double(lat) ?? 0.0, pitches: pitch, stars: stars, grade: grade, climbType: climbTypeOptions[selectedTypeIndex], icons: icons, imageUrl: imageUrl, height: Double(height) ?? 1, inputImage: inputImage, progress: progress, screenCoordinator: screenCoordinator)
print("x")
}){
Text("Add Climb")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 5, maxHeight: 10)
.font(.title)
.padding()
.background(Color.yellow)
.foregroundColor(.white)
.padding(10)
.border(Color.yellow, width: 5).cornerRadius(8)
}
Spacer()
}
}
}.navigationBarTitle("Add a Climb", displayMode:.inline).sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
It is not all components present, but it could be like the following
}.navigationBarTitle("Add a Climb", displayMode:.inline).sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}
.overlay(Group {
if self.progress.isUploading {
ProgressIndicator(progress: self.progress) // << here !!
}
})
When I type some text in one of the text fields, somehow it adds to all the text fields the same text at the same time. I am trying to separate them from each other so I can assign them correctly.
struct CardInfo : View {
#State var creditCard : CreditCard
#State var isSaved: Bool = false
#State private(set) var text = ""
var body: some View {
VStack {
CustomTextField(data: $text, tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
.onReceive(Just(text)) { data in
self.creditCard.cardOwnerName = self.text
}
CustomTextField(data: $text, tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
.onReceive(Just(text)) { data in
self.creditCard.cardNumber = self.text
}
struct CustomTextField: View {
#Binding var data : String
var tFtext: String = ""
var tFImage: String = ""
var body: some View {
HStack {
Image(tFImage)
.resizable()
.frame(width: 20, height: 20)
.padding()
TextField(tFtext, text: $data)
.padding()
.font(Font.custom("SFCompactDisplay", size: 16))
.foregroundColor(.black)
}
.background(RoundedRectangle(cornerRadius: 10))
.foregroundColor(Color(#colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9882352941, alpha: 1)))
}
}
You need to use separate #State variables for each TextField:
struct CardInfo : View {
#State var creditCard : CreditCard
#State var isSaved: Bool = false
#State private(set) var cardOwnerName = ""
#State private(set) var cardNumber = ""
var body: some View {
VStack {
CustomTextField(data: $cardOwnerName, tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
.onReceive(Just(cardOwnerName)) { data in
self.creditCard.cardOwnerName = data
}
CustomTextField(data: $cardNumber, tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
.onReceive(Just(cardNumber)) { data in
self.creditCard.cardNumber = data
}
...
}
}
}