SwiftUI picker not changing selection with an Enum - swiftui

I'm trying to use an Enum with a SwiftUI picker, but can't get the picker to update the chosen value. Not quite sure what I'm doing wrong.
enum WindowLayout: String, Equatable, Codable, CaseIterable, Identifiable {
case oneByOne = "1 x 1"
case oneByTwo = "1 x 2"
case oneByThree = "1 x 3"
case twoByOne = "2 x 1"
case twoByTwo = "2 x 2"
case twoByThree = "2 x 3"
case threeByOne = "3 x 1"
case threeByTwo = "3 x 2"
var id: WindowLayout {
self
}
var rows: Int {
switch self {
case .oneByOne, .oneByTwo, .oneByThree:
return 1
case .twoByOne, .twoByTwo, .twoByThree:
return 2
case .threeByOne, .threeByTwo:
return 3
}
}
var columns: Int {
switch self {
case .oneByOne, .twoByOne, .threeByOne:
return 1
case .oneByTwo, .twoByTwo, .threeByTwo :
return 2
case .oneByThree, .twoByThree :
return 3
}
}
}
struct WindowCommands: Commands {
#ObservedObject var viewModel = GridConfigViewModel(windowLayout: WindowLayout.oneByOne)
var body: some Commands {
CommandMenu("Video") {
Picker(selection: $viewModel.windowLayout, label: Text("Window Configuration")) {
ForEach(WindowLayout.allCases, id: \.id) {
Text($0.rawValue).tag($0)
}
}
}
}
}
class GridConfigViewModel: ObservableObject {
#Published public var windowLayout: WindowLayout {
didSet {
print("set \(windowLayout.rawValue)")
}
}
init(windowLayout: WindowLayout) {
self.windowLayout = windowLayout
}
}

Create the viewModel wrapped in a #Publisher in the caller of WindowCommand instead of inside WindowCommand. That caller should be an ObservableObject.
The #ObservedObject is the correct wrapper for the property in WindowCommand, but they way it is above means it's instantiated each time WindowCommand is called.
#Published var gridViewModel = GridConfigViewModel(windowLayout: WindowLayout.oneByOne)
... then where WindowCommands is called ...
WindowCommand(viewModel: gridViewModel)

Related

SwiftUI .onAppear not firing as expected when I load a new page of visible JSON data

