SwiftUI Picker in Form does not show checkmark - ios

I have a Picker embedded in Form, however I can't get it working that it shows a checkmark and the selected value in the form.
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
ForEach(0 ..< codes.count) {
Text(self.codes[$0]).tag($0)
}
}
}
}
}

TL;DR
Your variable currencyCode does not match the type of the ID for each element in your ForEach. Either iterate over the codes in your ForEach, or pass your Picker an index.
Below are three equivalent examples. Notice that the #State variable which is passed to Picker always matches the ID of element that the ForEach iterates over:
Also note that I have picked a default value for the #State variable which is not in the array ("", -1, UUID()), so nothing is shown when the form loads. If you want a default option, just make that the default value for your #State variable.
Example 1: Iterate over codes (i.e. String)
struct ContentView: View {
#State private var currencyCode: String = ""
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
// ID is a String ----v
ForEach(codes, id: \.self) { (string: String) in
Text(string)
}
}
}
}
}
}
}
Example 2: Iterate over indices (i.e. Int)
struct ContentView: View {
#State private var selectedIndex: Int = -1
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedIndex, label: Text("Currency")) {
// ID is an Int --------------v
ForEach(codes.indices, id: \.self) { (index: Int) in
Text(self.codes[index])
}
}
}
}
}
}
}
Example 3: Iterate over an identifiable struct by its ID type (i.e. UUID)
struct Code: Identifiable {
var id = UUID()
var value: String
init(_ value: String) {
self.value = value
}
}
struct ContentView: View {
#State private var selectedUUID = UUID()
var codes = [Code("EUR"), Code("GBP"), Code("USD")]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedUUID, label: Text("Currency")) {
// ID is a UUID, because Code conforms to Identifiable
ForEach(self.codes) { (code: Code) in
Text(code.value)
}
}
}
}
}
}
}

It's difficult to say, what you are doing wrong, because your example doesn't include the declaration of codes or currencyCode. I suspect that the problem is with the binding being of a different type than the tag you are setting on a picker (which is an Int in your case).
Anyway, this works:
struct ContentView: View {
let codes = Array(CurrencyCode.allCases)
#State private var currencyCode: CurrencyCode?
var body: some View {
NavigationView {
Form {
Section {
Picker("Currency",
selection: $currencyCode) {
ForEach(codes, id: \.rawValue) {
Text($0.rawValue).tag(Optional<CurrencyCode>.some($0))
}
}
}
}
}
}
}
enum CurrencyCode: String, CaseIterable {
case eur = "EUR"
case gbp = "GBP"
case usd = "USD"
}

Related

How to loop HashMap style in the View in SWIFTUI

var someProtocol = [SurveyItems : [Surveys]]()
sectionLabels.forEach{ a in
var finalSurveys = [Surveys]()
surveys.forEach{ b in
if a.groupHeader == b.group_survey {
finalSurveys.append(b)
}
someProtocol[a] = finalSurveys
}
}
I wanted to use that someProtocol to dynamically display the label section and the surveys under that section.
for (Surveys, SurveyItems) in someProtocol {
Text(Surveys.sectionTitle)
for survey in SurveyItems {
Text(survey.label)
}
}
I tried ViewBuider but getting some error.
To loop and display your someProtocol dictionary in a View, try this example code:
Adjust the code for your own purpose. Note that in a SwiftUI View you need to use a ForEach not the "normal" swift for x in ... to loop over a sequence.
struct ContentView: View {
#State var someProtocol = [SurveyItems : [Surveys]]()
var body: some View {
List(Array(someProtocol.keys), id: \.self) { key in
VStack {
if let surveys = someProtocol[key] {
Text(key.title).foregroundColor(.red)
ForEach(surveys, id: \.self) { survey in
Text("survey \(survey.label)")
}
}
}
}
.onAppear {
// for testing
someProtocol[SurveyItems(id: "1", number: 1, title: "title-1")] = [Surveys(id: "s1", label: "label-1"), Surveys(id: "s2", label: "label-2")]
someProtocol[SurveyItems(id: "2", number: 2, title: "title-2")] = [Surveys(id: "s3", label: "label-3")]
}
}
}
struct SurveyItems: Identifiable, Hashable {
let id: String
let number: Int
var title: String
}
struct Surveys: Identifiable, Hashable {
let id: String
let label: String
}

SwiftUI: Checkmarks disappear when changing from one view to another using NavigationLink

