Code for textfield character limit isn't working(SwiftUI) - swiftui

I've stumbled across this piece of code:
class TextLimiter: ObservableObject {
private let limit: Int
init(limit: Int) {
self.limit = limit
}
#Published var value = "" {
didSet {
if value.count > self.limit {
value = String(value.prefix(self.limit))
self.hasReachedLimit = true
} else {
self.hasReachedLimit = false
}
}
}
#Published var hasReachedLimit = false }
struct Strix: View {
#ObservedObject var input = TextLimiter(limit: 5)
var body: some View {
TextField("Text Input",
text: $input.value)
.border(Color.red,
width: $input.hasReachedLimit.wrappedValue ? 1 : 0 )
} }
It's a TextField limiting code where after a user inputs characters after a limit, it won't keep inputing characters inside the box. I've tried this code and after the limit is reached, it just keeps on inputting characters.
For example:
How it's supposed to work: limit is 5 so the only input allowed is 'aaaaa'
How it's behaving: limit is 5 but input allowed is 'aaaaaaaa.....'
I'm aware of a recent solution to this:
How to set textfield character limit SwiftUI?
but the solution is specifically tailored for iOS 14. I was hoping to be able to support iOS 13. Thanks.
Link to original code:
https://github.com/programmingwithswift/SwiftUITextFieldLimit/blob/master/SwiftUITextFieldLimit/SwiftUITextFieldLimit/ContentView.swift

Your solution is lies in SwiftUI's subscriber .onReceive,
Make sure that your property hasReachedLimit must not marked with #Published else it will trigger infinite loop of view body rendering.
Below shown code works as your expectation.
class TextLimiter: ObservableObject {
let limit: Int
#Published var value = ""
var hasReachedLimit = false
init(limit: Int) {
self.limit = limit
}
}
struct Strix: View {
#ObservedObject var input = TextLimiter(limit: 5)
var body: some View {
TextField("Text Input",
text: $input.value)
.border(Color.red,
width: $input.hasReachedLimit.wrappedValue ? 1 : 0 )
.onReceive(Just(self.input.value)) { inputValue in
self.input.hasReachedLimit = inputValue.count > self.input.limit
if inputValue.count > self.input.limit {
self.input.value.removeLast()
}
}
}
}
BTW this is not an efficient solution.

Related

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

Cannot assign value of type Binding<XXX> to type XXX [duplicate]