Edited from an earlier post to include a working subset of code:
I'm apparently not understanding how .onAppear works in SwiftUI with respect to Views inside of Navigation Links. I'm trying to use it to get paged JSON (in this case from the Pokemon API at pokeapi.co.
A minimal reproducible bit of code is below. As I scroll through the list, I see all of the Pokemon names for the first page print out & when I hit the last Pokemon on the page, I get the next page of JSON (I can see the # jump from 20, one page, to 40, two pages). My API call seems to be working fine & I'm loading a second page of Pokemon. I see their names appear & they print to the console when running in the simulator. However, even though the JSON is properly loaded into my list & I go from 20 to 40 Pokemon - a correct array of the first two pages - as I scroll past 40 it looks like the third page has loaded, creatures through 60 are visible in the List, but the console only occasionally shows an index name printing (also shown a sample of the output, below, note the values printing past 40 don't all show). The .onAppear doesn't seem to be firing as I expected past the 40th element, even though I can see 60 names showing up in the List. I was hoping to use .onAppear to detect when a new page needs to load & call it, but this method doesn't seem sound. Any hints why .onAppear isn't working as I expect & how I might more soundly handle recognizing when I need to load the next page of JSON? Thanks!
struct Creature: Hashable, Codable {
var name: String
var url: String
}
#MainActor
class Creatures: ObservableObject {
private struct Returned: Codable {
var count: Int
var next: String?
var results: [Creature]
}
var count = 0
var urlString = "https://pokeapi.co/api/v2/pokemon/"
#Published var creatureArray: [Creature] = []
var isFetching = false
func getData() async {
guard !isFetching else { return }
isFetching = true
print("🕸 We are accessing the url \(urlString)")
// Create a URL
guard let url = URL(string: urlString) else {
print("😡 ERROR: Could not create a URL from \(urlString)")
isFetching = false
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let returned = try? JSONDecoder().decode(Returned.self, from: data) {
self.count = returned.count
self.urlString = returned.next ?? ""
DispatchQueue.main.async {
self.creatureArray = self.creatureArray + returned.results
}
isFetching = false
} else {
isFetching = false
print("😡 JSON ERROR: Could not decode returned data.")
}
} catch {
isFetching = false
print("😡 ERROR: Could not get URL from data at \(urlString). \(error.localizedDescription)")
}
}
}
struct ContentView: View {
#StateObject var creatures = Creatures()
var body: some View {
NavigationStack {
List {
ForEach(0..<creatures.creatureArray.count, id: \.self) { index in
NavigationLink {
Text(creatures.creatureArray[index].name)
} label: {
Text("\(index+1). \(creatures.creatureArray[index].name)")
}
.onAppear() {
print("index = \(index+1)")
if index == creatures.creatureArray.count-1 && creatures.urlString.hasPrefix("http") {
Task {
await creatures.getData()
}
}
}
}
}
.toolbar {
ToolbarItem (placement:.status) {
Text("\(creatures.creatureArray.count) of \(creatures.count)")
}
}
}
.task {
await creatures.getData()
}
}
}
Here's a sample of the output. The triple dots simply indicate order printed as expected:
🕸 We are accessing the url https://pokeapi.co/api/v2/pokemon/
index = 1
index = 2
index = 3
…
index = 37
index = 38
index = 39
index = 40
🕸 We are accessing the url https://pokeapi.co/api/v2/pokemon/?offset=40&limit=20
index = 41
index = 44
Try my fully functional example code that fetches the pokemons data as required.
The code gets the server response with the results when the PokeListView first appears (in .task {...}).
Then, as the user scrolls to the bottom of the current list, another page is fetched, until
all data is presented.
The new page fetching is triggered by checking for the last creature id displayed and if more data is available.
This is the crux of the paging. Note, you can adjust to trigger before the last creature is displayed.
As the user tap on any one of the creatures name, the details view is presented.
As the PokeDetailsView appears, the details are fetched from the server or from cache.
This alleviates the server burden.
The ApiService manages all server processing. With this approach
you are not fetching all the details before hand, only as required.
Since you are fetching data from a remote server, there will be times when you will see the progress view,
as it takes somethimes to download the data.
struct ContentView: View {
#StateObject var apiService = ApiService()
var body: some View {
PokeListView()
.environmentObject(apiService)
}
}
struct PokeListView: View {
#EnvironmentObject var apiService: ApiService
var body: some View {
NavigationStack {
List(apiService.pokeList.results) { pokemon in
NavigationLink(pokemon.name, value: pokemon.url)
// check if need to paginate
if let lastPoke = apiService.pokeList.results.last {
if pokemon.id == lastPoke.id && apiService.pokeList.next.hasPrefix("https") {
ProgressView()
.task {
do {
try await apiService.getPokemonList()
} catch {
print("---> refresh error: \(error)")
}
}
}
}
}
.navigationDestination(for: String.self) { urlString in
PokeDetailsView(urlString: urlString)
}
}
.environmentObject(apiService)
.task {
do {
try await apiService.getPokemonList()
} catch{
print(error)
}
}
}
}
struct PokeDetailsView: View {
#EnvironmentObject var apiService: ApiService
#State var urlString: String
#State var poky: Pokemon?
var body: some View {
VStack {
Text(poky?.name ?? "no name")
Text("height: \(poky?.height ?? 0)")
// ... other info
}
.task {
do {
poky = try await apiService.getPokemon(from: urlString)
} catch{
print(error)
}
}
}
}
class ApiService: ObservableObject {
var serverUrl = "https://pokeapi.co/api/v2/pokemon?limit=20&offset=0"
// the response from the server with the list of names and urls in `results`
#Published var pokeList: PokemonList = PokemonList(count: 0, results: [])
// dictionary store of Pokemons details [urlString:Pokemon]
#Published var pokemonStore: [String : Pokemon] = [:]
func getPokemonList() async throws {
guard let url = URL(string: serverUrl) else { return }
let (data, _) = try await URLSession.shared.data(from: url)
Task{#MainActor in
let morePoke = try JSONDecoder().decode(PokemonList.self, from: data)
self.pokeList.count = morePoke.count // <-- here
self.pokeList.next = morePoke.next
self.serverUrl = morePoke.next
self.pokeList.results.append(contentsOf: morePoke.results)
}
}
func getPokemon(from urlString: String) async throws -> Pokemon? {
if let poky = pokemonStore[urlString] {
// if already have it
return poky
} else {
// fetch it from the server
guard let url = URL(string: urlString) else { return nil }
let (data, _) = try await URLSession.shared.data(from: url)
do {
let poky = try JSONDecoder().decode(Pokemon.self, from: data)
Task{#MainActor in
// store it for later use
pokemonStore[urlString] = poky
}
return poky
} catch {
return nil
}
}
}
}
// MARK: - PokemonList
struct PokemonList: Codable {
var count: Int // <-- here
var next: String
var results: [ListItem] // <-- don't use the word Result
init(count: Int, results: [ListItem], next: String = "") {
self.count = count
self.results = results
self.next = next
}
}
// MARK: - ListItem
struct ListItem: Codable, Identifiable {
let id = UUID()
let name: String
let url: String
enum CodingKeys: String, CodingKey {
case name, url
}
}
struct HeldItem: Codable {
let item: Species
let versionDetails: [VersionDetail]
enum CodingKeys: String, CodingKey {
case item
case versionDetails = "version_details"
}
}
struct VersionDetail: Codable {
let rarity: Int
let version: Species
}
// MARK: - Pokemon
struct Pokemon: Codable, Identifiable {
let abilities: [Ability]
let baseExperience: Int
let forms: [Species]
let gameIndices: [GameIndex]
let height: Int
let heldItems: [HeldItem]
let id: Int
let isDefault: Bool
let locationAreaEncounters: String
let moves: [Move]
let name: String
let order: Int
let pastTypes: [String]
let species: Species
let sprites: Sprites
let stats: [Stat]
let types: [TypeElement]
let weight: Int
enum CodingKeys: String, CodingKey {
case abilities
case baseExperience = "base_experience"
case forms
case gameIndices = "game_indices"
case height
case heldItems = "held_items"
case id
case isDefault = "is_default"
case locationAreaEncounters = "location_area_encounters"
case moves, name, order
case pastTypes = "past_types"
case species, sprites, stats, types, weight
}
}
// MARK: - Ability
struct Ability: Codable {
let ability: Species
let isHidden: Bool
let slot: Int
enum CodingKeys: String, CodingKey {
case ability
case isHidden = "is_hidden"
case slot
}
}
// MARK: - Species
struct Species: Codable {
let name: String
let url: String
}
// MARK: - GameIndex
struct GameIndex: Codable {
let gameIndex: Int
let version: Species
enum CodingKeys: String, CodingKey {
case gameIndex = "game_index"
case version
}
}
// MARK: - Move
struct Move: Codable {
let move: Species
let versionGroupDetails: [VersionGroupDetail]
enum CodingKeys: String, CodingKey {
case move
case versionGroupDetails = "version_group_details"
}
}
// MARK: - VersionGroupDetail
struct VersionGroupDetail: Codable {
let levelLearnedAt: Int
let moveLearnMethod, versionGroup: Species
enum CodingKeys: String, CodingKey {
case levelLearnedAt = "level_learned_at"
case moveLearnMethod = "move_learn_method"
case versionGroup = "version_group"
}
}
// MARK: - GenerationV
struct GenerationV: Codable {
let blackWhite: Sprites
enum CodingKeys: String, CodingKey {
case blackWhite = "black-white"
}
}
// MARK: - GenerationIv
struct GenerationIv: Codable {
let diamondPearl, heartgoldSoulsilver, platinum: Sprites
enum CodingKeys: String, CodingKey {
case diamondPearl = "diamond-pearl"
case heartgoldSoulsilver = "heartgold-soulsilver"
case platinum
}
}
// MARK: - Versions
struct Versions: Codable {
let generationI: GenerationI
let generationIi: GenerationIi
let generationIii: GenerationIii
let generationIv: GenerationIv
let generationV: GenerationV
let generationVi: [String: Home]
let generationVii: GenerationVii
let generationViii: GenerationViii
enum CodingKeys: String, CodingKey {
case generationI = "generation-i"
case generationIi = "generation-ii"
case generationIii = "generation-iii"
case generationIv = "generation-iv"
case generationV = "generation-v"
case generationVi = "generation-vi"
case generationVii = "generation-vii"
case generationViii = "generation-viii"
}
}
// MARK: - Sprites
class Sprites: Codable {
let backDefault: String
let backFemale: String?
let backShiny: String
let backShinyFemale: String?
let frontDefault: String
let frontFemale: String?
let frontShiny: String
let frontShinyFemale: String?
let other: Other?
let versions: Versions?
let animated: Sprites?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backFemale = "back_female"
case backShiny = "back_shiny"
case backShinyFemale = "back_shiny_female"
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
case other, versions, animated
}
}
// MARK: - GenerationI
struct GenerationI: Codable {
let redBlue, yellow: RedBlue
enum CodingKeys: String, CodingKey {
case redBlue = "red-blue"
case yellow
}
}
// MARK: - RedBlue
struct RedBlue: Codable {
let backDefault, backGray, backTransparent, frontDefault: String
let frontGray, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backGray = "back_gray"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontGray = "front_gray"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIi
struct GenerationIi: Codable {
let crystal: Crystal
let gold, silver: Gold
}
// MARK: - Crystal
struct Crystal: Codable {
let backDefault, backShiny, backShinyTransparent, backTransparent: String
let frontDefault, frontShiny, frontShinyTransparent, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case backShinyTransparent = "back_shiny_transparent"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontShinyTransparent = "front_shiny_transparent"
case frontTransparent = "front_transparent"
}
}
// MARK: - Gold
struct Gold: Codable {
let backDefault, backShiny, frontDefault, frontShiny: String
let frontTransparent: String?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIii
struct GenerationIii: Codable {
let emerald: Emerald
let fireredLeafgreen, rubySapphire: Gold
enum CodingKeys: String, CodingKey {
case emerald
case fireredLeafgreen = "firered-leafgreen"
case rubySapphire = "ruby-sapphire"
}
}
// MARK: - Emerald
struct Emerald: Codable {
let frontDefault, frontShiny: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontShiny = "front_shiny"
}
}
// MARK: - Home
struct Home: Codable {
let frontDefault: String
let frontFemale: String?
let frontShiny: String
let frontShinyFemale: String?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
}
}
// MARK: - GenerationVii
struct GenerationVii: Codable {
let icons: DreamWorld
let ultraSunUltraMoon: Home
enum CodingKeys: String, CodingKey {
case icons
case ultraSunUltraMoon = "ultra-sun-ultra-moon"
}
}
// MARK: - DreamWorld
struct DreamWorld: Codable {
let frontDefault: String
let frontFemale: String?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
}
}
// MARK: - GenerationViii
struct GenerationViii: Codable {
let icons: DreamWorld
}
// MARK: - Other
struct Other: Codable {
let dreamWorld: DreamWorld
let home: Home
let officialArtwork: OfficialArtwork
enum CodingKeys: String, CodingKey {
case dreamWorld = "dream_world"
case home
case officialArtwork = "official-artwork"
}
}
// MARK: - OfficialArtwork
struct OfficialArtwork: Codable {
let frontDefault: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
}
}
// MARK: - Stat
struct Stat: Codable {
let baseStat, effort: Int
let stat: Species
enum CodingKeys: String, CodingKey {
case baseStat = "base_stat"
case effort, stat
}
}
// MARK: - TypeElement
struct TypeElement: Codable {
let slot: Int
let type: Species
}

