I am making a personal project to study SwiftUI. All was going well, the I noticed a bug on my app.
I have the simple view bellow, that saves a description, a value and some tags on my ViewModel. I am having an issue with the $viewModel.value. That variable is not being filled with values from the view.
I supose that my #Published var value: Double? from my ViewModel should be updated whenever the user types some value. Thing is, it is not updating on any iPhone 11 and up, but it works perfectly on the iPhone 8.
public struct AddBillView: View {
#ObservedObject private var viewModel: AddBillViewModel
#Environment(\.presentationMode) var presentationMode
public let onExpenseCreated: ((_ expense: Expense)->Void)
public var body: some View {
Text("Add Expense")
VStack {
TextField("Descrição", text: $viewModel.name)
HStack {
Text("Valor \(NumberFormatter.currency.currencySymbol)")
CurrencyTextField("Value", value: $viewModel.value)
.multilineTextAlignment(TextAlignment.leading)
}
HStack {
Text("Tags")
TextField("car pets home",
text: $viewModel.tags)
}
Picker("Type", selection: $viewModel.type) {
Text("Paid").tag("Paid")
Text("Unpaid").tag("Unpaid")
Text("Credit").tag("Credit")
}
}.navigationTitle("+ Expense")
Button("Adicionar") {
if !viewModel.hasExpense() {
return
}
onExpenseCreated(viewModel.expense())
self.presentationMode.wrappedValue.dismiss()
}
}
public init(viewModel outViewModel: AddBillViewModel,
onExpenseCreated: #escaping ((_ expense: Expense)->Void)) {
self.viewModel = outViewModel
self.onExpenseCreated = onExpenseCreated
}
}
And I have a ViewModel:
public class AddBillViewModel: ObservableObject {
#Published var name: String = ""
#Published var type: String = "Paid"
#Published var tags: String = ""
#Published var value: Double?
init(expense: Expense?=nil) {
self.name = expense?.name ?? ""
self.type = expense?.type.rawValue ?? "Paid"
self.tags = expense?.tags?.map { String($0.name) }.joined(separator: " ") ?? ""
self.value = expense?.value
}
func hasExpense() -> Bool {
if self.name.isEmpty ||
self.value == nil ||
self.value?.isZero == true {
return false
}
return true
}
func expense() -> Expense {
let tags = self.tags.split(separator: " ").map { Tag(name: String($0)) }
return Expense(name: self.name, value: self.value ?? 0.0 ,
type: ExpenseType(rawValue: self.type)!,
id: UUID().uuidString,
tags: tags)
}
}
Then I use my view:
AddBillView(viewModel: AddBillViewModel()) { expense in
viewModel.add(expense: expense)
viewModel.state = .idle
}
I already google it and spend a couple of hours looking for an answer, with no luck. Someone have any ideas?
Edited
Here is the code for the CurrencyTextField. I`m using this component:
https://github.com/youjinp/SwiftUIKit/blob/master/Sources/SwiftUIKit/views/CurrencyTextField.swift
But the component works perfectly fine on iPhone 8 simulator and with a #State property inside my view. It does not work only with my ViewModel
I figured it out! The problem was that my AddBillViewModel is an ObservableObject and I was marking each property with #Published. This was causing some kind of double observable object.
I removed the #Published and it started working again.
Related
I am fairly new to iOS development. I am trying to update the property "cars" in the "Sire" model using a stepper. Whenever I press on + or - from the stepper controls, it changes the value by a step and then becomes disabled.
If I bind the stepper to the variable cars, it works flawlessly.
struct AddSireView: View {
// #EnvironmentObject var sireVM:SiresViewModel
#State var newSire = Sire (id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
#State var cars = 0
#State var cups = 0
#State private var state = FormState.idle
var createAction: CreateAction
// TODO: Put validation that the added sire is valid and if not show errors to the user
var body: some View {
Form {
VStack (spacing: 18) {
TitledTextView(text: $newSire.name, placeHolder: "الاسم", title: "الاسم")
TiltedTextEditor(text: Binding<String>($newSire.info)!, title: "معلومات البعير")
TiltedTextEditor(text: Binding<String>($newSire.achievments)!, title: "انجازات البعير")
}
Stepper(value: $newSire.cars, in: 0...10,step:1) {
HStack {
Text ("سيارات:")
TextField("Cars", value: $newSire.cars, formatter: NumberFormatter.decimal)
}
}
And this is the "Sire" struct
struct Sire: Hashable, Identifiable, Decodable {
static func == (lhs: Sire, rhs: Sire) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.ownerID == rhs.ownerID
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
hasher.combine(ownerID)
}
var id:String
var name:String
var ownerID:Int
var fatherID:String?
var info:String?
var achievments:String?
var cars:Int = 0
var cups:Int = 0
init (id:String, name:String, ownerID:Int, info:String? = nil, achievments:String? = nil,
fatherID:String? = nil, cars:Int = 0, cups:Int = 0) {
self.id = id
self.name = name
self.ownerID = ownerID
self.cars = cars
self.cups = cups
self.info = info
self.achievments = achievments
}
}
"Sire" was a class and i made it a Struct thinking that that was the problem, but to no avail.
Consider this approach using an ObservableObject to hold your Sire. This allows you to use
both the Stepper and the Textfield at the same time.
struct ContentView: View {
#StateObject var sireModel = SireModel() // <-- here
var body: some View {
Form {
Stepper(value: $sireModel.sire.cars, in: 0...10, step:1) {
HStack {
Text ("سيارات: ")
TextField("", value: $sireModel.sire.cars, formatter: NumberFormatter())
}
}
}
}
}
class SireModel: ObservableObject {
#Published var sire: Sire = Sire(id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
}
Get rid of the custom implementations for Equatable and Hashable (func == and func hash) you don't include cars in it so SwiftUI doesn't know when to reload.
SwiftUI is all about identity if you change how Swift computes the identity (using Hashable, Equatable and Identifiable) you change the behavior.
Check out Demystify SwiftUI
The video above is the "best" place to learn about the concept.
I have a list with a .searchable search bar on top of it. It filters a struct that has text, but also a date.
I'm trying to have the user select whether he/she wants to search for all items containing the specific text, or search for all items with a data. I thought the way that iOS Mail did it when you hit he search bar on top is a good way (I'm open to other options tho...).
It looks like this:
So, when you tap the search field, the picker, or two buttons, or a tab selector shows up. I can't quite figure which is it. Regardless, I tried with a picker, but:
I don't know where to place it
I don't know how to keep it hidden until needed, and then hide it again.
this is the basic code:
let dateFormatter = DateFormatter()
struct LibItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("myapp") public var collectables: [LibItem] = []
init() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
var body: some View {
NavigationView {
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer() Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
And I was trying to add something like, taking into account isSearching:
#Environment(\.isSearching) var isSearching
var searchBy = [0, 1] // 0 = by text, 1 = by date
#State private var selectedSearch = 0
// Yes, I'd add the correct text to it, but I wanted to have it
// working first.
Picker("Search by", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
How do I do it? How can I replicate that search UX from Mail? Or, is there any better way to let the user chose whether search text or date that appears when the user taps on the search?
isSearching works on a sub view that has a searchable modifier attached. So, something like this would work:
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
#State private var selectedItem = 0
var body: some View {
NavigationView {
VStack {
SearchableSubview(selectedItem: $selectedItem)
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer()
Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
}.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
struct SearchableSubview : View {
#Environment(\.isSearching) private var isSearching
#Binding var selectedItem : Int
var body: some View {
if isSearching {
Picker("Search by", selection: $selectedItem) {
Text("Choice 1").tag(0)
Text("Choice 2").tag(1)
}.pickerStyle(.segmented)
}
}
}
I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}
I have a SwiftUI app with SwiftUI App lifecycle that includes a master-detail type
list driven from CoreData. I have the standard list in ContentView and NavigationLinks
to the DetailView. I pass a Core Data entity object to the Detailview.
My struggle is setting-up bindings to TextFields in the DetailView for data entry
and for editing. I tried to create an initializer which I could not make work. I have
only been able to make it work with the following. Assigning the initial values
inside the body does not seem like the best way to do this, though it does work.
Since the Core Data entities are ObservableObjects I thought I should be able to
directly access and update bound variables, but I could not find any way to reference
a binding to Core Data in a ForEach loop.
Is there a way to do this that is more appropriate than my code below?
Simplified Example:
struct DetailView: View {
var thing: Thing
var count: Int
#State var localName: String = ""
#State private var localComment: String = ""
#State private var localDate: Date = Date()
//this does not work - cannot assign String? to State<String>
// init(t: Thing) {
// self._localName = t.name
// self._localComment = t.comment
// self._localDate = Date()
// }
var body: some View {
//this is the question - is this safe?
DispatchQueue.main.async {
self.localName = self.thing.name ?? "no name"
self.localComment = self.thing.comment ?? "No Comment"
self.localDate = self.thing.date ?? Date()
}
return VStack {
Text("\(thing.count)")
.font(.title)
Text(thing.name ?? "no what?")
TextField("name", text: $localName)
Text(thing.comment ?? "no comment?")
TextField("comment", text: $localComment)
Text("\(thing.date ?? Date())")
//TextField("date", text: $localDate)
}.padding()
}
}
And for completeness, the ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Thing.date, ascending: false)])
private var things : FetchedResults<Thing>
#State private var count: Int = 0
#State private var coverDeletedDetail = false
var body: some View {
NavigationView {
List {
ForEach(things) { thing in
NavigationLink(destination: DetailView(thing: thing, count: self.count + 1)) {
HStack {
Image(systemName: "gear")
.resizable()
.frame(width: 40, height: 40)
.onTapGesture(count: 1, perform: {
updateThing(thing)
})
Text(thing.name ?? "untitled")
Text("\(thing.count)")
}
}
}
.onDelete(perform: deleteThings)
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}
.navigationTitle("Thing List")
.navigationBarItems(trailing: Button("Add Task") {
addThing()
})
}
}
private func updateThing(_ thing: FetchedResults<Thing>.Element) {
withAnimation {
thing.name = "Updated Name"
thing.comment = "Updated Comment"
saveContext()
}
}
private func deleteThings(offsets: IndexSet) {
withAnimation {
offsets.map { things[$0] }.forEach(viewContext.delete)
saveContext()
self.coverDeletedDetail = true
}
}
private func addThing() {
withAnimation {
let newThing = Thing(context: viewContext)
newThing.name = "New Thing"
newThing.comment = "New Comment"
newThing.date = Date()
newThing.count = Int64(self.count + 1)
self.count = self.count + 1
saveContext()
}
}
func saveContext() {
do {
try viewContext.save()
} catch {
print(error)
}
}
}
And Core Data:
extension Thing {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Thing> {
return NSFetchRequest<Thing>(entityName: "Thing")
}
#NSManaged public var comment: String?
#NSManaged public var count: Int64
#NSManaged public var date: Date?
#NSManaged public var name: String?
}
extension Thing : Identifiable {
}
Any guidance would be appreciated. Xcode 12.2 iOS 14.2
You already mentioned it. CoreData works great with SwiftUI.
Just make your Thing as ObservableObject
#ObservedObject var thing: Thing
and then you can pass values from thing as Binding. This works in ForEach aswell
TextField("name", text: $thing.localName)
For others - note that I had to use the Binding extension above since NSManagedObjects are optionals. Thus as davidev stated:
TextField("name", text: Binding($thing.name, "no name"))
And ObservedObject, not Observable
I have a simple use case of having a VStack of a dynamic number of Text with Toggle buttons coming from an array.
import SwiftUI
public struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
public init() {
}
public var body: some View {
VStack {
ForEach(viewModel.models) { model in
ToggleView(title: <#T##Binding<String>#>, switchState: <#T##Binding<Bool>#>)
//how to add the above
}
}.padding(50)
}
}
struct ToggleView: View {
#Binding var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
public class TestModel: Identifiable {
#Published var state: Bool {
didSet {
//do something
//publish to the view that the state has changed
}
}
#Published var title: String
init(state: Bool, title: String) {
self.state = state
self.title = title
}
}
public class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [TestModel(state: false, title: "Title 1"), TestModel(state: true, title: "Title 2")]
}
The following questions arise:
In MVVM pattern, is it ok to have the binding variables in model class or should it be inside the view model?
How to send the message of state change from model class to view/scene when the toggle state is changed?
If using an array of binding variables in view model for each of the toggle states, how to know which particular element of array has changed? (see the following code snippet)
class ViewModel {
#Published var dataModel: [TestModel]
#Published var toggleStates = [Bool]() {
didSet {
//do something based on which element of the toggle states array has changed
}
}
}
Please help with the above questions.
here is one way you could achieve what you desire.
Has you will notice you have to use the binding power of #ObservedObject.
The trick is to use indexes to reach the array elements for you binding.
If you loop on the array elements model directly you loose the underlying binding properties.
struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
var body: some View {
VStack {
ForEach(viewModel.models.indices) { index in
ToggleView(title: self.viewModel.models[index].title, switchState: self.$viewModel.models[index].state)
}
}.padding(50)
}
}
class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [
TestModel(state: false, title: "Title 1"),
TestModel(state: true, title: "Title 2")]
}
struct ToggleView: View {
var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
class TestModel: Identifiable {
var state: Bool
var title: String
init(state: Bool, title: String) {
self.title = title
self.state = state
}
}
Hope this does the trick for you.
Best