I'm trying to make an app that is displaying lists with selections/checkmarks based on clicked NavigationLink. The problem I encountered is that my selections disappear when I go back to main view and then I go again inside the NavigationLink. I'm trying to save toggles value in UserDefaults but it's not working as expected. Below I'm pasting detailed and main content view.
Second view:
struct CheckView: View {
#State var isChecked:Bool = false
#EnvironmentObject var numofitems: NumOfItems
var title:String
var count: Int=0
var body: some View {
HStack{
ScrollView {
Toggle("\(title)", isOn: $isChecked)
.toggleStyle(CheckToggleStyle())
.tint(.mint)
.onChange(of: isChecked) { value in
if isChecked {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
UserDefaults.standard.set(self.isChecked, forKey: "locationToggle")
}.onTapGesture {
}
.onAppear {
self.isChecked = UserDefaults.standard.bool(forKey: "locationToggle")
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
Main view:
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach(menu){
section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach(section.items) { item in
CheckView( title: item.name)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
ItemsSection:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria"
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola"
}
Model:
struct ItemsSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [CountriesItem]
}
struct CountriesItem: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
}
As allready stated in the comment you have to relate the isChecked property to the CountryItem itself. To get this to work i have changed the model and added an isChecked property. You would need to add this to the JSON by hand if the JSON allread exists.
struct CheckView: View {
#EnvironmentObject var numofitems: NumOfItems
//use a binding here as we are going to manipulate the data coming from the parent
//and pass the complete item not only the name
#Binding var item: CountriesItem
var body: some View {
HStack{
ScrollView {
//use the name and the binding to the item itself
Toggle("\(item.name)", isOn: $item.isChecked)
.toggleStyle(.button)
.tint(.mint)
// you now need the observe the isChecked inside of the item
.onChange(of: item.isChecked) { value in
if value {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
}.onTapGesture {
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach($menu){ // from here on you have to pass a binding on to the decendent views
// mark the $ sign in front of the property name
$section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach($section.items) { $item in
//Pass the complete item to the CheckView not only the name
CheckView(item: $item)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
Example JSON:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria",
"isChecked": false
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola",
"isChecked": false
}
]
}
]
Remarks:
The aproach with JSON and storing this in the bundle will prevent you from persisting the isChecked property between App launches. Because you cannot write to the Bundle from within your App. The choice will persist as long as the App is active but will be back to default as soon as you either reinstall or force quit it.
As already mentioned in the comment, I don'r see where you read back from UserDefaults, so whatever gets stored there, you don't read it. But even if so, each Toggle is using the same key, so you are overwriting the value.
Instead of using the #State var isChecked, which is used just locally, I'd create another struct item which gets the title from the json and which contains a boolean that gets initialized with false.
From what I understood, I assume a solution could look like the following code. Just a few things:
I am not sure how your json looks like, so I am not loading from a json, I add ItemSections Objects with a title and a random number of items (actually just titles again) with a function.
Instead of a print with the number of checked toggles, I added a text output on the UI. It shows you on first page the number of all checked toggles.
Instead of using UserDefaults I used #AppStorage.
To make that work you have to make Array conform to RawRepresentable you achieve that with the following code/extension (just add it once somewhere in your project)
Maybe you should thing about a ViewModel (e.g. ItemSectionViewModel), to load the data from the json and provide it to the views as an #ObservableObject.
The code for the views:
//
// CheckItem.swift
// CheckItem
//
// Created by Sebastian on 24.08.22.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack() {
CheckItemView()
}
}
}
struct CheckItemView: View {
let testStringForTestData: String = "Check Item Title"
#AppStorage("itemSections") var itemSections: [ItemSection] = []
func addCheckItem(title: String, numberOfItems: Int) {
var itemArray: [Item] = []
for i in 0...numberOfItems {
itemArray.append(Item(title: "item \(i)"))
}
itemSections.append(ItemSection(title: title, items: itemArray))
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
var body: some View {
NavigationView{
VStack() {
List(){
ForEach(itemSections.indices, id: \.self){ id in
NavigationLink(destination: ItemSectionDetailedView(items: $itemSections[id].items)) {
Text(itemSections[id].title)
}
.padding()
}
}
Text("Number of checked items: \(self.getSelectedItemsCount())")
.padding()
Button(action: {
self.addCheckItem(title: testStringForTestData, numberOfItems: Int.random(in: 0..<4))
}) {
Text("Add Item")
}
.padding()
}
}
}
}
struct ItemSectionDetailedView: View {
#Binding var items: [Item]
var body: some View {
ScrollView() {
ForEach(items.indices, id: \.self){ id in
Toggle(items[id].title, isOn: $items[id].isOn)
.padding()
}
}
}
}
struct ItemSection: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var items: [Item]
}
struct Item: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var isOn: Bool = false
}
Here the adjustment to work with #AppStorage:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}

Why my SwiftUI List is not getting updated?

I use a SwiftUI List and pass a String to a different view via a Binding.
But the list get not updated when I went back.
Here is my example:
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: String?
#State private var showSheet = false
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = list[i]
showSheet.toggle()
}) {
Text(list[i])
}
}
}
.sheet(isPresented: $showSheet, content: {
DetailView(input: $item)
})
}
}
And the detail page:
struct DetailView: View {
#Binding var input: String?
var body: some View {
Text(input ?? "")
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
What I want to achieve is, that on every Item I click, I see the detail page. After that the element should change to "changed". But this does not happen. Why?
You update item but not list, so don't see any result. Here is corrected variant - store selected index and pass binding to list by index.
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var list = ["a", "b", "c"]
#State private var item: Int?
var body: some View {
List {
ForEach(list.indices) { i in
Button(action: {
item = i
}) {
Text(list[i])
}
}
}
.sheet(item: $item, content: { i in
DetailView(input: $list[i])
})
}
}
extension Int: Identifiable {
public var id: Self { self }
}
struct DetailView: View {
#Binding var input: String
var body: some View {
Text(input)
.onDisappear{
print("changed to changed")
input = "changed"
}
}
}
I recommend you use .sheet(item:content:) instead of .sheet(isPresented:content:)
struct ContentView: View {
#State private var items = ["a", "b", "c"]
#State private var selectedIndex: Int?
var body: some View {
List {
ForEach(items.indices) { index in
Button(action: {
selectedIndex = index
}) {
Text(items[index])
}
}
}
.sheet(item: $selectedIndex) { index in
DetailView(item: $items[index])
}
}
}
struct DetailView: View {
#Binding var item: String
var body: some View {
Text(item)
.onDisappear {
print("changed to changed")
item = "changed"
}
}
}
This will, however, require the selectedIndex to conform to Identifiable.
You can either create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}
or create a custom struct for your data (and conform to Identifiable).

