I have a Stepper component that corresponds to number of people (for now, I don't want to restrict what the stepper goes up to, but users will likely never enter more than 10). I want to dynamically generate a box that is essentially a Person object/class based on the current value of Stepper.
Current code
struct ContentView: View {
#State private var numPeople = 0
var body: some View {
VStack {
Text("Enter number of people below:")
Stepper("\(numPeople.formatted()) People", value: $numPeople)
}
}
Desired outcome if user clicks plus button until the number 3.
Should this be done using a for loop (or ForEach loop)? Should a component other than Stepper be used?
There are a few different ways of doing this but for growth sake you can consider a PersonModel and basing the stepper on the count of Array<PersonModel> using a computed variable that adds and removes from the array.
import SwiftUI
struct PersonModel: Identifiable{
let id: UUID = .init()
let name: String
}
class PersonVM: ObservableObject{
#Published var people: [PersonModel] = []
var personCount: Int{
get{
//Single source of truth.
people.count
}
set{
let diff: Int = people.count - newValue
if diff == 0{
//No change
} else if diff < 0 { //Add the difference
let new: [PersonModel] = (0..<abs(diff)).map{ n in
PersonModel(name: "Person \(people.count + n + 1)")
}
people.append(contentsOf: new)
} else if !people.isEmpty{ //Remove the difference if the array isn't empty
people.removeLast(diff)
}
}
}
}
#available(iOS 15.0, *)
struct PersonView: View {
#StateObject private var vm: PersonVM = .init()
var body: some View {
ScrollView {
Text("Enter number of people below:")
Stepper("\(vm.people.count.formatted()) People", value: $vm.personCount, in: 0...10, step: 1)
ForEach(vm.people) { person in
ZStack{
Color.blue
Text(person.name)
.foregroundColor(.white)
}.padding(2)
}
}
}
}
I'm using SwiftUI just started. I often confused how difference between Declarative programming and Imperative programming.
Console log show error:
ForEach<Range, Int, Text> count (2) != its initial count (1).
ForEach(_:content:) should only be used for constant data. Instead
conform data to Identifiable or use ForEach(_:id:content:) and
provide an explicit id!
Why?How to update list dynamically?How dose SwiftUI handle data source?
struct TestView: View {
#State var n = 1
var body: some View {
VStack {
Button.init("update") {
self.n += 1
}
AView.init(n: n)
}
}
}
struct AView: View {
let n: Int
var body: some View {
List {
ForEach(0..<n) { i in
Text.init(String(i))
}
}
}
}
ForEach(_:id:content:) differs from ForEach(_:content:) by the id parameter.
If you use a static ForEach the loop is generated once and not refreshed when you modify the n.
Try this:
struct AView: View {
let n: Int
var body: some View {
List {
ForEach(0 ..< n, id:\.self) { i in // <- add `id` parameter
Text(String(i))
}
}
}
}
You can take a look at Why does .self work for ForEach? for a more detailed explanation.
In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.
I have a SwiftUI view that takes in an EnvironmentObject called appModel. It then reads the value appModel.submodel.count in its body method. I expect this to bind my view to the property count on submodel so that it re-renders when the property updates, but this does not seem to happen.
Is this a bug? And if not, what is the idiomatic way to have views bind to nested properties of environment objects in SwiftUI?
Specifically, my model looks like this...
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: Submodel = Submodel()
}
And my view looks like this...
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.submodel.count += 1
}
}
}
When I run the app and click on the label, the count property does increase but the label does not update.
I can fix this by passing in appModel.submodel as a property to ContentView, but I'd like to avoid doing so if possible.
Nested models does not work yet in SwiftUI, but you could do something like this
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: SubModel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Basically your AppModel catches the event from SubModel and send it further to the View.
Edit:
If you do not need SubModel to be class, then you could try something like this either:
struct SubModel{
var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: SubModel = SubModel()
}
Sorin Lica's solution can solve the problem but this will result in code smell when dealing with complicated views.
What seems to better advice is to look closely at your views, and revise them to make more, and more targeted views. Structure your views so that each view displays a single level of the object structure, matching views to the classes that conform to ObservableObject. In the case above, you could make a view for displaying Submodel (or even several views) that display's the property from it that you want show. Pass the property element to that view, and let it track the publisher chain for you.
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
SubView(submodel: appModel.submodel)
}
}
struct SubView: View {
#ObservedObject var submodel: Submodel
var body: some View {
Text("Count: \(submodel.count)")
.onTapGesture {
self.submodel.count += 1
}
}
}
This pattern implies making more, smaller, and focused views, and lets the engine inside SwiftUI do the relevant tracking. Then you don't have to deal with the book keeping, and your views potentially get quite a bit simpler as well.
You can check for more detail in this post: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
I wrote about this recently on my blog: Nested Observable Objects. The gist of the solution, if you really want a hierarchy of ObservableObjects, is to create your own top-level Combine Subject to conform to the ObservableObject protocol, and then encapsulate any logic of what you want to trigger updates into imperative code that updates that subject.
For example, if you had two "nested" classes, such as
class MainThing : ObservableObject {
#Published var element : SomeElement
init(element : SomeElement) {
self.element = element
}
}
class SomeElement : ObservableObject {
#Published var value : String
init(value : String) {
self.value = value
}
}
Then you could expand the top-level class (MainThing in this case) to:
class MainThing : ObservableObject {
#Published var element : SomeElement
var cancellable : AnyCancellable?
init(element : SomeElement) {
self.element = element
self.cancellable = self.element.$value.sink(
receiveValue: { [weak self] _ in
self?.objectWillChange.send()
}
)
}
}
Which grabs a publisher from the embedded ObservableObject, and sends an update into the local published when the property value on SomeElement class is modified. You can extend this to use CombineLatest for publishing streams from multiple properties, or any number of variations on the theme.
This isn't a "just do it" solution though, because the logical conclusion of this pattern is after you've grown that hierarchy of views, you're going to end up with potentially huge swatches of a View subscribed to that publisher that will invalidate and redraw, potentially causing excessive, sweeping redraws and relatively poor performance on updates. I would advise seeing if you can refactor your views to be specific to a class, and match it to just that class, to keep the "blast radius" of SwiftUI's view invalidation minimized.
#Published is not designed for reference types so it's a programming error to add it on the AppModel property, even though the compiler or runtime doesn't complain. What would've been intuitive is adding #ObservedObject like below but sadly this silently does nothing:
class AppModel: ObservableObject {
#ObservedObject var submodel: SubModel = SubModel()
}
I'm not sure if disallowing nested ObservableObjects was intentional by SwiftUI or a gap to be filled in the future. Wiring up the parent and child objects as suggested in the other answers is very messy and hard to maintain. What seems to be the idea of SwiftUI is to split up the views into smaller ones and pass the child object to the subview:
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
SubView(model: appModel.submodel)
}
}
struct SubView: View {
#ObservedObject var model: SubModel
var body: some View {
Text("Count: \(model.count)")
.onTapGesture {
model.count += 1
}
}
}
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
var submodel: SubModel = SubModel()
}
The submodel mutations actually propagate when passing into a subview!
However, there's nothing stopping another dev from calling appModel.submodel.count from the parent view which is annoying there's no compiler warning or even some Swift way to enforce not doing this.
Source: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
If you need to nest observable objects here is the best way to do it that I could find.
class ChildModel: ObservableObject {
#Published
var count = 0
}
class ParentModel: ObservableObject {
#Published
private var childWillChange: Void = ()
let child = ChildModel()
init() {
child.objectWillChange.assign(to: &$childWillChange)
}
}
Instead of subscribing to child's objectWillChange publisher and firing parent's publisher, you assign values to published property and parent's objectWillChange triggers automatically.
All three ViewModels can communicate and update
// First ViewModel
class FirstViewModel: ObservableObject {
var facadeViewModel: FacadeViewModels
facadeViewModel.firstViewModelUpdateSecondViewModel()
}
// Second ViewModel
class SecondViewModel: ObservableObject {
}
// FacadeViewModels Combine Both
import Combine // so you can update thru nested Observable Objects
class FacadeViewModels: ObservableObject {
lazy var firstViewModel: FirstViewModel = FirstViewModel(facadeViewModel: self)
#Published var secondViewModel = secondViewModel()
}
var anyCancellable = Set<AnyCancellable>()
init() {
firstViewModel.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &anyCancellable)
secondViewModel.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &anyCancellable)
}
func firstViewModelUpdateSecondViewModel() {
//Change something on secondViewModel
secondViewModel
}
Thank you Sorin for Combine solution.
I have a solution that I believe is more ellegant than subscribing to the child (view)models. It's weird and I don't have an explanation for why it works.
Solution
Define a base class that inherits from ObservableObject, and defines a method notifyWillChange() that simply calls objectWillChange.send(). Any derived class then overrides notifyWillChange() and calls the parent's notifyWillChange() method.
Wrapping objectWillChange.send() in a method is required, otherwise the changes to #Published properties do not cause the any Views to update. It may have something to do with how #Published changes are detected. I believe SwiftUI/Combine use reflection under the hood...
I have made some slight additions to OP's code:
count is wrapped in a method call which calls notifyWillChange() before the counter is incremented. This is required for the propagation of the changes.
AppModel contains one more #Published property, title, which is used for the navigation bar's title. This showcases that #Published works for both the parent object and the child (in the example below, updated 2 seconds after the model is initialized).
Code
Base Model
class BaseViewModel: ObservableObject {
func notifyWillUpdate() {
objectWillChange.send()
}
}
Models
class Submodel: BaseViewModel {
#Published var count = 0
}
class AppModel: BaseViewModel {
#Published var title: String = "Hello"
#Published var submodel: Submodel = Submodel()
override init() {
super.init()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self = self else { return }
self.notifyWillChange() // XXX: objectWillChange.send() doesn't work!
self.title = "Hello, World"
}
}
func increment() {
notifyWillChange() // XXX: objectWillChange.send() doesn't work!
submodel.count += 1
}
override func notifyWillChange() {
super.notifyWillChange()
objectWillChange.send()
}
}
The View
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
NavigationView {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.increment()
}.navigationBarTitle(appModel.title)
}
}
}
I liked solution by sorin-lica. Based upon that I've decided to implement a custom Property Wrapper (following this amazing article) named NestedObservableObject to make that solution more developer friendly.
This allow to write your model in the following way
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#NestedObservableObject var submodel: Submodel = Submodel()
}
Property Wrapper implementation
#propertyWrapper
struct NestedObservableObject<Value : ObservableObject> {
static subscript<T: ObservableObject>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
get {
if instance[keyPath: storageKeyPath].cancellable == nil, let publisher = instance.objectWillChange as? ObservableObjectPublisher {
instance[keyPath: storageKeyPath].cancellable =
instance[keyPath: storageKeyPath].storage.objectWillChange.sink { _ in
publisher.send()
}
}
return instance[keyPath: storageKeyPath].storage
}
set {
if let cancellable = instance[keyPath: storageKeyPath].cancellable {
cancellable.cancel()
}
if let publisher = instance.objectWillChange as? ObservableObjectPublisher {
instance[keyPath: storageKeyPath].cancellable =
newValue.objectWillChange.sink { _ in
publisher.send()
}
}
instance[keyPath: storageKeyPath].storage = newValue
}
}
#available(*, unavailable,
message: "This property wrapper can only be applied to classes"
)
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
private var cancellable: AnyCancellable?
private var storage: Value
init(wrappedValue: Value) {
storage = wrappedValue
}
}
I've published code on gist
I do it like this:
import Combine
extension ObservableObject {
func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
{
objectWillChange.propagateWeakly(to: inputObservableObject)
}
}
extension Publisher where Failure == Never {
public func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
{
sink { [weak inputObservableObject] _ in
inputObservableObject?.objectWillChange.send()
}
}
}
So on the call side:
class TrackViewModel {
private let playbackViewModel: PlaybackViewModel
private var propagation: Any?
init(playbackViewModel: PlaybackViewModel) {
self.playbackViewModel = playbackViewModel
propagation = playbackViewModel.propagateWeakly(to: self)
}
...
}
Here's a gist.
See following post for a solution: [arthurhammer.de/2020/03/combine-optional-flatmap][1] . This is solving the question in a Combine-Way with the $ publisher.
Assume class Foto has an annotation struct and and annotation publisher, which publish an annotation struct. Within Foto.sample(orientation: .Portrait) the annotation struct gets "loaded" through the annotation publisher asynchroniously. Plain vanilla combine.... but to get that into a View & ViewModel, use this:
class DataController: ObservableObject {
#Published var foto: Foto
#Published var annotation: LCPointAnnotation
#Published var annotationFromFoto: LCPointAnnotation
private var cancellables: Set<AnyCancellable> = []
init() {
self.foto = Foto.sample(orientation: .Portrait)
self.annotation = LCPointAnnotation()
self.annotationFromFoto = LCPointAnnotation()
self.foto.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotation, on: self)
.store(in: &cancellables)
$foto
.flatMap { $0.$annotation }
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotationFromFoto, on: self)
.store(in: &cancellables)
}
}
Note: [1]: https://arthurhammer.de/2020/03/combine-optional-flatmap/
Pay attention the $annotation above within the flatMap, it's a publisher!
public class Foto: ObservableObject, FotoProperties, FotoPublishers {
/// use class not struct to update asnyc properties!
/// Source image data
#Published public var data: Data
#Published public var annotation = LCPointAnnotation.defaultAnnotation
......
public init(data: Data) {
guard let _ = UIImage(data: data),
let _ = CIImage(data: data) else {
fatalError("Foto - init(data) - invalid Data to generate CIImage or UIImage")
}
self.data = data
self.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.sink {resultAnnotation in
self.annotation = resultAnnotation
print("Foto - init annotation = \(self.annotation)")
}
.store(in: &cancellables)
}
You can create a var in your top view that is equal to a function or published var in your top class. Then pass it and bind it to every sub view. If it changes in any sub view then the top view will be updated.
Code Structure:
struct Expense : Identifiable {
var id = UUID()
var name: String
var type: String
var cost: Double
var isDeletable: Bool
}
class Expenses: ObservableObject{
#Published var name: String
#Published var items: [Expense]
init() {
name = "John Smith"
items = [
Expense(name: "Lunch", type: "Business", cost: 25.47, isDeletable: true),
Expense(name: "Taxi", type: "Business", cost: 17.0, isDeletable: true),
Expense(name: "Sports Tickets", type: "Personal", cost: 75.0, isDeletable: false)
]
}
func totalExpenses() -> Double { }
}
class ExpenseTracker: ObservableObject {
#Published var name: String
#Published var expenses: Expenses
init() {
name = "My name"
expenses = Expenses()
}
func getTotalExpenses() -> Double { }
}
Views:
struct MainView: View {
#ObservedObject var myTracker: ExpenseTracker
#State var totalExpenses: Double = 0.0
var body: some View {
NavigationView {
Form {
Section (header: Text("Main")) {
HStack {
Text("name:")
Spacer()
TextField("", text: $myTracker.name)
.multilineTextAlignment(.trailing)
.keyboardType(.default)
}
NavigationLink(destination: ContentView(myExpenses: myTracker.expenses, totalExpenses: $totalExpenses),
label: {
Text("View Expenses")
})
}
Section (header: Text("Results")) {
}
HStack {
Text("Total Expenses")
Spacer()
Text("\(totalExpenses, specifier: "%.2f")")
}
}
}
.navigationTitle("My Expense Tracker")
.font(.subheadline)
}
.onAppear{
totalExpenses = myTracker.getTotalExpenses()
}
}
}
struct ContentView: View {
#ObservedObject var myExpenses:Expenses
#Binding var totalExpenses: Double
#State var selectedExpenseItem:Expense? = nil
var body: some View {
NavigationView{
Form {
List {
ForEach(myExpenses.items) { item in
HStack {
Text("\(item.name)")
Spacer()
Button(action: {
self.selectedExpenseItem = item
} ) {
Text("View")
}
}
.deleteDisabled(item.isDeletable)
}
.onDelete(perform: removeItem)
}
HStack {
Text("Total Expenses:")
Spacer()
Text("\(myExpenses.totalExpenses(), specifier: "%.2f")")
}
}
.navigationTitle("Expenses")
.toolbar {
Button {
let newExpense = Expense(name: "Enter name", type: "Expense item", cost: 10.00, isDeletable: false)
self.myExpenses.items.append(newExpense)
self.totalExpenses = myExpenses.totalExpenses()
} label: {
Image(systemName: "plus")
}
}
}
.fullScreenCover(item: $selectedExpenseItem) { myItem in
ItemDetailView(item: myItem, myExpenses: myExpenses, totalExpenses: $totalExpenses)
}
}
func removeItem(at offsets: IndexSet){
self.myExpenses.items.remove(atOffsets: offsets)
self.totalExpenses = myExpenses.totalExpenses()
}
}
Just noting that I'm using the NestedObservableObject approach from #bsorrentino in my latest app.
Normally I'd avoid this but the nested object in question is actually a CoreData model so breaking things out into smaller views doesn't really work in this regard.
This solution seemed best since the world treats NSManagedObjects as (mostly) ObservableObjects and I really, really need to trigger an update if the CodeData object model is changed down the line.
The var submodel in AppModel doesn't need the property wrapper #Published.
The purpose of #Published is to emit new values and objectWillChange.
But the variable is never changed but only initiated once.
Changes in submodel are propagated to the view by the subscriber anyCancellable and ObservableObject-protocol via the sink-objectWillChange construction and causes a View to redraw.
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
let submodel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Nested ObservableObject models do not work yet.
However, you can make it work by manually subscribing each model. The answer gave a simple example of this.
I wanted to add that you can make this manual process a bit more streamlined & readable via extensions:
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel = Submodel()
#Published var submodel2 = Submodel2() // the code for this is not defined and is for example only
private var cancellables: Set<AnyCancellable> = []
init() {
// subscribe to changes in `Submodel`
submodel
.subscribe(self)
.store(in: &cancellables)
// you can also subscribe to other models easily (this solution scales well):
submodel2
.subscribe(self)
.store(in: &cancellables)
}
}
Here is the extension:
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
func subscribe<T: ObservableObject>(
_ observableObject: T
) -> AnyCancellable where T.ObjectWillChangePublisher == ObservableObjectPublisher {
return objectWillChange
// Publishing changes from background threads is not allowed.
.receive(on: DispatchQueue.main)
.sink { [weak observableObject] (_) in
observableObject?.objectWillChange.send()
}
}
}
It looks like bug. When I update the xcode to the latest version, it work correctly when binding to nested ObservableObjects
How to create a swiftui textfield that allows the user to only input numbers and a single dot?
In other words, it checks digit by digit as the user inputs, if the input is a number or a dot and the textfield doesn't have another dot the digit is accepted, otherwise the digit entry is ignored.
Using a stepper isn't an option.
SwiftUI doesn't let you specify a set of allowed characters for a TextField. Actually, it's not something related to the UI itself, but to how you manage the model behind. In this case the model is the text behind the TextField. So, you need to change your view model.
If you use the $ sign on a #Published property you can get access to the Publisher behind the #Published property itself. Then you can attach your own subscriber to the publisher and perform any check you want. In this case I used the sink function to attach a closure based subscriber to the publisher:
/// Attaches a subscriber with closure-based behavior.
///
/// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber.
/// - parameter receiveValue: The closure to execute on receipt of a value.
/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
public func sink(receiveValue: #escaping ((Self.Output) -> Void)) -> AnyCancellable
The implementation:
import SwiftUI
import Combine
class ViewModel: ObservableObject {
#Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
Important to note that:
$text ($ sign on a #Published property) gives us an object of type Published<String>.Publisher i.e. a publisher
$viewModel.text ($ sign on an #ObservableObject) gives us an object of type Binding<String>
That are two completely different things.
EDIT: If you want you can even create you own custom TextField with this behaviour. Let's say you want to create a DecimalTextField view:
import SwiftUI
import Combine
struct DecimalTextField: View {
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
#ObservedObject private var viewModel = DecimalTextFieldViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
struct ContentView: View {
var body: some View {
DecimalTextField()
}
}
This way you can use your custom text field just writing:
DecimalTextField()
and you can use it wherever you want.
This is a simple solution for TextField validation: (updated)
struct ContentView: View {
#State private var text = ""
func validate() -> Binding<String> {
let acceptableNumbers: String = "0987654321."
return Binding<String>(
get: {
return self.text
}) {
if CharacterSet(charactersIn: acceptableNumbers).isSuperset(of: CharacterSet(charactersIn: $0)) {
print("Valid String")
self.text = $0
} else {
print("Invalid String")
self.text = $0
self.text = ""
}
}
}
var body: some View {
VStack {
Spacer()
TextField("Text", text: validate())
.padding(24)
Spacer()
}
}
}
I think using an async dispatch is the wrong approach and may cause other issues. Here's an implementation that achieves the same thing with a Double-backed property and manually iterates over the characters each time you type in the bound view.
final class ObservableNumber: ObservableObject {
let precision: Int
#Published
var value: String {
didSet {
var decimalHit = false
var remainingPrecision = precision
let filtered = value.reduce(into: "") { result, character in
// If the character is a number that by adding wouldn't exceed the precision and precision is set then add the character.
if character.isNumber, remainingPrecision > 0 || precision <= 0 {
result.append(character)
// If a decimal has been hit then decrement the remaining precision to fulfill
if decimalHit {
remainingPrecision -= 1
}
// If the character is a decimal, one hasn't been added already, and precision greater than zero then add the decimal.
} else if character == ".", !result.contains("."), precision > 0 {
result.append(character)
decimalHit = true
}
}
// Only update value if after processing it is a different value.
// It will hit an infinite loop without this check since the published event occurs as a `willSet`.
if value != filtered {
value = filtered
}
}
}
var doubleValue: AnyPublisher<Double, Never> {
return $value
.map { Double($0) ?? 0 }
.eraseToAnyPublisher()
}
init(precision: Int, value: Double) {
self.precision = precision
self.value = String(format: "%.\(precision)f", value)
}
}
This solution also ensures you only have a single decimal instead of allowing multiple instances of ".".
Note the extra computed property to put it "back" into a Double. This allows you to continue to react to the number as a number instead of a String and have to cast/convert everywhere. You could very easily add as many computed properties as you want to react to it as Int or whatever numeric type as long as you convert it in the way you would expect.
One more note you could also make it generic ObservableNumber<N: Numeric> and handle different inputs but using Double and keeping the generics out of it will simplify other things down the road. Change according to your needs.
Easy solution is to set .numberPad keyboardType:
TextField(
"0.0",
text: $fromValue
)
.keyboardType(UIKeyboardType.numberPad)