put observedObject in List - swiftui

I get the data from my api and create a class for them. I can use swifyJSON to init them correctly. The problem is that when I put my observedObject in a List, it can only show correctly once. It will crashed after I changed the view. It's very strong because my other List with similar data struct can work.(this view is in a tabView) Is somebody know where my getAllNotification() should put view.onAppear() or List.onAppear()? Thanks!!
class ManagerNotification : Identifiable, ObservableObject{
#Published var id = UUID()
var notifyId : Int = 0
var requestId : Int = 0
var requestName: String = ""
var groupName : String = ""
// var imageName: String { return name }
init(jsonData:JSON) {
notifyId = jsonData["notifyId"].intValue
requestId = jsonData["requestId"].intValue
requestName = jsonData["requestName"].stringValue
groupName = jsonData["groupName"].stringValue
}
}
import SwiftUI
import SwiftyJSON
struct NotificationView: View {
var roles = ["userNotification", "managerNotification"]
#EnvironmentObject var userToken:UserToken
#State var show = false
#State private var selectedIndex = 0
#State var userNotifications : [UserNotification] = [UserNotification]()
#State var managerNotifications : [ManagerNotification] = [ManagerNotification]()
var body: some View {
VStack {
Picker(selection: $selectedIndex, label: Text(" ")) {
ForEach(0..<roles.count) { (index) in
Text(self.roles[index])
}
}
.pickerStyle(SegmentedPickerStyle())
containedView()
Spacer()
}
.onAppear(perform: getAllNotification)
}
func containedView() -> AnyView {
switch selectedIndex {
case 0:
return AnyView(
List(userNotifications) { userNotification in
UserNotificationCellView(userNotification: userNotification)
}
)
case 1:
return AnyView(
List(managerNotifications) { managernotification in
ManagerNotificationCellView(managerNotification : managernotification)
}
.onAppear(perform: getManagerNotification)
)
default:
return AnyView(Text("22").padding(40))
}
}
func getAllNotification(){
// if (self.userNotifications.count != 0){
// self.userNotifications.removeAll()
// }
// I think the crash was in here, because when i don't use removeAll().
// It works fine, but i don't want every times i change to this view. my array will be longer and
// longer
if (self.managerNotifications.count != 0){
self.managerNotifications.removeAll()
}
NetWorkController.sharedInstance.connectApiByPost(api: "/User/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.userNotifications.append(UserNotification(jsonData: notification))
}
}
}
}
}
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
func getManagerNotification(){
// if (self.managerNotifications.count != 0){
// self.managerNotifications.removeAll()
// }
print(self.managerNotifications.count)
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
}
error message
Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. reason: 'attempt to delete section 0, but there are only 0 sections before the update'

I think you are confused about the role of #State and #ObservebableObject; it's not like MVC where you replace the ViewController with a SwiftUI.View as it appears you are trying to do in your example. Instead the view should be a function of either some local #State and/or an external #ObservedObject. This is closer to MVVM where your #ObservedObject is analogous to the ViewModel and the view will rebuild itself in response to changes in the #Published properties on the ObservableObject.
TLDR: move your fetching logic to an ObservableObject and use #Published to allow the view to subscribe to the results. I have an example here: https://github.com/joshuajhomann/TVMaze-SwiftUI-Navigation

Related

Displaying and Logging Persistence and App Store Errors