Using Searchbar to dynamically show data in SwiftUI from an Array

I need some help trying to work with .searchable in Swift UI.
I am trying to present a few pieces of data that are associated with the search string.
For example, the user searches for a certain description, and the code/price is also presented in the row. I do not want to present the data on a new view, just dynamically as the user is typing.
The Row with values
The problem I am having is that I cannot find a way to manipulate the computed value "search results" and also the generic "named: in the ForEach loop.
I understand the "search results" is an array but I can't a way to access it.
The removalAll was added because every time I went back to this view, it duplicated the list every time a character was typed in the search bar.
I have the data as an enum, and stored in an array because at the time it made sense in case I needed to iterate over the data.
Any advice or suggestions to do this?
Thank you in advance!
Here is the main view:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
storedOtherWoundsNames.removeAll()
self.createOtherWound()
return
NavigationView {
List {
ForEach(searchResults, id: \.self) { named in
VStack {
VStack {
Text(named) //search result
.frame(maxWidth: .infinity, alignment: .leading)
.font(.caption)
}
HStack {
Text("\(named) Code") //want to display from the array "storedOtherWoundsCodes"
.frame(maxWidth: .infinity, alignment: .leading)
.font(.caption)
Text("\(named) Value") //want to display from the array "storedOtherWoundsValues"
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.padding(.all)
.border(Color.gray)
}
}
}
.searchable(text: $searchText)
}
var searchResults: [String] {
if searchText.isEmpty {
storedOtherWoundsNames.removeAll()
return storedOtherWoundsNames
} else {
return storedOtherWoundsNames.filter{$0.contains(searchText)
}
}
}
func createOtherWound() { //takes enum and places it in array
for otherWoundName in allOtherWoundsNames {
storedOtherWoundsNames.append(otherWoundName.rawValue)
}
for otherWoundCode in allOtherWoundsCodes {
storedOtherWoundsCodes.append(otherWoundCode.rawValue)
}
for otherWoundValue in allOtherWoundsValues {
storedOtherWoundsValues.append(otherWoundValue.rawValue)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the data if it helps:
enum OtherWoundsName: String, CaseIterable {
case secondaryclosure_name = "Secondary Closure"
case musclerepair_name = "Muscle Repair (including skin)"
case woundone_name = "Debride Wound/Ulcer - One"
case woundtwo_name = "Debride Wound/Ulcer - Two"
case woundthree_name = "Debride Wound/Ulcer - Three"
case woundfourmore_name = "Debride Wound/Ulcer - Four Plus"
case debrideone_name = "Debride into Tendon/Bone/Bursa/Bone"
case debridetwo_name = "Debride into Tendon/Bone/Bursa/Bone - Two or More"
case extensortendon_name = "Extensor Tendon Repair"
case flexortendor_name = "Flexor Tendon Repair"
case fbskin_name = "FB Skin"
case fbskinsedation_name = "FB Skin with Sedation"
case fbdermissedation_name = "FB Dermis with Sedation"
case flapface5_name = "Advancement Flap: Neck Up < 5.1cm"
case flapother5_name = "Advancement Flap: Other < 5.1cm"
case flapface5plus_name = "Advancement Flap: Neck up > 10.1cm"
case flapother5plus_name = "Advancement Flap: Other > 10.1.cm"
case fingerdebride_name = "Finger Debridement"
case handdebride_name = "Hand Debridement"
case facedebride_name = "Face Debridement"
case otherdebride_name = "Other per % BSA"
case ampulatephalanx_name = "Amputate Phalanx"
case revisionfingertip_name = "Revision of Amputation Finger Tip"
}
enum OtherWoundsCode: String, CaseIterable {
case secondaryclosure_code = "Z738"
case musclerepair_code = "R525"
case woundone_code = "Z080"
case woundtwo_code = "Z081"
case woundthree_code = "Z082"
case woundfourmore_code = "Z083"
case debrideone_code = "Z084"
case debridetwo_code = "Z085"
case extensortendon_code = "R578"
case flexortendor_code = "R585"
case fbskin_code = "Z114"
case fbskinsedation_code = "Z115"
case fbdermissedation_code = "R517"
case flapface5_code = "R011"
case flapother5_code = "R002"
case flapface5plus_code = "R012"
case flapother5plus_code = "R003"
case fingerdebride_code = "R660"
case handdebride_code = "R661"
case facedebride_code = "R662"
case otherdebride_code = "R637"
case ampulatephalanx_code = "R606"
case revisionfingertip_code = "R629"
}
enum OtherWoundsValue: Double, CaseIterable {
case secondaryclosure_value = 97.35
case musclerepair_value = 88.60
case woundone_value = 20
case woundtwo_value = 30
case woundthree_value = 45
case woundfourmore_value = 60
case debrideone_value = 60.001
case debridetwo_value = 90
case extensortendon_value = 164.10
case flexortendor_value = 307.60
case fbskin_value = 25.20
case fbskinsedation_value = 88.80
case fbdermissedation_value = 107.70
case flapface5_value = 89.85
case flapother5_value = 67.40
case flapface5plus_value = 247.15
case flapother5plus_value = 161.75
case fingerdebride_value = 28.90
case handdebride_value = 47.85
case facedebride_value = 28.901
case otherdebride_value = 29.68
case ampulatephalanx_value = 161.45
case revisionfingertip_value = 241.55
}
let allOtherWoundsNames = OtherWoundsName.allCases
var storedOtherWoundsNames: [String] = []
let allOtherWoundsCodes = OtherWoundsCode.allCases
var storedOtherWoundsCodes: [String] = []
let allOtherWoundsValues = OtherWoundsValue.allCases
var storedOtherWoundsValues: [Double] = []
Try this approach as shown in the example code, where the data is in an array of structs ([DataModel]), and
the search returns a filtered array that can be used to "access" the name, code and value
of each item.
struct ContentView: View {
#State private var searchText = ""
#State var items = [DataModel]()
var searchResults: [DataModel] {
searchText.isEmpty ? [] : items.filter{$0.name.contains(searchText)}
// or alternatively
// searchText.isEmpty ? items : items.filter{$0.name.contains(searchText)}
}
var body: some View {
NavigationView {
List {
ForEach(searchResults) { item in
VStack {
Text(item.name).font(.caption)
HStack {
Text(item.code).font(.caption).foregroundColor(.red)
Text("\(item.value)").foregroundColor(.blue)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.all)
.border(Color.gray)
}
}
}
.searchable(text: $searchText)
.onAppear {
items = [DataModel(name: "Secondary Closure", code: "Z738", value: 97.35),
DataModel(name: "Muscle Repair (including skin)", code: "R525", value: 88.60),
DataModel(name: "Debride Wound/Ulcer - One", code: "Z080", value: 20)]
// etc...
}
}
}
struct DataModel: Identifiable {
let id = UUID()
var name: String
var code: String
var value: Double
}

Code for textfield character limit isn't working(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.

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()