I am working on a money input screen and I need to implement a custom init to set a state variable based on the initialized amount.
I thought the following would work:
struct AmountView : View {
#Binding var amount: Double
#State var includeDecimal = false
init(amount: Binding<Double>) {
self.amount = amount
self.includeDecimal = round(amount)-amount > 0
}
}
However, this gives me a compiler error as follows:
Cannot assign value of type 'Binding' to type 'Double'
How do I implement a custom init method which takes in a Binding struct?
Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:
You'll see that I removed the #State in includeDecimal, check the explanation at the end.
This is using the property (put self in front of it):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
or using .value after (but without self, because you are using the passed parameter, not the struct's property):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(amount.value)-amount.value > 0
}
}
This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(withAmount.value)-withAmount.value > 0
}
}
Note that .value is not necessary with the property, thanks to the property wrapper (#Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.
As you discovered, modifying the #State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the #State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing #State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal: Bool {
return round(amount)-amount > 0
}
init(withAmount: Binding<Double>) {
self.$amount = withAmount
}
var body: some View { ... }
}
As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:
// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
You should use underscore to access the synthesized storage for the property wrapper itself.
In your case:
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount)-amount > 0
}
Here is the quote from Apple document:
The compiler synthesizes storage for the instance of the wrapper type by prefixing the name of the wrapped property with an underscore (_)—for example, the wrapper for someProperty is stored as _someProperty. The synthesized storage for the wrapper has an access control level of private.
Link: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> propertyWrapper section
You said (in a comment) “I need to be able to change includeDecimal”. What does it mean to change includeDecimal? You apparently want to initialize it based on whether amount (at initialization time) is an integer. Okay. So what happens if includeDecimal is false and then later you change it to true? Are you going to somehow force amount to then be non-integer?
Anyway, you can't modify includeDecimal in init. But you can initialize it in init, like this:
struct ContentView : View {
#Binding var amount: Double
init(amount: Binding<Double>) {
$amount = amount
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
}
#State private var includeDecimal: Bool
(Note that at some point the $$includeDecimal syntax will be changed to _includeDecimal.)
Since it's mid of 2020, let's recap:
As to #Binding amount
_amount is only recommended to be used during initialization. And never assign like this way self.$amount = xxx during initialization
amount.wrappedValue and amount.projectedValue are not frequently used, but you can see cases like
#Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()
A common use case of #binding is:
#Binding var showFavorited: Bool
Toggle(isOn: $showFavorited) {
Text("Change filter")
}
State:
To manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body and You should only access a state property from inside the view’s body, or from methods called.
Note: To pass a state property to another view in the view hierarchy, use the variable name with the $ prefix operator.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
Toggle(isOn: $isSmile, label: {
Text("State")
}).fixedSize()
}
}
}
Binding:
The parent view declares a property to hold the isSmile state, using the State property wrapper to indicate that this property is the value’s source of deferent view.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
SwitchView(isSmile: $isSmile)
}
}
}
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
struct SwitchView: View {
#Binding var isSmile : Bool
var body: some View {
VStack{
Toggle(isOn: $isSmile, label: {
Text("Binding")
}).fixedSize()
}
}
}
The accepted answer is one way but there is another way too
struct AmountView : View {
var amount: Binding<Double>
init(withAmount: Binding<Double>) {
self.amount = withAmount
}
var body: some View { ... }
}
You remove the #Binding and make it a var of type Binding
The tricky part is while updating this var. You need to update it's property called wrapped value. eg
amount.wrappedValue = 1.5 // or
amount.wrappedValue.toggle()
You can achieve this either with static function or with custom init.
import SwiftUI
import PlaygroundSupport
struct AmountView: View {
#Binding var amount: Double
#State var includeDecimal: Bool
var body: some View {
Text("The amount is \(amount). \n Decimals \(includeDecimal ? "included" : "excluded")")
}
}
extension AmountView {
static func create(amount: Binding<Double>) -> Self {
AmountView(amount: amount, includeDecimal: round(amount.wrappedValue) - amount.wrappedValue > 0)
}
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount.wrappedValue) - amount.wrappedValue > 0
}
}
struct ContentView: View {
#State var amount1 = 5.2
#State var amount2 = 5.6
var body: some View {
AmountView.create(amount: $amount1)
AmountView(amount: $amount2)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Actually you don't need custom init here at all since the logic could be easily moved to .onAppear unless you need to explicitly set initial state externally.
struct AmountView: View {
#Binding var amount: Double
#State private var includeDecimal = true
var body: some View {
Text("The amount is \(amount, specifier: includeDecimal ? "%.3f" : "%.0f")")
Toggle("Include decimal", isOn: $includeDecimal)
.onAppear {
includeDecimal = round(amount) - amount > 0
}
}
}
This way you keep your #State private and initialized internally as documentation suggests.
Don’t initialize a state property of a view at the point in the view
hierarchy where you instantiate the view, because this can conflict
with the storage management that SwiftUI provides. To avoid this,
always declare state as private, and place it in the highest view in
the view hierarchy that needs access to the value
.

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 ....

How to publish changes to a single object in a object array

I have the following classes
class ListItem: Identifiable {
var id: UUID
var name: String
var description: String
var isFavorite: Bool
var debugDescription: String {
return "Name: \(self.name) | Favorite?: \(self.isFavorite)"
}
public init(name: String) {
self.name = name
id = UUID()
self.description = "Some text describing why \(self.name.lowercased()) is awesome"
self.isFavorite = false
}
}
class ListItems: ObservableObject {
#Published var items: [ListItem]
let defaultAnimals = ["Ant", "Bear", "Cat", "Dog", "Elephant",
"Fish", "Giraffe", "Hyena", "Iguana", "Jackal", "Kingfisher", "Leopard", "Monkey"]
public init(animals: [String] = []) {
let animalList: [String] = animals.count > 0 ? animals : defaultAnimals
self.items = animalList.sorted {
$0.lowercased() < $1.lowercased()
}.map {
ListItem(name: $0.firstUppercased)
}
}
}
and the following image view in ContentView
struct ContentView: View {
#ObservedObject var list: ListItems = ListItems()
var body: some View {
List(list.items) {
animal in HStack {
// ...
Image(systemName: animal.isFavorite ? "heart.fill" : "heart").foregroundColor(.pink).onTapGesture {
let index = self.list.items.firstIndex { $0.id == animal.id } ?? -1
if (index >= 0) {
self.list.items[index].isFavorite = !animal.isFavorite
self.list.items = Array(self.list.items[0...self.list.items.count-1]) // <--
}
}
// ...
}
}
}
}
Everytime, the image view is tapped, I am basically reassigning the entire array like this so that the changes can be reflected in the UI
self.list.items = Array(self.list.items[0...self.list.items.count-1])
My question: How can I refactor my code to prevent reassigning the entire object array every time some object property changes?
I am fairly new to Swift & iOS development, not sure if I am missing something basic.
Declare ListItem as an struct instead of a class, this way the view will be notified when isFavorite changes. And just a little suggestion; you can use toggle to change the value of a boolean: self.list.items[index].isFavorite.toggle()

put observedObject in List

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