I am trying to figure out how to display errors such as those found in the App Store Store class and Persistence.swift file in a single reporting module. Some of my errors within views use an environment object to tie the error production to the error reporting, but this isn’t feasible with errors in different classes.
So any pointers on handling these types of errors would be much appreciated.
Below is an example of my process to report an error within a view
// the save button has been pressed
struct saveButton: View {
#Environment(\.managedObjectContext) var viewContext
#EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
// prepping data for save to core data
do {
try viewContext.save()
} catch {
// report: Unable to Save Transaction
self.errorHandling.handle(error: AppError.saveTransactionError)
}
The App Store purchase method (below) may produce two errors that I would like to display with my app. This is the type of logic that I need help display the error within a view
#MainActor
func purchase() {
Task.init {
guard let product = products.first else {
return
}
guard AppStore.canMakePayments else {
return
}
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
// save to user defaults
self.purchasedIds = transaction.productID
break
case .unverified:
// throw PurchaseError.failed
// FIX: report error (Unable to purchase verification failed)
break
}
break
case .userCancelled:
break
// asked to buy (setting on phone prevents purchases)
case .pending:
break
default:
break
}
}
catch {
print(error.localizedDescription)
// FIX: report error (Unable to Complete Purchase)
}
}
}
enum AppError: LocalizedError {
case storePurchaseError
case storeVerificationError
var errorDescription: String? {
switch self {
case .storePurchaseError:
return NSLocalizedString("Store Purchase Error", comment: "")
case .storeVerificationError:
return NSLocalizedString("Store Verification Error", comment: "")
}
}
}
Below is some code I have been using to display an alert
struct ErrorAlert: Identifiable {
var id = UUID()
var message: String
var dismissAction: (() -> Void)?
}
class ErrorHandling: ObservableObject {
#Published var currentAlert: ErrorAlert?
func handle(error: Error) {
currentAlert = ErrorAlert(message: error.localizedDescription)
}
}
struct HandleErrorsByShowingAlertViewModifier: ViewModifier {
#StateObject var errorHandling = ErrorHandling()
func body(content: Content) -> some View {
content
.environmentObject(errorHandling)
.background(
EmptyView()
.alert(item: $errorHandling.currentAlert) { currentAlert in
Alert(
title: Text("Error"),
message: Text(currentAlert.message),
dismissButton: .default(Text("Ok")) {
currentAlert.dismissAction?()
}
)
}
)
}
}
extension View {
func withErrorHandling() -> some View {
modifier(HandleErrorsByShowingAlertViewModifier())
}
}

SwiftUI Using MapKit for Address Auto Complete

