SwiftUI TextField Binding to a simple Model (non-class sturct) - swift

I have a simple struct, that is decodable / codable and hashable.
public struct Field: Codable, Hashable {
let key: String
enum CodingKeys: String, CodingKey {
case key
}
}
In my SwiftUI, I'm creating an array, where I want to add and remove and Bind to the Struct values. But I am getting errors, that Struct is not Binding.
let decodableJSON = """
{
"key": ""
}
"""
let settableInit: Field = try! JSONDecoder().decode(Field.self, from: decodableJSON.data(using: .utf8)!)
struct Test_view: View {
#State
var settableFields: [Field] = [settableInit]
var body: some View {
ForEach(settableFields, id: \.key) { (settableField: Field) in
TextField("Key", text: settableField.key)
}
But I get an error, that settableField is not Binding. I have tried adding the settableInit as an #ObservableObject to the main Swift View, but it still doesn't work.
Is there a way, to have the View bind to Struct properties, and have TextField change these properties? It feels like something so trivial, but for some reason undoable for me.
Thank you for any pointers!

In Xcode13 you can use the new element binding syntax:
public struct Field: Codable, Hashable {
var key: String
enum CodingKeys: String, CodingKey {
case key
}
}
struct Demo: View {
#State var settableFields: [Field] = [Field(key: "1"), Field(key: "2")]
var body: some View {
ForEach($settableFields, id: \.key) { $settableField in
TextField("Key", text: $settableField.key)
}
}
}
In earlier versions of Xcode you could use the indices of the array:
struct Demo: View {
#State var settableFields: [Field] = [Field(key: "1"), Field(key: "2")]
var body: some View {
ForEach(settableFields.indices, id: \.self) { index in
TextField("Key", text: $settableFields[index].key)
}
}
}

Related

How to use the Transferable protocol in SwiftUI tables

I am building a macOS app in Swift, using SwiftUI. I have the following struct:
struct focusArticle: Identifiable {
let category: String
let headline: String
let body: String
let publishedDate: String
let source: String
var id = UUID()
}
I have created an array with instances of focusArticle that are being used to populate a Table element, like so
struct ArticleView: View {
#State private var selectedArticles = Set<focusArticle.ID>()
var body: some View {
Table(articles, selection: $selectedArticles) {
TableColumn("Category", value: \.category)
.width(70)
TableColumn("Headline", value: \.headline)
.width(350)
TableColumn("Published", value: \.publishedDate)
}
.frame(width: 600, height: 200)
}
}
I want to make each row in this table draggable, so I can drag-drop from this table, to a similar table in another view. From reading Apple's documentation, it seems like I need to do two things.
Make my focusArticle struct conform to the Transferable protocol.
Use the .draggable modifier on the table to make the elements draggable.
I can't seem to do either of these things. Copying over the example code from the documentation into my struct makes it throw an error — Static method 'buildBlock' requires that 'String' conform to 'TransferRepresentation'. Adding a .draggable modifier to the table throws an expected error: Instance method 'draggable' requires that '[focusArticle]' conform to 'Transferable'
EDIT: This is one of the ways I have tried to make focusArticles confirm to Transferable.
struct focusArticle: Identifiable, Codable, Transferable {
let category: String
let headline: String
let body: String
let publishedDate: String
let source: String
var id = UUID()
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .focusArticle)
}
}
This shows an error Type 'UTType' has no member 'focusArticle'
You need to add a UTType, as an extension like this:
extension UTType {
static var focusArticle = UTType(exportedAs: "com.yourdomain.yourapp.focusarticle")
}

Pass struct as generic type and access that generic types properties

I'm working on a Swift package where an option will be to pass in a generic type (Person) and then that GenericStruct can use properties on that type passed in. The issue obviously is that the generic T has no idea what's being passed in. Is there a way to define the property to access on the generic type T?
struct Person: Equatable {
var name: String
var height: Double
}
struct ContentView: View {
#State private var james = Person(name: "James", height: 175.0)
var body: some View {
GenericStruct(person: $james)
}
}
struct GenericStruct<T: Equatable>: View {
#Binding var person: T
var body: some View {
Text(person.name)) // This line.
}
}
I want to specifically pass in which property to access on Person when passing it to GenericStruct. The property won't always be name it could be anything I define within Person. For example:
GenericStruct(person: $james, use: Person.name)
Isn't this exactly a protocol?
protocol NameProviding {
var name: String { get }
}
struct GenericStruct<T: Equatable & NameProviding>: View { ... }
Or is there a more subtle part of the question?
The best way to do this is to pass a String Binding:
struct GenericStruct: View {
#Binding var text: String
var body: some View {
Text(text)) // This line.
}
}
GenericStruct(text: $james.name)
But it is possible with key paths. It's just a bit more awkward and less flexible in this particular case:
// Should be the syntax, but I haven't double-checked it.
struct GenericStruct<T: Equatable>: View {
#Binding var person: T
var use: KeyPath<T, String>
var body: some View {
Text(person[keyPath: use]))
}
}
GenericStruct(person: $james, use: \.name)

issue with list in swift (Xcode 13.0)

import SwiftUI
struct HomeView: View {
var categories:[String:[Sticker]] {
.init(
grouping: stickerData, by: {$0.category.rawValue}
)
}
var body: some View {
NavigationView{
//Error in this line
List (categories.keys.sorted().identified(by: \String.self)){ key in
StickerRow(categoryName: " Sticker \(key)".uppercased(), stickers: self.categories[key]!)
}
}
}
}
it display this error but I don't know what to do, any suggestions?
Value of type '[Dictionary<String, [Sticker]>.Keys.Element]' (aka 'Array') has no member 'identified'
Sticker.swift code
import Foundation
import SwiftUI
struct Sticker: Hashable, Codable, Identifiable {
var id:Int
var name:String
var imageName:String
var category:Category
var description:String
enum Category: String, CaseIterable, Hashable, Codable {
case first = "first"
case second = "second"
}
}
Instead of using identified, you can use id as below:
List(categories.keys.sorted(), id: \.self) { key in
StickerRow(categoryName: " Sticker \(key)".uppercased(), stickers: self.categories[key]!)
}

Change a #Published var via Picker

I would like to change a #Published var through the Picker but I found that it fails to compile.
I have a Language struct:
enum Language: Int, CaseIterable, Identifiable, Hashable {
case en
case zh_hant
var description: String {
switch self {
case.en:
return "English"
case.zh_hant:
return "繁體中文"
}
}
var id: Int {
self.rawValue
}
}
And inside my DataModel, I have published the variable selectedLanguage
final class ModelData: ObservableObject {
#Published var currentLanguage: Language = Language.zh_hant
}
I tried to use a picker for the user to change the language:
#EnvironmentObject var modelData: ModelData
In the body of the view:
Picker("Selected Language", selection: modelData.currentLanguage) {
ForEach(Language.allCases, id: \.rawValue) { language in
Text(language.description).tag(language)
}
}.pickerStyle(SegmentedPickerStyle()).padding(.horizontal)
It says "Cannot convert value of type 'Language' to expected argument type 'Binding'"
How can I modify the currentLanguage in the modelData directly with the picker?
You were very close. You need to use the $ when making a binding to a #Published property like that:
Picker("Selected Language", selection: $modelData.currentLanguage) {
ForEach(Language.allCases, id: \.id) { language in
Text(language.description).tag(language)
}
}.pickerStyle(SegmentedPickerStyle()).padding(.horizontal)
I also changed the id: to \.id rather than \.rawValue which is somewhat meaningless since they resolve to the same thing, but makes a little more sense semantically.

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}