I have 2 #State variables:
#State var test1:String
#State var test2:String
I can do this:
_test1 = State(initialValue: "test1")
_test2 = State(initialValue: "test2")
and this:
_test1 = State(initialValue: "test1")
_test2 = _test1
but not this:
_test1 = State(initialValue: "test1")
_test2 = State(initialValue: test1 + " and test2")
with the error: Variable 'self.test2' used before being initialized
What's the reasoning behind this? Is there an appropriate way to use the value in test1 as part of test2?
Here is tested solution. Xcode 11.4 / iOS 13.4
struct TestStatesInitialization: View {
#State var test1:String
#State var test2:String
init() {
_test1 = State(initialValue: "test1")
_test2 = State(initialValue: _test1.wrappedValue + " and test2")
}
var body: some View {
VStack {
Text("1: \(test1)")
Text("2: \(test2)")
}
}
}
The swift compiler prevents you from using any instance property before everything is initialised. A good workaround is to create a temporary variable to hold the value of test1, like so
let tempTest1 = State(initialValue: "test1")
_test1 = tempTest1
_test2 = State(initialValue: tempTest1.wrappedValue + " and test2")
Related
I am trying to use fileExport logic to copy history data from CoreData to a CSV file. Since the data is coming from CoreData I need to use #FetchRequest and it is my understanding that #FetchRequest may only be used in a view.
I'm getting a number of errors related to misusing a view and transferring the data to fileExporter. It seems like I'm misusing a view to transfer data. Are there other features of CoreData that can be used to retrieve data outside of a view?
I have several similar structures that create CSV files without using coreData working. Therefore I believe my structures CreateHistoryTable and MessageDocument are working correctly. So I need help getting my data from CoreData to fileExporter.
struct CreateHistoryTable: View {
#Environment(\.managedObjectContext) var viewContext
#State private var showingExporter: Bool = false
#State private var document: MessageDocument?
var body: some View {
VStack {
Button ( action: {
self.showingExporter = true
document = CreateHistoryCSV() <-- need help here retrieving document to export
}) {
HStack (alignment: .firstTextBaseline) {
Text("Export History Entries")
.fontWeight(.bold)
.font(.title3)
Image(systemName: "square.and.arrow.up")
}
}
}.fileExporter(
isPresented: $showingExporter,
document: document,
contentType: .plainText,
defaultFilename: "TripSenseHistory.csv"
) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
.navigationBarTitle(Text("Export History Entries"), displayMode: .inline)
}
}
Retrieve data from CoreData and copy to single text string
struct CreateHistoryCSV: View {
#Binding MessageDocument
var csvData: String = ""
var title = ",Trip Sense History Entries,\n"
var subtitle = "Date,Category,Payment Type, Amount\n"
var messageRow: String = ""
var sHisCatName: String = ""
var sHisDsc: String = ""
var sHisPayType: String = ""
var sHisMoney: String = ""
var dHisMoney: Double = 0.0
var sHisLoc: String = ""
var payType = ["Cash", "Debit", "Credit"]
var code: String = ""
var messageRow = ""
// fetch core data
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
var csvData = title + subtitle
for item in 0..<currTrans.count {
let messageRow = createHistoryRow(item: item)
csvData += messageRow
}
print(csvData)
//return MessageDocument(message: csvData)
}
func createHistoryRow(item: Int) ->(String) {
// format expense date and time
let dHisDate = currTrans[item].entryDT ?? Date()
let sHisDate = dHisDate.formatted(.dateTime.year().day().month(.wide).hour().minute())
// get history category
let sHisCatName = currTrans[item].entryCatName ?? "cat name"
// get payment type
let sHisPayType = payType[Int(currTrans[item].entryPT)]
// get description
let sHisDsc = currTrans[item].entryDsc ?? "Unk"
// format transaction amount
let code = currTrans[item].entryCode ?? "Unk" // 3 digit country code for this transaction
let dHisMoney = currTrans[item].entryMoney
let sHisMoney = dHisMoney.formatted(.currency(code: sym))
// get location
let sHisLoc = currTrans[item].entryAddr ?? "Unk"
messageRow = "\"\(sHisDate)\"" + "," + sHisCatName + "," + sHisPayType + "," + "\"\(sHisDsc)\"" + "," + "\"\(sHisMoney)\"" + "," + "\"\(sHisLoc)\"" + "\n"
return messageRow
}
}
This code is part of the Swiftui file export logic
struct MessageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var message: String = ""
init(message: String) {
self.message = message
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
message = string
}
// this will be called when the system wants to write our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: message.data(using: .utf8)!)
}
}
With further research I realized that I could place the #FetchRequest in CreateHistoryTable along with the fileExporter view logic. That allowed me to change CreateHistoryCSV to a function of CreateHistoryTable. No changes were made to createHistoryRow
// copy history entrys to csv file
struct CreateHistoryTable: View {
#EnvironmentObject var base: BaseCurrency
#EnvironmentObject var bank: BankWithdrawal
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
#State private var showingExporter: Bool = false
#State private var document: MessageDocument?
var title = ",Trip Sense History Entries,\n"
var subtitle = "Date,Category,Payment Type, Amount\n"
var messageRow: String = ""
var sHisCatName: String = ""
var sHisDsc: String = ""
var sHisPayType: String = ""
var sHisMoney: String = ""
var dHisMoney: Double = 0.0
var sHisLoc: String = ""
var payType = ["Cash", "Debit", "Credit"]
var sym: String = ""
// fetch core data
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
VStack {
Button ( action: {
self.showingExporter = true
let dates = userData.formatCsvDate(startDate: startDate, endDate: endDate)
document = CreateHistoryCSV(dates: dates)
}) {
HStack (alignment: .firstTextBaseline) {
Text("Export History Entries")
.fontWeight(.bold)
.font(.title3)
Image(systemName: "square.and.arrow.up")
}
}
}.fileExporter(
isPresented: $showingExporter,
document: document,
contentType: .plainText,
defaultFilename: "TripSenseHistory.csv"
) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
.navigationBarTitle(Text("Export History Entries"), displayMode: .inline)
}
func CreateHistoryCSV() -> (MessageDocument) {
var csvData = title + subtitle
for item in 0..<currTrans.count {
let messageRow = createHistoryRow(item: item)
csvData += messageRow
}
print(csvData)
return MessageDocument(message: csvData)
}
I'm trying to figure out how to link the #Binding passed into a custom View to an #Published from that view's model. Essentially I'm trying to create a reusable integer only TextField. I'm using the below code, which works to set the integer value into the text field, but what I can't figure out is how to update the binding when the text changes.
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
}
}
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
}
}
If I understand you correctly
.onChange (of: fieldValue.value) { vl in
value = vl
}
this modifier updates the binding value to $fieldValue.value
Here is modified code to demo a possible approach (tested with Xcode 12.1 / iOS 14.1):
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
if let number = Int(value) {
numberValue = number
}
}
}
#Published var numberValue: Int = 0
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
.onChange(of: fieldValue.numberValue) {
if $0 != self.value {
self.value = $0
}
}
}
}
I'm new to IOS and SwiftUI coding. I googled a lot but could not find a solution, how to pass a computed variable to the next view.
Here snippets of what I have:
import SwiftUI
struct ContentView: View {
#State private var isShowingResultView = false
#State private var netRate = "0"
#State var daysMonth = "0"
#State var hoursWeek: String = "0"
#State var daysOnsite: String = "0"
#State var ticketCost: String = "0"
#State var hotelCost: String = "0"
#State var otherCost: String = "0"
//#State var travellCostResult: Double = 0.00
var travellCostPerHour: Double{
get {
let daysMonthNbr = Int(daysMonth) ?? 0
let hoursWeekNbr = Int(hoursWeek) ?? 0
let daysOnsiteNbr = Int(daysOnsite) ?? 0
let ticketCostNbr = Double(ticketCost) ?? 0
let hotelCostNbr = Double(hotelCost) ?? 0
let otherCostNbr = Double(otherCost) ?? 0
let travellCostPerWeek = (ticketCostNbr + (Double((daysOnsiteNbr-1))*hotelCostNbr)+otherCostNbr)
let travellCostPerHour: Double = Double(travellCostPerWeek) / Double(hoursWeekNbr)
return travellCostPerHour.isNaN ? 0 : travellCostPerHour
}
}
.
.
.
var body: some View {
HStack {
NavigationLink("Calculate", destination: ResultView(netRate: self.$netRate, travellCostPerHour: travellCostPerHour), isActive: $isShowingResultView).navigationBarTitle("Result").buttonStyle(GradientButtonStyle())
.adaptToKeyboard()
}
struct ResultView: View {
#Binding var netRate: String
#Binding var travellCostPerHour: Double
.
.
.
struct ResultView_Previews: PreviewProvider {
#State static var netRate: String = ""
#State static var travellCostPerHour: Double = 0.00
static var previews: some View {
ResultView(netRate: $netRate, travellCostPerHour: $travellCostPerHour )
}
}
I get this error msg in the navigationLink for travellCostPerHour: Cannot convert value of type 'Double' to expected argument type 'Binding'
Can one put me on the right path here please?
If it is computed property then binding is not needed, pass it as-is
struct ResultView: View {
#Binding var netRate: String
var travellCostPerHour: Double // << just regular
// .. other code
There are 2 views (structs).
First view has a #state update:
struct SettingsView: View {
#State private var lang = 0
#State private var languages = ["English", "Spanish"]
#State private var text1 = "Close"
#State private var text2 = "Settings"
#State var show = false
#State var update = false
var body: some View {
ZStack{
Button(action: {
self.show.toggle()
}) {
Text("Choose language")
}
if self.$show.wrappedValue {
GeometryReader {proxy in
ChooseLanguage(show: self.$show, update: self.$update)
}.background(Color.black.opacity(0.65)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation{
self.show.toggle()
}
})
}
}.onAppear{
switch UserDefaults.standard.string(forKey: "languageSettings"){
case "en": self.lang = 0
case "es": self.lang = 1
default: return
}
self.updateLanguage()
}
func updateLanguage(){
if self.lang == 1 {
self.text1 = "Cerrar"
self.text2 = "Configuración"
self.languages = ["Inglés", "Español"]
} else {
self.text1 = "Close"
self.text2 = "Settings"
self.languages = ["English", "Spanish"]
}
}
}
}
The second view has #Binding update:
import SwiftUI
struct ChooseLanguage : View {
var languages = UserDefaults.standard.stringArray(forKey: "langlist")
#Binding var show: Bool
#Binding var update: Bool
var body: some View {
ZStack {
VStack {
Button(action: {
UserDefaults.standard.set("en", forKey: "languageSettings")
UserDefaults.standard.set(["English", "Spanish"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![0])
}
Button(action: {
UserDefaults.standard.set("es", forKey: "languageSettings")
UserDefaults.standard.set(["Inglés", "Español"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![1])
}
}
}
}
}
When I call the func updateLanguage() before the .onAppear only errors appear.
Why I can update the values with function from the onAppear and I can't do this from the wrappedValue?
if self.$update.wrappedValue {
self.updateLanguage()
self.update.toggle()
}
This part doesn't work if to place before }.onAppear
As far as I see, you can make it so much easier with using init() method for your view.
There you can declare and initialize all your #State variables with the correct value (depending on your UserDefaults)
Just to show you an example:
struct SetView: View {
#State private var lang : Int
#State private var languages : [String]
#State private var text1 : String
#State private var text2 : String
#State var show = false
#State var update = false
init()
{
var state : Int = 0
switch UserDefaults.standard.string(forKey: "languageSettings")
{
case "en": state = 0
case "es": state = 1
default:
//Default value here
state = 0
}
if state == 1 {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Cerrar")
self._text2 = State(initialValue: "Configuración")
self._languages = State(initialValue: ["Inglés", "Español"])
} else {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Close")
self._text2 = State(initialValue: "Settings")
self._languages = State(initialValue: ["English", "Spanish"])
}
}
You won't need onAppear method at all, when you initialize your State variables with the correct value from the beginning.
I haven't tested it yet. Code is just out of my mind above.
Instead of calling the function after the #State upload value has been changed easier to send bindings for each text to the popup view.
GeometryReader {proxy in
ChooseLanguage(show: self.$show,
text1: self.$text1,
text2: self.$text2,
languages: self.$languages)
}
I would like to initialise the value of a #State var in SwiftUI through the init() method of a Struct, so it can take the proper text from a prepared dictionary for manipulation purposes in a TextField.
The source code looks like this:
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField($fullText)
}
}
Unfortunately the execution fails with the error Thread 1: Fatal error: Accessing State<String> outside View.body
How can I resolve the situation? Thank you very much in advance!
SwiftUI doesn't allow you to change #State in the initializer but you can initialize it.
Remove the default value and use _fullText to set #State directly instead of going through the property wrapper accessor.
#State var fullText: String // No default value of ""
init(letter: String) {
_fullText = State(initialValue: list[letter]!)
}
I would try to initialise it in onAppear.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
var body: some View {
TextField($fullText)
.onAppear {
self.fullText = list[letter]!
}
}
}
Or, even better, use a model object (a BindableObject linked to your view) and do all the initialisation and business logic there. Your view will update to reflect the changes automatically.
Update: BindableObject is now called ObservableObject.
The top answer is incorrect. One should never use State(initialValue:) or State(wrappedValue:) to initialize state in a View's init. In fact, State should only be initialized inline, like so:
#State private var fullText: String = "The value"
If that's not feasible, use #Binding, #ObservedObject, a combination between #Binding and #State or even a custom DynamicProperty
In your specific case, #Bindable + #State + onAppear + onChange should do the trick.
More about this and in general how DynamicPropertys work, here.
It's not an issue nowadays to set a default value of the #State variables inside the init method. But you MUST just get rid of the default value which you gave to the state and it will work as desired:
,,,
#State var fullText: String // <- No default value here
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField("", text: $fullText)
}
}
Depending on the case, you can initialize the State in different ways:
// With default value
#State var fullText: String = "XXX"
// Not optional value and without default value
#State var fullText: String
init(x: String) {
fullText = x
}
// Optional value and without default value
#State var fullText: String
init(x: String) {
_fullText = State(initialValue: x)
}
The answer of Bogdan Farca is right for this case but we can't say this is the solution for the asked question because I found there is the issue with the Textfield in the asked question. Still we can use the init for the same code So look into the below code it shows the exact solution for asked question.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
VStack {
Text("\(self.fullText)")
TextField("Enter some text", text: $fullText)
}
}
}
And use this by simply calling inside your view
struct ContentView: View {
var body: some View {
StateFromOutside(letter: "a")
}
}
You can create a view model and initiate the same as well :
class LetterViewModel: ObservableObject {
var fullText: String
let listTemp = [
"a": "Letter A",
"b": "Letter B",
// ...
]
init(initialLetter: String) {
fullText = listTemp[initialLetter] ?? ""
}
}
struct LetterView: View {
#State var viewmodel: LetterViewModel
var body: some View {
TextField("Enter text", text: $viewmodel.fullText)
}
}
And then call the view like this:
struct ContentView: View {
var body: some View {
LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
}
}
By this you would also not have to call the State instantiate method.
See the .id(count) in the example come below.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var count = 0
var body: some View {
Button("Tap me") {
self.count += 1
print(count)
}
Spacer()
testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
}
}
struct testView: View {
var count2: Int
#State private var region: MKCoordinateRegion
init(count: Int) {
count2 = 2*count
print("in testView: \(count)")
let lon = -0.1246402 + Double(count) / 100.0
let lat = 51.50007773 + Double(count) / 100.0
let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
_region = State(initialValue: myRegion)
}
var body: some View {
Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
Text("\(count2)")
}
}