Cannot use mutating getter on immutable value: 'self' is immutable - swiftui

So I have been trying to fix this problem that has been already discussed here for a few times but I cannot seem to truly understand where the issue comes from and how to fix it in my application. Apologies if this one is obvious but I've picked up SwiftUI a week ago.
Basically what I am doing here is that I have a function called countStrokes() where I have an array of strings as an input. First of all I convert the array to an int array, then compute the sum of the array and return the sum as a String.
After that I declare a new lazy var called strokes and initialise it by calling the countStrokes() function.
All I want to do in the View is to print out the strokes value using the Text() module.
Any ideas of how to modify my existing code will be much appreciated.
import SwiftUI
struct Skore: View {
#State var skore: [String]
lazy var strokes: String = countStrokes(array: skore)
var body: some View {
Text(strokes)
}
}
func countStrokes(array: [String]) -> String {
let newArray = array.compactMap{Int($0)}
let total = newArray.reduce(0, +)
let totalString = String(total)
return totalString
}

The simplest is just use function inline. As soon as your view depends on skore state and countStrokes depends on it, once skore will be modified the corresponding Text will be recalculated and shows correct result.
struct Skore: View {
#State var skore: [String]
var body: some View {
Text(countStrokes(array: skore))
}
}

what you can do is this:
struct Skore: View {
#State var skore: [String]
#State var strokes: String = ""
var body: some View {
Text(strokes).onAppear(perform: loadData)
}
func loadData() {
self.strokes = countStrokes(array: skore)
}
}
func countStrokes(array: [String]) -> String {
let newArray = array.compactMap{Int($0)}
let total = newArray.reduce(0, +)
let totalString = String(total)
return totalString
}
struct ContentView: View {
#State var skore = ["1","2","3"]
var body: some View {
Skore(skore: skore)
}
}

Related

Trouble Passing Data From Form Entry

This is a module where the user enters transaction data and then it is saved to coreData. EntryView calls getFormData for entry of form data then calls the function saveButton() for saving the data to coreData.
Things have been working great until I recently added two additional parameters gotCountry and gotHome. These parameters are defined in EntryView. The data I want is found in getFormData but I don't want to make it available to other parts of the app until the save button is pressed (func saveButton()) hence I need to pass the data from getFormData to saveButton().
One of the two warnings is Initialization of immutable value 'gotCountry' was never used; consider replacing with assignment to '_' or removing it if I place let in front of the parameters gotCountry and gotHome in getFormData. These are variables so shouldn't have let in front of them. Removing 'let' results in the error Type '()' cannot conform to 'View'
The parameters entryDT and entryPT are coming from form input data while gotCountry and gotHome are coming from calculated data available at time of entry.
Note that I have stripped out some of the code to see the passing of data better.
struct EntryView: View {
#EnvironmentObject var ctTotals: CountryTotals
#State private var entryDT = Date()
#State private var entryPT: Int = 0
#State private var gotCountry: String = ""
#State private var gotHome: Double = 0.0
var body: some View {
VStack (alignment: .leading){
ShowTabTitle(g: g, title: "Enter Transaction")
getFormData(entryDT: $entryDT, entryPT: $entryPT, gotCountry: $gotCountry, gotHome: $gotHome)
Button {
self.saveButton() // button pressed
} label: {
Text ("Save")
}
}
}
func saveButton() {
// save entry to core data
let newEntry = CurrTrans(context: viewContext)
// entry id
newEntry.id = UUID()
// entry date
newEntry.entryDT = entryDT
// entry payment type
newEntry.entryPT = Int64(entryPT)
ctTotals.sendTotals(gotCountry: gotCountry, gotHome: gotHome)
do {
try viewContext.save()
} catch {
}
// reset parameters for next entry
self.entryDT = Date()
self.entryPT = 0
}
}
struct getFormData: View {
#Binding var entryDT: Date
#Binding var entryPT: Int
#Binding var gotCountry: String
#Binding var gotHome: Double
var body: some View {
// get entry date and time
DatePicker("", selection: $entryDT, in: ...Date())
// select payment type
Picker(selection: $entryPT, label: Text("")) {}
// copy data to totals by country
gotCountry = currencies.curItem[userData.entryCur].cunName
gotHome = totalValue
}
}
struct CtModel: Codable, Identifiable, Hashable {
var id = UUID()
var ctName: String
var ctHome: Double
}
class CountryTotals: ObservableObject {
#Published var ctItem: [CtModel] {
didSet {
if let encoded = try? JSONEncoder().encode(ctItem) {
UserDefaults.standard.set(encoded, forKey: StorageKeys.ctTotals.rawValue)
}
}
}
init() {
}
func sendTotals(gotCountry: String, gotHome: Double) -> () {
let item = CtModel(ctName: gotCountry, ctHome: gotHome)
ctItem.append(item)
}
}

