Is there any reason I should not use a class instance rather than using a struct instance and #Binding?
I am using two instances of a class Cat (sire and dam) to hold my data, and displaying them as a view.
ColourMenuView(cat: sire)
class Cat {
init(sex: Sex = .male) {
self.sex = sex
}
...
}
and
struct ColourMenuView: View {
var cat: Cat
#State var colour = CatColour.black
var body: some View {
HStack {
Text("Colour:")
.contextMenu {
ForEach(CatColour.all, id: \.self) {i in
Button(i.rawValue) { colour = i }
}
}
Text(colour.rawValue)
}
.onChange(of: colour) { value in
cat.colour = value
}
}
}
Are there any drawbacks to this method? Or should I make Cat a struct, and go
#Binding var cat
Related
I am a newbie to SwiftUI, and I have a simple question, how to update the #State content of a VIEW in another Class, a simulated Code is as follows
When I press the button, the fruits.price variable is not updated in the View. Is there something I'm missing, or is there something wrong I don't understand
class Fruits: ObservableObject {
#Published var price = 1
}
struct ContentView: View {
#ObservedObject var fruits = Fruits()
var extenalClass = ExtenalClass()
var body: some View {
VStack {
Text(String(fruits.price))
.padding()
Button("Add-Me") {
// fruits.price += 1
extenalClass.fChangePrice()
print (fruits.price)
}
}
}
}
class ExtenalClass: ObservableObject {
#ObservedObject var fruits = Fruits()
func fChangePrice() {
fruits.price = 888
print (fruits.price)
}
}
We don't use classes in SwiftUI for model types, we use structs. Any change to the struct is detected by SwiftUI as a change to the state, so body is called to get the new Views, e.g.
struct Fruit {
var price = 1
}
struct ContentView: View {
#State var fruit = Fruit()
var body: some View {
VStack {
Text(fruit.price, format: .currency)
.padding()
Button("Add-Me") {
fruit.price += 1
print (fruit.price)
}
}
}
}
Thanks jnpdx, Baglan for the guidance .... I just found out my answer (probably not the right way), which is to put #Published and "My Function" together in the same CLASS...that is, Function is placed in the CLASS containing #Published...or, another way of saying , put #Published in the CLASS that contains "My Function"
class Fruits: ObservableObject {
#Published var price = 1
// Move (ExtenalClass) Function to here
func fChangePrice() {
price = 888
print (price)
}
}
struct ContentView: View {
#ObservedObject var fruits = Fruits()
// var extenalClass = ExtenalClass()
var body: some View {
VStack {
Text(String(fruits.price))
.padding()
Button("Add-Me") {
// fruits.price += 1
fruits.fChangePrice() // Modified
print (fruits.price)
}
}
}
}
//class ExtenalClass: ObservableObject {
// #ObservedObject var fruits = Fruits()
// func fChangePrice() {
// fruits.price = 888
// print (fruits.price)
// }
//}
let's say you have a list of some objects - let's say cats. There is a common viewmodel that stores the data of the cats received from the server. Each element on the view is a small sector of data for one cat. Within this sector, the data can be changed by the user. How to correctly link the viewmodel of the list of cats and the viewmodel of one cat?
struct Cat : Identifiable {
let id = UUID()
var name: String
var breed: String
}
struct CatListView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
List(self.viewModel.items) { (item) in
CatView(viewModel: self.viewModel.createViewModel(item: item))
}
}
}
extension CatListView {
class ViewModel : ObservableObject {
#Published var items: [Cat]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
.init(data: item)
}
}
}
struct CatView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
VStack {
TextField("Name", text: self.$viewModel.data.name)
TextField("Breed", text: self.$viewModel.data.breed)
}
}
}
extension CatView {
class ViewModel : ObservableObject {
#Published var data: Cat
init(data: Cat) {
self._data = .init(initialValue: data)
}
}
}
How to properly transfer changes from CatView.ViewModel to CatListView.ViewModel?
Is Binding possible? But I think this is a variant of the connection between View and View.
Perhaps this option will be correct?
extension CatListView {
class ViewModel : ObservableObject {
#Published private(set) var items: [Cat]
private var cancelItems: [Int: AnyCancellable] = [:]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
let model: CatView.ViewModel = .init(data: item)
if let index = self.items.firstIndex(where: { $0.id == item.id }) {
self.cancelItems[index] = model.$data
.dropFirst()
.removeDuplicates()
.sink {
self.items[index] = $0
}
}
return model
}
}
}
You have to manage these items:
Model: single Cat Model
ViewModel: Cat Collection Model that stores all cats and fetch them
View 1 with cats List
View 2 (optional) A View with single cat, can be inside View 1 or separate, but for illustrate this we will separate the View 2.
struct Cat: Identifiable, Hashable {
let id = UUID()
var name: String
var colorDescription: String
}
class CatsModel: ObservableObject {
#Published var catsBag: [Cat]
init() {
// Fetch here the cats from your server
}
// CRUD cicle
func create(_ cat: Cat) { ... }
func update(_ cat: Cat) { ... }
func delete(_ cat: Cat) { ... }
}
struct CatsList: View {
#StateObject var catsModel = CatsModel()
var body: some View {
List {
ForEach(catsModel.catsBag.indices, id: \.self) { catIndice in
CatCell(cat: $catsModel.catsBag[catIndice])
}
}
}
}
struct CatCell: View {
#Binding var cat: Cat
var body: some View {
TextField("The cat name is: ", text: $cat.name)
.padding()
}
}
You will appreciate at this point that if you delete the last cat of that list the app crashes. This could be a SwiftUI error, you can find the solution here: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
Is there a way to delete all the list items in SwiftUI?
I'm using a ForEach() inside a List() and I want to have a clear all button to remove all the items from the list, is there a way to do it?
struct SwiftUIView: View {
#State var filters : [filter] = [filter(name: "new"), filter(name: "old"), filter(name: "some")]
#State var afterFilters : [someFilter] = []
var body: some View {
List{
ForEach(0..<self.filters.count, id:\.self){ i in
filterRepresent(string: self.$afterFilters[i].filter.name, isOn: self.$afterFilters[i].isOn)
}
}.onAppear {
for filter in self.filters {
self.afterFilters.append(someFilter(filter: filter))
}
}
}
}
struct filterRepresent : View {
#Binding var string : String
#Binding var isOn : Bool
var body : some View {
HStack{
Text(string)
Toggle("",isOn: $isOn)
}
}
}
struct filter {
var name : String
var isOn : Bool
init(name: String){
self.name = name
self.isOn = false
}
}
struct someFilter : Identifiable{
var id : Int
var filter : filter
var isOn : Bool
init(filter : filter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isOn
}
}
As you can see, in the example above, I'm using a #Binding to change the data I store based on the Toggle state, I want to have a button that deletes the entire list (in the real app the data to the list is uploaded from a server side into a temp array just like in the above) when I do it with .removeall() I get thrown with "out of index" error.
The button I use :
Button(action: {
self.afterFilters.removeAll()
}, label: {
Text("Clear all").font(Font.custom("Quicksand-Medium", size: 15))
})
The error I'm getting:
Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.13/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
You have to clean up model and view will be refreshed automatically.
Here is a simple demo:
struct DemoCleanUpList: View {
#State private var persons = ["Person 1", "Person 2", "Person 3"]
var body: some View {
VStack {
Button("CleanUp") { self.persons.removeAll() }
List {
ForEach(persons, id: \.self) { person in
Text(person)
}
}
}
}
}
I want a dynamic array of mutable strings to be presented by a mother view with a list of child views, each presenting one of the strings, editable. Also, the mother view will show a concatenation of the strings which will update whenever one of the strings are updated in the child views.
Can't use (1) ForEach(self.model.strings.indices) since set of indices may change and can't use (2) ForEach(self.model.strings) { string in since the sub views wants to edit the strings but string will be immutable.
The only way I have found to make this work is to make use of an #EnvironmentObject that is passed around along with the parameter. This is really clunky and borders on offensive.
However, I am new to swiftui and I am sure there a much better way to go about this, please let know!
Here's what I have right now:
import SwiftUI
struct SimpleModel : Identifiable { var id = UUID(); var name: String }
let simpleData: [SimpleModel] = [SimpleModel(name: "text0"), SimpleModel(name: "text1")]
final class UserData: ObservableObject { #Published var simple = simpleData }
struct SimpleRowView: View {
#EnvironmentObject private var userData: UserData
var simple: SimpleModel
var simpleIndex: Int { userData.simple.firstIndex(where: { $0.id == simple.id })! }
var body: some View {
TextField("title", text: self.$userData.simple[simpleIndex].name)
}
}
struct SimpleView: View {
#EnvironmentObject private var userData: UserData
var body: some View {
let summary_binding = Binding<String>(
get: {
var arr: String = ""
self.userData.simple.forEach { sim in arr += sim.name }
return arr;
},
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(userData.simple) { tmp in
SimpleRowView(simple: tmp).environmentObject(self.userData)
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
Where the EnironmentObject is created and passed as SimpleView().environmentObject(UserData()) from AppDelegate.
EDIT:
For reference, should someone find this, below is the full solution as suggested by #pawello2222, using ObservedObject instead of EnvironmentObject:
import SwiftUI
class SimpleModel : ObservableObject, Identifiable {
let id = UUID(); #Published var name: String
init(name: String) { self.name = name }
}
class SimpleArrayModel : ObservableObject, Identifiable {
let id = UUID(); #Published var simpleArray: [SimpleModel]
init(simpleArray: [SimpleModel]) { self.simpleArray = simpleArray }
}
let simpleArrayData: SimpleArrayModel = SimpleArrayModel(simpleArray: [SimpleModel(name: "text0"), SimpleModel(name: "text1")])
struct SimpleRowView: View {
#ObservedObject var simple: SimpleModel
var body: some View {
TextField("title", text: $simple.name)
}
}
struct SimpleView: View {
#ObservedObject var simpleArrayModel: SimpleArrayModel
var body: some View {
let summary_binding = Binding<String>(
get: { return self.simpleArrayModel.simpleArray.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(simpleArrayModel.simpleArray) { simple in
SimpleRowView(simple: simple).onReceive(simple.objectWillChange) {_ in self.simpleArrayModel.objectWillChange.send()}
}
Button(action: { self.simpleArrayModel.simpleArray.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
You don't actually need an #EnvironmentObject (it will be available globally for all views in your environment).
You may want to use #ObservedObject instead (or #StateObject if using SwiftUI 2.0):
...
return VStack {
TextField("summary", text: summary_binding)
ForEach(userData.simple, id:\.id) { tmp in
SimpleRowView(userData: self.userData, simple: tmp) // <- pass userData to child views
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text")) }) {
Text("Add text")
}
}
struct SimpleRowView: View {
#ObservedObject var userData: UserData
var simple: SimpleModel
...
}
Note that if your data is not constant you should use a dynamic ForEach loop (with an explicit id parameter):
ForEach(userData.simple, id:\.id) { ...
However, the best results you can achieve when you make your SimpleModel a class and ObservableObject. Here is a better solution how do do it properly:
SwiftUI update data for parent NavigationView
Also, you can simplify your summary_binding using reduce:
let summary_binding = Binding<String>(
get: { self.userData.simple.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
I am learning SwiftUI and having trouble with data flow.
I can't figure out how to create a "state"-variable to keep track of a list of published child items.
I need a variable that is true if at least one child item is selected. These items are mapped to SwiftUI toggle-switches, which works. (Making a computed property is not allowed)
Property wrapper cannot be applied to a computed property
class Main : ObservableObject {
#Published var items : [Item]
//This is what i want, but not allowed
#Published var selectedItemsExist : Bool {
get {
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
return exists
}
}
init(items: [Item]) {
self.items = items
}
}
class Item : Identifiable, ObservableObject {
var id = UUID()
var name : String!
#Published var selected : Bool = false
}
And then in my view be able to do something like
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
Any guidance appreciated!
Please find below the demo of approach how this could be done. Tested as worked with Xcode 11.2 / iOS 13.2
class Main : ObservableObject {
#Published var items : [Item] {
didSet { // << as item is value array will be updated on item changed
var exists = false
for item in items {
if item.selected {
exists = true
break
}
}
self.selectedItemsExist = exists
}
}
#Published var selectedItemsExist : Bool = false
init(items: [Item]) {
self.items = items
}
}
struct Item : Identifiable { // << value type, so changed on any property change
var id = UUID()
var name : String!
var selected : Bool = false
init(_ name: String) {
self.name = name
}
}
struct TestCalculablePublishing: View {
#ObservedObject var main = Main(items: [Item("1"), Item("2"), Item("3")])
var body: some View {
VStack {
Button("Test") {
self.main.items[1].selected.toggle() // << just for test purpose
}
if main.selectedItemsExist {
Text("This text is shown if at least on toggle is on")
}
}
}
}
struct TestCalculablePublishing_Previews: PreviewProvider {
static var previews: some View {
TestCalculablePublishing()
}
}