I have a form where the user enters their address. While they can always enter it manually, I also wanted to provide them with an easy solution with auto complete so that they could just start typing their address and then tap on the correct one from the list and have it auto populate the various fields.
I started by working off of jnpdx's Swift5 solution - https://stackoverflow.com/a/67131376/11053343
However, there are two issues that I cannot seem to solve:
I need the results to be limited to the United States only (not just the continental US, but the entire United States including Alaska, Hawaii, and Puerto Rico). I am aware of how MKCoordinateRegion works with the center point and then the zoom spread, but it doesn't seem to work on the results of the address search.
The return of the results provides only a title and subtitle, where I need to actually extract all the individual address information and populate my variables (i.e. address, city, state, zip, and zip ext). If the user has an apt or suite number, they would then fill that in themselves. My thought was to create a function that would run when the button is tapped, so that the variables are assigned based off of the user's selection, but I have no idea how to extract the various information required. Apple's docs are terrible as usual and I haven't found any tutorials explaining how to do this.
This is for the latest SwiftUI and XCode (ios15+).
I created a dummy form for testing. Here's what I have:
import SwiftUI
import Combine
import MapKit
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.region = MKCoordinateRegion()
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//currentPromise?(.failure(error))
}
}
struct MapKit_Interface: View {
#StateObject private var mapSearch = MapSearch()
#State private var address = ""
#State private var addrNum = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
#State private var zipExt = ""
var body: some View {
List {
Section {
TextField("Search", text: $mapSearch.searchTerm)
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
// Function code goes here
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End Section
Section {
TextField("Address", text: $address)
TextField("Apt/Suite", text: $addrNum)
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
TextField("Zip-Ext", text: $zipExt)
} // End Section
} // End List
} // End var Body
} // End Struct
Since no one has responded, I, and my friend Tolstoy, spent a lot of time figuring out the solution and I thought I would post it for anyone else who might be interested. Tolstoy wrote a version for the Mac, while I wrote the iOS version shown here.
Seeing as how Google is charging for usage of their API and Apple is not, this solution gives you address auto-complete for forms. Bear in mind it won't always be perfect because we are beholden to Apple and their maps. Likewise, you have to turn the address into coordinates, which you then turn into a placemark, which means there will be some addresses that may change when tapped from the completion list. Odds are this won't be an issue for 99.9% of users, but thought I would mention it.
At the time of this writing, I am using XCode 13.2.1 and SwiftUI for iOS 15.
I organized it with two Swift files. One to hold the class/struct (AddrStruct.swift) and the other which is the actual view in the app.
AddrStruct.swift
import SwiftUI
import Combine
import MapKit
import CoreLocation
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results.filter { $0.subtitle.contains("United States") } // This parses the subtitle to show only results that have United States as the country. You could change this text to be Germany or Brazil and only show results from those countries.
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//could deal with the error here, but beware that it will finish the Combine publisher stream
//currentPromise?(.failure(error))
}
}
struct ReversedGeoLocation {
let streetNumber: String // eg. 1
let streetName: String // eg. Infinite Loop
let city: String // eg. Cupertino
let state: String // eg. CA
let zipCode: String // eg. 95014
let country: String // eg. United States
let isoCountryCode: String // eg. US
var formattedAddress: String {
return """
\(streetNumber) \(streetName),
\(city), \(state) \(zipCode)
\(country)
"""
}
// Handle optionals as needed
init(with placemark: CLPlacemark) {
self.streetName = placemark.thoroughfare ?? ""
self.streetNumber = placemark.subThoroughfare ?? ""
self.city = placemark.locality ?? ""
self.state = placemark.administrativeArea ?? ""
self.zipCode = placemark.postalCode ?? ""
self.country = placemark.country ?? ""
self.isoCountryCode = placemark.isoCountryCode ?? ""
}
}
For testing purposes, I called my main view file Test.swift. Here's a stripped down version for reference.
Test.swift
import SwiftUI
import Combine
import CoreLocation
import MapKit
struct Test: View {
#StateObject private var mapSearch = MapSearch()
func reverseGeo(location: MKLocalSearchCompletion) {
let searchRequest = MKLocalSearch.Request(completion: location)
let search = MKLocalSearch(request: searchRequest)
var coordinateK : CLLocationCoordinate2D?
search.start { (response, error) in
if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
coordinateK = coordinate
}
if let c = coordinateK {
let location = CLLocation(latitude: c.latitude, longitude: c.longitude)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard let placemark = placemarks?.first else {
let errorString = error?.localizedDescription ?? "Unexpected Error"
print("Unable to reverse geocode the given location. Error: \(errorString)")
return
}
let reversedGeoLocation = ReversedGeoLocation(with: placemark)
address = "\(reversedGeoLocation.streetNumber) \(reversedGeoLocation.streetName)"
city = "\(reversedGeoLocation.city)"
state = "\(reversedGeoLocation.state)"
zip = "\(reversedGeoLocation.zipCode)"
mapSearch.searchTerm = address
isFocused = false
}
}
}
}
// Form Variables
#FocusState private var isFocused: Bool
#State private var btnHover = false
#State private var isBtnActive = false
#State private var address = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
// Main UI
var body: some View {
VStack {
List {
Section {
Text("Start typing your street address and you will see a list of possible matches.")
} // End Section
Section {
TextField("Address", text: $mapSearch.searchTerm)
// Show auto-complete results
if address != mapSearch.searchTerm && isFocused == false {
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
reverseGeo(location: location)
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End if
// End show auto-complete results
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
} // End Section
.listRowSeparator(.visible)
} // End List
} // End Main VStack
} // End Var Body
} // End Struct
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
If anyone is wondering how to generate global results, change the code from this:
self.locationResults = results.filter{$0.subtitle.contains("United States")}
to this in Address Structure file:
self.locationResults = results

How to add and delete objects from a List from an object who's inside another List in SwiftUI and Realm