Calling a new view in a function SwiftUI

I am currently using an api to grab the definitions for a specific word that the user has entered, and the api returns multiple definitions. I want the user to be able to choose what exact definition they want to pair a word with. Since I am interacting with an api, it is in a function and I cannot return anything out of it. I want to grab all the definitions and then show a new view where the user can pick the appropriate definition. How can I go about doing this? I've thought of making an ObservableObject that just has an array as a work around, but that seems a bit excessive. I am new to SwiftUI, so I am unsure whether or not this would be possible. However, I think it would not be because I am not trying to return a view anywhere or using any of the built in things that accepts views.
EDIT: I made SaveArray an ObservableObject and now my problem is that the object is not being updated by my getDef function call. Within the function it is but it is not editing the actual class or at least that is what it is looking like, because on my next view I have a foreach going through the array and nothing is displayed because it is empty. I am not sure whether that is because the sheet is being brought up before the getDef function is done executing.
struct AddWord: View {
#ObservedObject var book: Book
#ObservedObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
self.presentationMode.wrappedValue.dismiss()
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
func getDef(_ word: String, _ book: Book, _ definitionsArray: SaveArray) {
let request = NSMutableURLRequest(url: NSURL(string: "https://wordsapiv1.p.rapidapi.com/words/\(word)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
let dict = dictionary?["results"] as? [Any]
definitionsArray.currentArray = createArray((dict!))
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
func createArray(_ array: [Any]) -> [String] {
//Get all the definitions given from the api and put it into a string array so you can display it for user to select the correct definiton for their context
var definitions = [String]()
for object in array {
let dictDef = object as? [String: Any]
definitions.append(dictDef?["definition"] as! String)
}
return definitions
}
}
struct AddWord_Previews: PreviewProvider {
static var previews: some View {
AddWord(book: Book())
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
.navigationTitle("Choose")
}
}
}
struct PickDefinition_Previews: PreviewProvider {
static var previews: some View {
PickDefinition(definitions: SaveArray(), book: Book(), word: "")
}
}
If you can post more of your code, I can provide a fully working example (e.g. the sample JSON and the views/classes you have built). But for now, I am working with what you provided. I hope the below will help you see just how ObservableObject works.
#Published var dict = [String]() //If the api returns a list of strings, you can make this of type string - I do not have a sample of the JSON so I cannot be sure. If you can provide a sample of the JSON I can better define the way this should work.
var body: some View {
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
dict = dictionary?["results"] as? [Any] //note that here we are assigning the results of the api call to the Published variable, which our StateObject variable in ContentView is listening to!
var allDef = createArray((dict!))
//No longer need to pass this data forward (as you have below) since we are publishing the information!
//Pass the array to the new view where the user will select the one they want
PickDefinition(definitions: allDef, book: self.book, word: self.word)
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
}
struct ContentView: View {
//StateObject receives updates sent by the Published variable
#StateObject var dictArray = Api()
var body: some View {
NavigationView {
List {
ForEach(dictArray.dict.indices, id: \.self) { index in
Text(dictArray.dict[index])
}
}
}
}
}

How to initialize a #Binding Array

So I'm doing some refactoring and I ran across this line of code that I wanted to refactor:
struct MyView: View {
#State private var myArrayOfCustomObjects = [CustomObject]
let text: String
var body: some View {
Text(text)
}
}
Then when I wanted to refactor the view as so..
struct ExtractedView: View {
#Binding var customObjects: [CustomObject]
let text: String
init(customObjects: Binding<Array<CustomObject>>, text: String) {
self.customObjects = customObjects // Error: 'self' used before all stored properties are initialized
// Also tried _customObjects = customObjects
self.text = text
}
var body: some View {
Text(text)
}
}
This code is simplified of course but I fear I may be getting that error due to some complexity I'm not exposing in the example. Any feedback is welcome
What am I doing wrong??
( I also have an Environment instance (managedObjectContext) and a coreData class - which has some logic inside of the init that are being initialized too but didn't think it was relevant for this code example )
This will work! also try clean your build folder and build your project first.
struct ExtractedView: View {
#Binding var customObjects: [CustomObject]
let text: String
init(customObjects: Binding<Array<CustomObject>>, text: String) {
self._customObjects = customObjects
self.text = text
}
var body: some View {
Text(text)
}
}
struct CustomObject { }

SwiftUI Combine Why can't bind data in init?

I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.

SwiftUI #State var initialization issue

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)")
}
}