If $score is bound to score, how come the textfield won't clear? How do I make it clear? Also, how would I get "Enter Score" to show rather than "0"? Thanks.
import SwiftUI
struct keyType: View {
#State private var score: Double = 0
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
return formatter
}()
func buttonClear() {
self.score = 0;
}
var body: some View {
VStack {
TextField("Enter Score", value: $score, formatter: formatter)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text(String(format: "Your Score: %.2f", score))
Button("CLEAR") {buttonClear()}
.padding()
}
}
}
If you use TextField(value: format:) the "empty" value is 0, but the prompt "Enter score" is only shown when the input string is "".
But you can use the standard TextField(text:) version and convert to a value manually. Then the prompt shows.
struct ContentView: View {
#State private var score: Double = 0
#State private var scoreTextInput = ""
func buttonClear() {
self.score = 0
self.scoreTextInput = ""
}
var body: some View {
VStack {
TextField("Enter Score", text: $scoreTextInput, onCommit: {
score = Double(scoreTextInput) ?? 0
})
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text("Your Score: \(score, specifier: "%.2f")")
Button("CLEAR") {buttonClear()}
.padding()
}
}
}
Related
I am using in swiftUI. When select picker, it is not changing. Here is code..
Here is datamodel:
struct SourceAccountModel:Codable,Identifiable{
var id: Int
let accountNumber: String
let accountTitle: String
let priaryAccount: String
init(id:Int=0,accountNumber: String, accountTitle: String, priaryAccount: String) {
self.id = id
self.accountNumber = accountNumber
self.accountTitle = accountTitle
self.priaryAccount = priaryAccount
}
}
Here is my code
struct Test2: View {
#State private var selectedOption = "Option 1"
#State private var sourceAccountList = [SourceAccountModel]()
var body: some View {
VStack{
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white)
.shadow(radius: 2)
Picker(selection: $selectedOption,label: EmptyView()) {
ForEach (0..<sourceAccountList.count,id: \.self) {
Text(sourceAccountList[$0].accountNumber)
}
}
.padding(8)
}
.frame(maxWidth: .infinity)
}.onAppear{
intitializeValue()
}
}
func intitializeValue(){
self.sourceAccountList.append(SourceAccountModel(id:1,accountNumber: "Option 1", accountTitle: "", priaryAccount: ""))
self.sourceAccountList.append(SourceAccountModel(id:2,accountNumber: "Option 2", accountTitle: "", priaryAccount: ""))
}
}
Always select first value. What is the wrong with my code?
selectedOption is a String, but your ForEach iterates over Range<Int>.
You can fix this by changing selectedOption to Int, e.g.
#State private var selectedOption = 0
You might find it easier to store the actual object in selectedOption: SourceAccountModel, iterate over the sourceAccountList, and tag each row:
struct SourceAccountModel: Identifiable, Hashable {
let id: Int
let accountNumber: String
init(id: Int, accountNumber: String) {
self.id = id
self.accountNumber = accountNumber
}
}
struct ContentView: View {
init() {
let sourceAccountList = [SourceAccountModel(id: 1, accountNumber: "Option 1"),
SourceAccountModel(id: 2, accountNumber: "Option 2")]
_sourceAccountList = State(wrappedValue: sourceAccountList)
_selectedOption = State(wrappedValue: sourceAccountList[0])
}
#State private var selectedOption: SourceAccountModel
#State private var sourceAccountList = [SourceAccountModel]()
var body: some View {
VStack {
Picker("Select", selection: $selectedOption) {
ForEach(sourceAccountList) { model in
Text(model.accountNumber).tag(model)
}
}
}
}
}
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'm not sure if I created my custom TextField properly, because I am unable to observe the value changes to an #Binded text. Running the following code, you may observe that print(text) is not executed when you manually enter text into the text field.
import SwiftUI
#main
struct TestOutWeirdTextFieldApp: App {
#State var text: String = "" {
didSet {
print(text)
}
}
var body: some Scene {
WindowGroup {
StandardTextField(placeholderText: "Enter text", defaultText: $text)
}
}
}
struct StandardTextField: View {
#State var placeholderText: String {
didSet {
print(#line)
print(placeholderText)
}
}
#Binding var defaultText: String {
didSet {
print(#line)
print(defaultText)
}
}
#State var systemImage: String?
#State var underlineColor: Color = .accentColor
#State var edges: Edge.Set = .all
#State var length: CGFloat? = nil
#State var secure: Bool = false
var body: some View {
HStack {
if secure {
SecureField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
} else {
TextField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
}
if let systemImage = systemImage {
Image(systemName: systemImage)
.foregroundColor(.white)
}
}
.overlay(
Rectangle()
.frame(height: 2)
.padding(.top, 35)
)
.foregroundColor(underlineColor)
.padding(edges, length)
}
}
struct StandardTextView_Previews: PreviewProvider {
static var previews: some View {
StandardTextField(placeholderText: "Placement text", defaultText: .constant("")).previewLayout(.sizeThatFits)
}
}
Instead of didSet you need to use .onChange(of: modifier, like
HStack {
// ... your code here
}
.onChange(of: defaultText) { print($0) } // << this !!
.overlay(
I have a view that includes a ForEach, and I have a button that adds more items to the list. In each instance, in the loop, I have a TextField that is pre-filled with an autogenerated counter name. But when I add a new instance, all the previously added items change the name to the same:
What I would like is to have the counter names be Counter 1 for the first, the second Counter 2, third Counter 3, etc.
Here's my code:
import SwiftUI
struct Counter: Identifiable, Equatable {
var id: UUID = UUID()
var name: String = ""
var rows: Int = 0
var repeats: Int?
var rowsPerRepeat: Int?
var countRepeats: Bool = false
}
struct Project: Identifiable, Equatable {
var id:UUID = UUID()
var name:String = ""
var counters: [Counter] = [Counter]()
}
class AddEditCounterViewModel : ObservableObject {
#Published var counter : Counter
#Published var project: Project
init(counter: Counter, project: Project) {
self.project = project
self.counter = counter
if self.counter.name.isEmpty {
self.counter.name = counterNameGenerator()
}
}
func counterNameGenerator() -> String {
let count = project.counters.count
return String.localizedStringWithFormat(NSLocalizedString("Counter %d", comment: "Counter name"), count)
}
func countRepeats(countRepeats : Bool) {
if countRepeats {
counter.countRepeats = true
if counter.rowsPerRepeat == nil {
counter.rowsPerRepeat = 2
}
} else {
counter.countRepeats = false
counter.rowsPerRepeat = nil
}
}
}
struct AddEditCounterView: View {
#ObservedObject var viewModel : AddEditCounterViewModel
#State var countRepeats = false
#State var hiddenHeight : CGFloat = 0.0
#State var opacity = 0.0
init(viewModel: AddEditCounterViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack(spacing: 20) {
VStack (alignment: .leading) {
Text("Counter Name")
.multilineTextAlignment(.leading)
TextField("", text: $viewModel.counter.name)
}
HStack {
Text("Start at")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rows, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}.frame(maxWidth: .infinity, alignment: .leading)
Toggle(isOn: $countRepeats) {
Text("Count sets (repeats)")
}.onChange(of: countRepeats, perform: { value in
viewModel.countRepeats(countRepeats: value)
hiddenHeight = value ? 30.0 : 0
opacity = value ? 1 : 0
})
HStack {
Text("How many rows per set (repeat)")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rowsPerRepeat, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}
.opacity(opacity)
.frame(maxWidth: .infinity, maxHeight: $hiddenHeight.wrappedValue, alignment: .leading)
.animation(.easeIn)
}
}
}
class AddEditProjectViewModel: ObservableObject {
#Published var project : Project
init(project: Project) {
self.project = project
if self.project.counters.count < 1 {
addNewCounter()
}
}
func addNewCounter() {
project.counters.append(Counter())
}
}
struct AddEditProjectView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
var body: some View {
VStack {
ForEach(viewModel.project.counters) { counter in
AddEditCounterView(viewModel: AddEditCounterViewModel(counter: viewModel.project.counters[0], project: viewModel.project))
}
Button( action: {
viewModel.addNewCounter()
}){
Text("Add Counter")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
AddEditProjectView(viewModel: AddEditProjectViewModel(project: Project()))
}
}
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 !!
}
})