I couldn't get my the below struct to initialise until I moved it into the body of the View. Can someone explain that to me please? Thanks!
This didn't work:
struct ContentView: View {
var sampleText = "This is sample text"
var booksArray = ["Book 1", "Book 2", "Book 3"]
let aBook = Book(passage: sampleText, books: booksArray)
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack {
Text(aBook.passage)
.frame(width: 350, height: .infinity, alignment: .center)
.kerning(1)
}
})
}
}
But this did work:
struct ContentView: View {
var sampleText = "This is sample text"
var booksArray = ["Book 1", "Book 2", "Book 3"]
var body: some View {
let aBook = Book(passage: sampleText, books: booksArray)
ScrollView(.vertical, showsIndicators: false, content: {
VStack {
Text(aBook.passage)
.frame(width: 350, height: .infinity, alignment: .center)
.kerning(1)
}
})
}
}
The error you got from this code reads:
cannot use instance member 'sampleText' within property initializer; property initializers run before 'self' is available
which in plain English means that when aBook is being assigned, self, which is the ContentView is not yet available, so you cannot use sampleText and booksArray to initialize aBook (because it requires all the properties to be initialized - to hold a value).
You might think, ok then I will use lazy to initialize aBook, which means that the assignment of the property will happen after ContentView has finished the initialization - at first access.
lazy var aBook = Book(passage: sampleText, books: booksArray)
but then a new error appears:
Text(aBook.passage) <= cannot use mutating getter on immutable value: 'self' is immutable
wait, what? But I'm not trying to mutate it you might say.
The reality though is that you do mutate ContentView by accessing aBook. Let's illustrate this:
sampleText is assigned a value
booksArray is assigned a value
aBook is not assigned since is lazy. Waits for access in order to be assigned a value.
ContentView is initalized
You access aBook in the body
aBook tries to assign a value, but because ContentView is a struct (value type - immutable) it needs to make a copy of the whole thing. Which is why you get the error
So, long story short you got a few options:
let aBook = Book(passage: sampleText, books: booksArray) in the body (as you already mention)
A computed property:
var aBook: Book { Book(passage: sampleText, books: booksArray) }
or a custom initializer:
init(sampleText: String, booksArray: [String]) {
self.sampleText = sampleText
self.booksArray = booksArray
self.aBook = Book(passage: sampleText, books: booksArray)
}
or even:
let sampleText = "This is sample text"
let booksArray = ["Book 1", "Book 2", "Book 3"]
let aBook: Book
init() {
self.aBook = Book(passage: sampleText, books: booksArray)
}
I hope that this makes sense.
Related
The problem
TL;DR: A String I'm trying to bind to inside TextField is nested in an Optional type, therefore I cannot do that in a straightforward manner. I've tried various fixes listed below.
I'm a simple man and my use case is rather simple - I want to be able to use TextField to edit my object's name.
The difficulty arises due to the fact that the object might not exist.
The code
Stripping the code bare, the code looks like this.
Please note that that the example View does not take Optional into account
model
struct Foo {
var name: String
}
extension Foo {
var sampleData: [Foo] = [
Foo(name: "Bar")
]
}
view
again, in the perfect world without Optionals it would look like this
struct Ashwagandha: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
var body: some View {
TextField("", text: $ashwagandhaVM.currentFoo.name)
}
}
view model
I'm purposely not unwrapping the optional, making the currentFoo: Foo?
class AshwagandhaVM: ObservableObject {
#Published var currentFoo: Foo?
init() {
self.currentFoo = Foo.sampleData.first
}
}
The trial and error
Below are the futile undertakings to make the TextField and Foo.name friends, with associated errors.
Optional chaining
The 'Xcode fix' way
TextField("", text: $ashwagandhaVM.currentFoo?.name)
gets into the cycle of fixes on adding/removing "?"/"!"
The desperate way
TextField("Change chatBot's name", text: $(ashwagandhaVM.currentFoo!.name) "'$' is not an identifier; use backticks to escape it"
Forced unwrapping
The dumb way
TextField("", text: $ashwagandhaVM.currentFoo!.name)
"Cannot force unwrap value of non-optional type 'Binding<Foo?>'"
The smarter way
if let asparagus = ashwagandhaVM.currentFoo.name {
TextField("", text: $asparagus.name)
}
"Cannot find $asparagus in scope"
Workarounds
My new favorite quote's way
No luck, as the String is nested inside an Optional; I just don't think there should be so much hassle with editing a String.
The rationale behind it all
i.e. why this question might be irrelevant
I'm re-learning about the usage of MVVM, especially how to work with nested data types. I want to check how far I can get without writing an extra CRUD layer for every property in every ViewModel in my app. If you know any better way to achieve this, hit me up.
Folks in the question comments are giving good advice. Don't do this: change your view model to provide a non-optional property to bind instead.
But... maybe you're stuck with an optional property, and for some reason you just need to bind to it. In that case, you can create a Binding and unwrap by hand:
class MyModel: ObservableObject {
#Published var name: String? = nil
var nameBinding: Binding<String> {
Binding {
self.name ?? "some default value"
} set: {
self.name = $0
}
}
}
struct AnOptionalBindingView: View {
#StateObject var model = MyModel()
var body: some View {
TextField("Name", text: model.nameBinding)
}
}
That will let you bind to the text field. If the backing property is nil it will supply a default value. If the backing property changes, the view will re-render (as long as it's a #Published property of your #StateObject or #ObservedObject).
I think you should change approach, the control of saving should remain inside the model, in the view you should catch just the new name and intercept the save button coming from the user:
class AshwagandhaVM: ObservableObject {
#Published var currentFoo: Foo?
init() {
self.currentFoo = Foo.sampleData.first
}
func saveCurrentName(_ name: String) {
if currentFoo == nil {
Foo.sampleData.append(Foo(name: name))
self.currentFoo = Foo.sampleData.first(where: {$0.name == name})
}
else {
self.currentFoo?.name = name
}
}
}
struct ContentView: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
#State private var textInput = ""
#State private var showingConfirmation = false
var body: some View {
VStack {
TextField("", text: $textInput)
.padding()
.textFieldStyle(.roundedBorder)
Button("save") {
showingConfirmation = true
}
.padding()
.buttonStyle(.bordered)
.controlSize(.large)
.tint(.green)
.confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
Button("Yes") {
confirmAndSave()
}
Button("No", role: .cancel) { }
}
//just to check
if let name = ashwagandhaVM.currentFoo?.name {
Text("in model: \(name)")
.font(.largeTitle)
}
}
.onAppear() {
textInput = ashwagandhaVM.currentFoo?.name ?? "default"
}
}
func confirmAndSave() {
ashwagandhaVM.saveCurrentName(textInput)
}
}
UPDATE
do it with whole struct
struct ContentView: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
#State private var modelInput = Foo(name: "input")
#State private var showingConfirmation = false
var body: some View {
VStack {
TextField("", text: $modelInput.name)
.padding()
.textFieldStyle(.roundedBorder)
Button("save") {
showingConfirmation = true
}
.padding()
.buttonStyle(.bordered)
.controlSize(.large)
.tint(.green)
.confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
Button("Yes") {
confirmAndSave()
}
Button("No", role: .cancel) { }
}
//just to check
if let name = ashwagandhaVM.currentFoo?.name {
Text("in model: \(name)")
.font(.largeTitle)
}
}
.onAppear() {
modelInput = ashwagandhaVM.currentFoo ?? Foo(name: "input")
}
}
func confirmAndSave() {
ashwagandhaVM.saveCurrentName(modelInput.name)
}
}
There is a handy Binding constructor that converts an optional binding to non-optional, use as follows:
struct ContentView: View {
#StateObject var store = Store()
var body: some View {
if let nonOptionalStructBinding = Binding($store.optionalStruct) {
TextField("Name", text: nonOptionalStructBinding.name)
}
else {
Text("optionalStruct is nil")
}
}
}
Also, MVVM in SwiftUI is a bad idea because the View data struct is better than a view model object.
I've written a couple generic optional Binding helpers to address cases like this. See this thread.
It lets you do if let unwrappedBinding = $optional.withUnwrappedValue { or TestView(optional: $optional.defaulting(to: someNonOptional).
#State var myDict: [String: Double] = [:]
var body: some View {
HStack(alignment: .top) {
Button(action: {
self.myDict[self.tag ?? ""] = amountValue
})
}
}
I have two user, when i select 1st user I add value to myDict, and when I select 2nd user I want to add another value to it (means myDict will have 2 object), but when i select 2nd user #state variable is refresh and have only one value of 2nd user. (means myDict have 1 object)
is there any way so that I can have both the value in dict not only one?
actually, this can happen that for both users self.tag is nil and ("") is used as a key for the dictionary
self.myDict[self.tag ?? ""] = amountValue
and if this happens you may want to make sure that self.tag is not nil
I have created a test environment just like your case but I have
import SwiftUI
struct swiftUIDoubt1: View {
#State var myDict: [String: Double] = [:]
var body: some View {
VStack{
Button(action: {
myDict["Hello, World!"] = 9.99999
print(myDict)
}, label: {
Text("\(myDict["Hello, World!"] ?? 0)")
.padding()
})
Button(action: {
myDict["bye, World!"] = 1.11111
print(myDict)
}, label: {
Text("\(myDict["bye, World!"] ?? 0)")
.padding()
})
}
.onAppear(perform: {
print(myDict)
})
}
}
now as you can see when my screen will appear my dictionary should be empty and an empty dictionary must be printed, as you can see in the image
console log when the app is first opened
when I click first button single item will be added and you can see in console log
and when I will click on the second button you can see I have two different items in my dictionary
let success = "two different items has been added to dictionary"
as #state variable to managed by swift UI and will not change with ui update
Small example : The add in the subview update the dictionnaire for the desired user in parent view
import SwiftUI
struct UpdateUsersDict: View {
#State var myDict: [String: Double] = [:]
#State var amount: Double = 100
var body: some View {
VStack {
HStack {
OneUserView(myDict: $myDict, amount: amount, tag: "user 1")
OneUserView(myDict: $myDict, amount: amount, tag: "user 2")
OneUserView(myDict: $myDict, amount: amount, tag: "user 3")
}
HStack {
Text("\(myDict["user 1"] ?? -1)")
Text("\(myDict["user 2"] ?? -1)")
Text("\(myDict["user 3"] ?? -1)")
}
}
}
}
struct OneUserView: View {
#Binding var myDict: [String: Double]
#State var amount: Double
var tag: String
var body: some View {
HStack(alignment: .top) {
Button(tag, action: {
self.myDict[self.tag] = amount
})
}
}
}
struct UpdateUserDict_Previews: PreviewProvider {
static var previews: some View {
UpdateUsersDict()
}
}
I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄
Simple question to ask but I probably forgot something in the code.
Let's explain better using an image:
I need basically to update "COUNT" and "PRICE" while selecting/deselecting items.
I have a code structure like this:
Model:
class ServiceSelectorModel: Identifiable, ObservableObject {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
#Published var selected: Bool = false
}
ViewModel:
class ServiceSelectorViewModel: ObservableObject {
#Published var services = [ServiceSelectorModel]()
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
ToggleView
struct ServiceToggleView: View {
#ObservedObject var model: ServiceSelectorModel
var body: some View {
VStack(alignment: .center) {
HStack {
Text(model.serviceName)
Toggle(isOn: $model.selected) {
Text(String(format: "€ +%.2f", model.price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.background(model.selected ? Color.yellow : Color.clear)
}
}
}
ServiceSelectorView
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach (serviceSelectorVM.services, id: \.id) { service in
ServiceToggleView(model: service)
}
}
let price = serviceSelectorVM.services.filter{ $0.selected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.selected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}
In this code I'm able to update the selected status of the model but the view model that contains all the models and should refresh the PRICE not updates.
Seems that the model in the array doesn't change.
what have i forgotten?
Probably most simple is to make your model as value type, then changing its properties the view model, holding it, will be updated. And to update view to use binding to those values
struct ServiceSelectorModel: Identifiable {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
var selected: Bool = false
}
struct ServiceToggleView: View {
#Binding var model: ServiceSelectorModel
...
}
...
ForEach (serviceSelectorVM.services.indices, id: \.self) { i in
ServiceToggleView(model: $serviceSelectorVM.services[i])
}
Note: written inline, not tested, some typo might needed to be fixed
You have created two different Single source of truth if you notice carefully. That’s the reason parent is not updating, as it’s not linked to child.
One way is to create ServiceSelectorModel as a struct, and pass services array as #Binding in child view. Below is the working example.
ViewModel-:
struct ServiceSelectorModel {
var id = UUID().uuidString
var serviceName: String
var price: Double
var isSelected:Bool = false
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
}
class ServiceSelectorViewModel: ObservableObject,Identifiable {
#Published var services = [ServiceSelectorModel]()
var id = UUID().uuidString
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
Views-:
struct ServiceToggleView: View {
#Binding var model: [ServiceSelectorModel]
var body: some View {
VStack(alignment: .center) {
ForEach(0..<model.count) { index in
HStack{
Text(model[index].serviceName)
Toggle(isOn: $model[index].isSelected) {
Text(String(format: "€ +%.2f", model[index].price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}.background(model[index].isSelected ? Color.yellow : Color.clear)
}
}
}
}
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ServiceToggleView(model: $serviceSelectorVM.services)
}
let price = serviceSelectorVM.services.filter{ $0.isSelected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.isSelected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}
I’m building like a demo app of different examples, and I’d like the root view to be a List that can navigate to the different example views. Therefore, I tried creating a generic Example struct which can take different destinations Views, like this:
struct Example<Destination: View> {
let id: UUID
let title: String
let destination: Destination
init(title: String, destination: Destination) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: Example1View()),
Example(title: "Example 2", destination: Example2View())
]
var body: some View {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
Unfortunately, this results in an error because examples is a heterogeneous collection:
I totally understand why this is broken; I’m creating a heterogeneous array of examples because each Example struct has its own different, strongly typed destination. But I don’t know how to achieve what I want, which is an array that I can make a List out of which has a number of different allowed destinations.
I’ve run into this kind of thing in the past, and in the past I’ve gotten around it by wrapping my generic type and only exposing the exact properties I needed (e.g. if I had a generic type that had a title, I would make a wrapper struct and protocol that exposed only the title, and then made an array of that wrapper struct). But in this case NavigationLink needs to have the generic type itself, so there’s not a property I can just expose to it in a non-generic way.
You can use the type-erased wrapper AnyView. Instead of making Example generic, make the destination view inside of it be of type AnyView and wrap your views in AnyView when constructing an Example.
For example:
struct Example {
let id: UUID
let title: String
let destination: AnyView
init(title: String, destination: AnyView) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: AnyView(Example1View())),
Example(title: "Example 2", destination: AnyView(Example2View()))
]
var body: some View {
NavigationView {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
}