Passing #Binding variable from protocol var to if in SwiftUI

I have a model like this:
protocol PurchasableProduct {
var randomId: String { get }
}
class Cart: Identifiable {
var items: [PurchasableProduct]
init(items: [PurchasableProduct]) {
self.items = items
}
}
class Product: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
var notes: String = ""
}
class DigitalGood: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
}
where items conform to protocol PurchasableProduct.
I want to build a View that shows cart like this:
struct CartView: View {
#State var cart: Cart
var body: some View {
List {
ForEach(cart.items.indices) { index in
CartItemView(item: self.$cart.items[index])
}
}
}
}
where CartItemView is:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
Text("Product")
} else {
Text("Digital Good")
}
}
}
}
That's working and give me result as
This (screenshot)
But I want to extend this a but more that my items element can be passed as a binding variable lets say as:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
VStack {
TextField("add notes", text: (self.$item as! Product).notes) // ❌ Cannot convert value of type 'String' to expected argument type 'Binding<String>'
TextField("add notes", text: (self.$item as! Binding<Product>).notes) // ⚠️ Cast from 'Binding<PurchasableProduct>' to unrelated type 'Binding<Product>' always fails
}
} else {
Text("Digital Good")
}
}
}
}
What I'm trying to achieve is:
I have a collection of items that depends on a class should be drawn differently
Items have different editable sync that should be binded into CartView
Not sure if thats syntax issue or my approach issue ... how to cast this on body to get the correct view based on type?
You may create a custom binding:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var product: Binding<Product>? {
guard item is Product else { return nil }
return .init(
get: {
self.$item.wrappedValue as! Product
}, set: {
self.$item.wrappedValue = $0
}
)
}
var body: some View {
VStack {
if product != nil {
TextField("add notes", text: product!.notes)
} else {
Text("Digital Good")
}
}
}
}

SwiftUI set a Binding from the String result of a picker

I am tearing out my hair trying to figure out how to bind the picked value in my SwiftUI view:
The picker needs to be bound to the Int returned from the tags. I need to covert this Int to the String and set the Binding. How?
struct ContentView: View {
#Binding var operatorValueString:String
var body: some View {
Picker(selection: queryType, label: Text("Query Type")) {
ForEach(0..<DM.si.operators.count) { index in
Text(DM.si.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
How and where can I set my operatorValueString ?
operatorValueString = DM.si.operators[queryType] //won't compile.
You can achieve the result, using your own custom binding that sets the string, whenever the picker's selection changes:
struct ContentView: View {
#State private var operatorString = ""
var body: some View {
VStack {
Subview(operatorValueString: $operatorString)
Text("Selected: \(operatorString)")
}
}
}
struct Subview: View {
#Binding var operatorValueString: String
#State private var queryType: Int = 0
let operators = ["OR", "AND", "NOT"]
var body: some View {
let binding = Binding<Int>(
get: { self.queryType },
set: {
self.queryType = $0
self.operatorValueString = self.operators[self.queryType]
})
return Picker(selection: binding, label: Text("Query Type")) {
ForEach(operators.indices) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}

Resources