In the following code I have a List of Cars and each Car from that list has its own list of Services, I can add and delete Cars without a problem by calling carViewModel.addNewCar(make:String, model:String) and carViewModel.deleteCar(at indexSet:IndexSet).
Car.swift
import RealmSwift
final class Car: Object, ObjectKeyIdentifiable{
#objc dynamic var make: String = ""
#objc dynamic var model: String = ""
// creation date, ID etc.
dynamic var services = List<CarService>()
}
CarList.swift
import RealmSwift
final class CarList: Object, ObjectKeyIdentifiable{
#objc dynamic var name: String = ""
// creation date, ID etc.
var cars = RealmSwift.List<Car>()
}
CarService.swift
import RealmSwift
final class CarService: Object, ObjectKeyIdentifiable{
#objc dynamic var serviceName: String = ""
// creation date, ID etc.
}
View Model
import RealmSwift
class CarViewModel: ObservableObject{
#Published var cars = List<Car>()
#Published var selectedCarList: CarList? = nil
var token: NotificationToken? = nil
init(){
// Create a the default lists if they don't already exist.
createDefaultCarList()
createDefaultServiceList()
// Initialize the SelectedCarList and the cars variables items from the Default Car List.
if let list = realm?.objects(CarList.self).first{
self.selectedCarList = list
self.cars = list.cars
}
token = selectedCarList?.observe({ [unowned self] (changes) in
switch changes{
case .error(_): break
case.change(_, _):self.objectWillChange.send()
case.deleted: self.selectedCarList = nil
}
})
}
func addNewCar(make:String, model:String){
if let realm = selectedCarList?.realm{
try? realm.write{
let car = Car()
car.make = make
car.model = model
selectedCarList?.cars.append(car)
}
}
}
func deleteCar(at indexSet:IndexSet){
if let index = indexSet.first,
let realm = cars[index].realm{
try? realm.write{
realm.delete(cars[index])
}
}
}
func addService(toCar: Car, serviceName: String){
try? realm?.write{
let service = CarService()
service.serviceName = serviceName
toCar.services.append(service)
}
}
/// Creates the Default Car List if it doesn't already exists otherwise just prints the error.
func createDefaultCarList(){
do {
if (realm?.objects(CarList.self).first) == nil{
try realm?.write({
let defaultList = CarList()
defaultList.name = "Default Car List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
/// Creates the Default Serivice List if it doesn't already exists otherwise just prints the error.
func createDefaultServiceList(){
do {
if (realm?.objects(ServiceList.self).first) == nil{
try realm?.write({
let defaultList = ServiceList()
defaultList.listName = "Default Service List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
}
My issue is adding or deleting Services to existing Cars. When I call carViewModel.addService(toCar: Car, serviceName: String) I get the error below...
Calling the addService() method.
struct NewServiceFormView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedCar:Car // pass from other cars view
var body: some View {
NavigationView {
Form {
// fields
}
.navigationBarItems( trailing:Button("Save", action: addNewCar))
}
}
func addNewCar(){
carViewModel.addService(toCar: selectedCar, serviceName: "Oil Change")
}
}
Error
"Cannot modify managed RLMArray outside of a write transaction."
I can add new Services by explicitly selecting a Car from the cars list. I don't get any errors but the UI doesn't update; I don't see the newly added Service until the app is relaunched.
No errors doing it this way but the UI doesn't update.
carViewModel.addService(toCar: carViewModel.cars[1], serviceName: "Rotors")
How can I properly watch, delete and add Services to existing Cars?
EDIT: Added the following code per Mahan's request.
View to present the NewServiceFormView
struct CarServicesView: View {
#State var selectedCar:Car // a car from parent view
#ObservedObject var carViewModel: CarViewModel
var body: some View {
VStack{
List {
Section(header: Text("Services: \(selectedCar.services.count)")) {
ForEach(selectedCar.services) { service in
}
}
}
.listStyle(GroupedListStyle())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: openNewServiceForm) {
Image(systemName: "plus")
}
}
}
}.sheet(isPresented: $newServiceFormIsPresented){
NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar)
}
}
func openNewServiceForm() {
newServiceFormIsPresented.toggle()
}
}
One issue is how the Realm objects are being observed - remember they are ObjC objects under the hood so you need to use Realm observers. So this
#ObservedObject var carViewModel: CarViewModel
should be this
#ObservedRealmObject var carViewModel: CarViewModel
See the documentation for observedRealmObject
Also, keep in mind if you're observing a Results, the same thing applies, use
#ObservedResults
as shown in the documentation

Swift How to reload an Environment-Object

I want to add a Button to my view to load more data. In my Environment Object the data is generated randomly via an API.
How can I reload my Environment object to get new items. The Code Below should make it clear. Thanks in advance
class observer : ObservableObject{
#Published var shows = [stacks]()
#Published var last = -1
var results = [Result1]()
init(){
let number = Int.random(in: 1...35)
print("das ist dier etste radodm nube: \(number)")
let endnumber = number + 8
print("das ist dier etste radodm nube: \(endnumber)")
for n in number...endnumber{
guard let url = URL(string:"https://api.themoviedb.org/3/discover/tv?api_key=3ed2bd15f916d0e3fbb77c193bf33b61&language=de-DE&region=DE&with_networks=213&page=\(n)" ) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
for i in self.results{
self.shows.append(stacks(id: "\(i.id)", name: i.name, typ: "Serie", status: "",overview: i.overview, vote: "\(i.vote_average)", image: i.poster_path, swipe: 0, degree: 0, commercal: "no"))
}
self.shows.shuffle()
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
}
struct View: View {
#EnvironmentObject var cards : observer
var body: some View {
VStack{
Button(action: {
//reload
}){
Text("reload")
}
}
I would put the fetching/loading data code that is inside the init in a function.
func codeToFetchData() {
//Code from your init
}
The call that method in your init and your Button for example cards.codeToFetchData()

Efficient way to model the data for SwiftUI

I am exploring SwiftUI+Combine with a demo app BP Management.
Homescreen has a provision to take bp readings(systolicBP, diastolicBP, pulse & weight).
Button "Next" is enabled only when all 4 fields are filled.
control should fall to the next textfield when a valid input is entered. (input is valid when it falls between the range specified by the placeholder - refer the image below)
On tapping next, on the detail screen user can edit the bp values (taken in the HomeScreen), additionally he can add recorded date, notes...
Thought enums would be best model this so I proceeded like
enum SBPInput: CaseIterable {
//name is a Text to indicate the specific row
typealias Field = (name: String, placeholder: String)
case spb, dbp, pulse, weight, note, date
var field: Field {
switch self {
case .dbp: return ("DBP", "40-250")
case .spb: return ("SBP", "50-300")
case .pulse: return ("Pulse", "40-400")
case .weight: return ("Weight", "30-350")
case .note: return ("Note", "")
case .date: return ("", Date().description)
}
}
// Here I am getting it wrong, - I can't bind a read only property
var value: CurrentValueSubject<String, Never> {
switch self {
case .date:
return CurrentValueSubject<String, Never>(Date().description)
case .spb:
return CurrentValueSubject<String, Never>("")
case .dbp:
return CurrentValueSubject<String, Never>("")
case .pulse:
return CurrentValueSubject<String, Never>("")
case .weight:
return CurrentValueSubject<String, Never>("70")
case .note:
return CurrentValueSubject<String, Never>("")
}
}
}
class HomeViewModel: ObservableObject {
#Published var aFieldsisEmpty: Bool = true
var cancellable: AnyCancellable?
var dataSoure = BPInput.allCases
init() {
var bpPublishers = (0...3).map{ BPInput.allCases[$0].value }
//If a field is empty, we need to disable "Next" button
cancellable = Publishers.CombineLatest4(bpPublishers[0], bpPublishers[1], bpPublishers[2], bpPublishers[3]).map { $0.isEmpty || $1.isEmpty || $2.isEmpty || $3.isEmpty }.assign(to: \.aFieldsisEmpty, on: self)
}
}
The idea is to create HStacks for each datasorce(sbp,dbp,pulse,weight) to look like this
struct HomeScreen: View {
#ObservedObject var viewModel = HomeViewModel()
var body: some View {
VStack {
ForEach(Range(0...3)) { index -> BPField in
BPField(input: self.$viewModel.dataSoure[index])
}
Button("Next", action: {
print("Take to the Detail screen")
}).disabled(self.viewModel.aFieldsisEmpty)
}.padding()
}
}
struct BPField: View {
#Binding var input: BPInput
var body: some View {
//implicit HStack
Text(input.field.name)
BPTextField(text: $input.value, placeHolder: input.field.name)//Error:- Cannot assign to property: 'value' is a get-only property
// input.value being read only I can't bind it. How to modify my model now so that I can bind it here?
}
}
And my custom TextField
struct BPTextField: View {
let keyboardType: UIKeyboardType = .numberPad
var style: some TextFieldStyle = RoundedBorderTextFieldStyle()
var text: Binding<String>
let placeHolder: String
// var onEdingChanged: (Bool) -> Void
// var onCommit: () -> ()
var background: some View = Color.white
var foregroundColor: Color = .black
var font: Font = .system(size: 14)
var body: some View {
TextField(placeHolder, text: text)
.background(background)
.foregroundColor(foregroundColor)
.textFieldStyle(style)
}
}
your problems are not there, what SwiftUI tells you.
but you should first compile "small parts" of your code and simplify it, so the compiler will tell you the real errors.
one is here:
BPTextField(text: self.$viewModel.dataSoure[index].value, placeHolder: viewModel.dataSoure[index].field.placeholder)
and the error is:
Cannot subscript a value of type 'Binding<[BPInput]>' with an argument of type 'WritableKeyPath<_, _>'
and of course you forgot the self ....